From f02fdfe2d1d3149579b29650d75117190c189cdb Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 14 Mar 2025 21:41:28 -0400 Subject: [PATCH 001/191] add handles for text and image overlays --- python/mujoco/simulate.cc | 50 +++++++++++++++++++++++++++++++++++++++ python/mujoco/viewer.py | 44 +++++++++++++++++++++++++++++++--- simulate/simulate.cc | 18 ++++++++++++++ simulate/simulate.h | 2 ++ 4 files changed, 111 insertions(+), 3 deletions(-) diff --git a/python/mujoco/simulate.cc b/python/mujoco/simulate.cc index 56fe4437..8fdb668c 100644 --- a/python/mujoco/simulate.cc +++ b/python/mujoco/simulate.cc @@ -148,6 +148,50 @@ class SimulateWrapper { void ClearFigures() { simulate_->user_figures_.clear(); } + void SetOverlayText( + const std::vector>& overlay_texts) { + // Collection of [font, gridpos, text1, text2] tuples for overlay text + std::vector> user_overlay_text; + for (const auto& [font, gridpos, text1, text2] : overlay_texts) { + user_overlay_text.push_back(std::make_tuple(font, gridpos, text1, text2)); + } + + // Set them all at once to prevent overlay text flickering. + simulate_->user_text_ = user_overlay_text; + } + + void ClearOverlayText() { simulate_->user_text_.clear(); } + + void SetImages( + const std::vector>>& viewport_images + ) { + // Clear previous images to prevent memory leaks + simulate_->user_images_.clear(); + + for (const auto& [viewport, image] : viewport_images) { + auto buf = image.request(); + if (static_cast(buf.shape[2]) != 3) { + throw std::invalid_argument("image must have 3 channels"); + } + if (buf.ndim != 3) { + throw std::invalid_argument("image must have 3 dimensions (H, W, C)"); + } + + // Calculate size of the image data + size_t height = buf.shape[0]; + size_t width = buf.shape[1]; + size_t size = height * width * 3; + + // Make a copy of the image data to prevent flickering + unsigned char* image_copy = new unsigned char[size]; + std::memcpy(image_copy, buf.ptr, size); + + simulate_->user_images_.push_back(std::make_tuple(viewport, image_copy)); + } + } + + void ClearImages() { simulate_->user_images_.clear(); } + private: mujoco::Simulate* simulate_; std::atomic_int destroyed_ = 0; @@ -249,6 +293,12 @@ PYBIND11_MODULE(_simulate, pymodule) { .def("set_figures", &SimulateWrapper::SetFigures, py::arg("viewports_figures")) .def("clear_figures", &SimulateWrapper::ClearFigures) + .def("overlay_text", &SimulateWrapper::SetOverlayText, + py::arg("overlay_texts")) + .def("clear_overlay_text", &SimulateWrapper::ClearOverlayText) + .def("set_images", &SimulateWrapper::SetImages, + py::arg("viewports_images")) + .def("clear_images", &SimulateWrapper::ClearImages) .def_property_readonly("m", &SimulateWrapper::GetModel) .def_property_readonly("d", &SimulateWrapper::GetData) .def_property_readonly("viewport", &SimulateWrapper::GetViewport) diff --git a/python/mujoco/viewer.py b/python/mujoco/viewer.py index 65852c87..b37355cc 100644 --- a/python/mujoco/viewer.py +++ b/python/mujoco/viewer.py @@ -23,9 +23,8 @@ import queue import sys import threading import time -from typing import Callable, Optional, Tuple, Union +from typing import Callable, List, Optional, Tuple, Union import weakref - import glfw import mujoco from mujoco import _simulate @@ -115,7 +114,7 @@ class Handle: return sim.viewport return None - def set_figures(self, viewports_figures): + def set_figures(self, viewports_figures: List[Tuple[mujoco.MjrRect, mujoco.MjvFigure]]): sim = self._sim() if sim is not None: sim.set_figures(viewports_figures) @@ -125,6 +124,45 @@ class Handle: if sim is not None: sim.clear_figures() + def overlay_text(self, overlay_texts: List[Tuple[int, int, str, str]]): + """ Overlay text on the viewer. + + Args: + overlay_texts: List of tuples of (font, gridpos, text1, text2) + let: + font: Font style from mujoco.mjtFontScale + gridpos: Position of text box from mujoco.mjtGridPos + text1: Left text column + text2: Right text column + """ + sim = self._sim() + if sim is not None: + sim.overlay_text(overlay_texts) + + def clear_overlay_text(self): + sim = self._sim() + if sim is not None: + sim.clear_overlay_text() + + def set_images(self, viewports_images: List[Tuple[mujoco.MjrRect, np.ndarray]]): + sim = self._sim() + if sim is not None: + # Nearest neighbor resize + resize = lambda a, s: a[(np.arange(s[0]) * a.shape[0]) // s[0]][:, (np.arange(s[1]) * a.shape[1]) // s[1]] + resized_viewports_images = [] + for viewport, image in viewports_images: + targ_shape = (viewport.height, viewport.width) + resized = resize(image, targ_shape) + resized = np.flip(resized, axis=0) + resized = np.ascontiguousarray(resized) + resized_viewports_images.append((viewport, resized)) + sim.set_images(resized_viewports_images) + + def clear_images(self): + sim = self._sim() + if sim is not None: + sim.clear_images() + def close(self): sim = self._sim() if sim is not None: diff --git a/simulate/simulate.cc b/simulate/simulate.cc index bf4b4a13..57e3eaaa 100644 --- a/simulate/simulate.cc +++ b/simulate/simulate.cc @@ -546,6 +546,14 @@ void ShowFigure(mj::Simulate* sim, mjrRect viewport, mjvFigure* fig){ mjr_figure(viewport, fig, &sim->platform_ui->mjr_context()); } +void ShowOverlayText(mj::Simulate* sim, mjrRect viewport, int font, int gridpos, std::string text1, std::string text2){ + mjr_overlay(font, gridpos, viewport, text1.c_str(), text2.c_str(), &sim->platform_ui->mjr_context()); +} + +void ShowImage(mj::Simulate* sim, mjrRect viewport, const unsigned char* image) { + mjr_drawPixels(image, nullptr, viewport, &sim->platform_ui->mjr_context()); +} + // load state from history buffer static void LoadScrubState(mj::Simulate* sim) { // get index into circular buffer @@ -2597,6 +2605,16 @@ void Simulate::Render() { ShowFigure(this, viewport, &figure); } + // overlay text + for (auto& [font, gridpos, text1, text2] : this->user_text_) { + ShowOverlayText(this, rect, font, gridpos, text1, text2); + } + + // user images + for (auto& [viewport, image] : this->user_images_) { + ShowImage(this, viewport, image); + } + // finalize this->platform_ui->SwapBuffers(); } diff --git a/simulate/simulate.h b/simulate/simulate.h index cd654192..0bf6ad25 100644 --- a/simulate/simulate.h +++ b/simulate/simulate.h @@ -253,6 +253,8 @@ class Simulate { mjvScene* user_scn = nullptr; mjtByte user_scn_flags_prev_[mjNRNDFLAG]; std::vector> user_figures_; + std::vector> user_text_; + std::vector> user_images_; // OpenGL rendering and UI int refresh_rate = 60; From 86fd31b9b86f1afe1dbf23ef37378fbbb8aca5bb Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 16 Mar 2025 17:44:24 -0400 Subject: [PATCH 002/191] run pyink and isort --- python/mujoco/viewer.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/python/mujoco/viewer.py b/python/mujoco/viewer.py index b37355cc..21a1eeea 100644 --- a/python/mujoco/viewer.py +++ b/python/mujoco/viewer.py @@ -25,10 +25,12 @@ import threading import time from typing import Callable, List, Optional, Tuple, Union import weakref + import glfw +import numpy as np + import mujoco from mujoco import _simulate -import numpy as np if not glfw._glfw: # pylint: disable=protected-access raise RuntimeError('GLFW dynamic library handle is not available') @@ -114,7 +116,9 @@ class Handle: return sim.viewport return None - def set_figures(self, viewports_figures: List[Tuple[mujoco.MjrRect, mujoco.MjvFigure]]): + def set_figures( + self, viewports_figures: List[Tuple[mujoco.MjrRect, mujoco.MjvFigure]] + ): sim = self._sim() if sim is not None: sim.set_figures(viewports_figures) @@ -125,8 +129,8 @@ class Handle: sim.clear_figures() def overlay_text(self, overlay_texts: List[Tuple[int, int, str, str]]): - """ Overlay text on the viewer. - + """Overlay text on the viewer. + Args: overlay_texts: List of tuples of (font, gridpos, text1, text2) let: @@ -138,17 +142,21 @@ class Handle: sim = self._sim() if sim is not None: sim.overlay_text(overlay_texts) - + def clear_overlay_text(self): sim = self._sim() if sim is not None: sim.clear_overlay_text() - def set_images(self, viewports_images: List[Tuple[mujoco.MjrRect, np.ndarray]]): + def set_images( + self, viewports_images: List[Tuple[mujoco.MjrRect, np.ndarray]] + ): sim = self._sim() if sim is not None: # Nearest neighbor resize - resize = lambda a, s: a[(np.arange(s[0]) * a.shape[0]) // s[0]][:, (np.arange(s[1]) * a.shape[1]) // s[1]] + resize = lambda a, s: a[(np.arange(s[0]) * a.shape[0]) // s[0]][ + :, (np.arange(s[1]) * a.shape[1]) // s[1] + ] resized_viewports_images = [] for viewport, image in viewports_images: targ_shape = (viewport.height, viewport.width) From faff1224ba962e1d14570eb790678fc152257faa Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 17 Mar 2025 10:42:11 -0400 Subject: [PATCH 003/191] fix memory leak --- python/mujoco/simulate.cc | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/python/mujoco/simulate.cc b/python/mujoco/simulate.cc index 8fdb668c..868bec28 100644 --- a/python/mujoco/simulate.cc +++ b/python/mujoco/simulate.cc @@ -91,6 +91,7 @@ class SimulateWrapper { void Destroy() { if (simulate_) { + ClearImages(); delete simulate_; simulate_ = nullptr; destroyed_.store(1); @@ -166,7 +167,7 @@ class SimulateWrapper { const std::vector>>& viewport_images ) { // Clear previous images to prevent memory leaks - simulate_->user_images_.clear(); + ClearImages(); for (const auto& [viewport, image] : viewport_images) { auto buf = image.request(); @@ -190,7 +191,13 @@ class SimulateWrapper { } } - void ClearImages() { simulate_->user_images_.clear(); } + void ClearImages() { + // Free memory for each image before clearing the vector + for (const auto& [viewport, image_ptr] : simulate_->user_images_) { + delete[] image_ptr; + } + simulate_->user_images_.clear(); + } private: mujoco::Simulate* simulate_; From 4e206c29c1afe9b0cf0dd879811e2b5fda31bbc3 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Tue, 25 Mar 2025 08:20:14 -0700 Subject: [PATCH 004/191] Keep tree lists updated at all times. This simplifies the compiler logic since an updated tree list was necessary at many stages (e.g. attach and keyframes resizing) and it will be anyway required when computing the mjSpec signature. PiperOrigin-RevId: 740356447 Change-Id: I7f2ec25b27b8d4ca4364801c9a401c40c6d84569 --- src/user/user_model.cc | 72 ++++++++-------------------------------- src/user/user_model.h | 4 ++- src/user/user_objects.cc | 44 +++++++++++++++++++++--- 3 files changed, 56 insertions(+), 64 deletions(-) diff --git a/src/user/user_model.cc b/src/user/user_model.cc index bc12a17e..2a27d430 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -449,9 +449,8 @@ static bool IsPluginActive( mjCModel& mjCModel::operator+=(const mjCModel& other) { // create global lists - mjCBody *world = bodies_[0]; ResetTreeLists(); - MakeLists(world); + MakeTreeLists(); ProcessLists(/*checkrepeat=*/false); // copy all elements not in the tree @@ -500,11 +499,6 @@ mjCModel& mjCModel::operator+=(const mjCModel& other) { nq = nv = na = nu = nmocap = 0; } - // restore to the original state - if (!compiled) { - ResetTreeLists(); - } - PointToLocal(); return *this; } @@ -609,13 +603,11 @@ mjCModel& mjCModel::operator-=(const mjCBody& subtree) { // create global lists in the old model if not compiled if (!oldmodel.IsCompiled()) { - oldmodel.MakeLists(oldmodel.bodies_[0]); oldmodel.ProcessLists(/*checkrepeat=*/false); } // create global lists in this model if not compiled if (!IsCompiled()) { - MakeLists(bodies_[0]); ProcessLists(/*checkrepeat=*/false); } @@ -629,7 +621,7 @@ mjCModel& mjCModel::operator-=(const mjCBody& subtree) { // update global lists ResetTreeLists(); - MakeLists(world); + MakeTreeLists(); ProcessLists(/*checkrepeat=*/false); // check if we have to remove anything else @@ -641,11 +633,6 @@ mjCModel& mjCModel::operator-=(const mjCBody& subtree) { RemoveFromList(sensors_, oldmodel); RemovePlugins(); - // restore to the original state - if (!compiled) { - ResetTreeLists(); - } - return *this; } @@ -742,18 +729,16 @@ void deletefromlist(std::vector* list, mjsElement* element) { // discard all invalid elements from all lists void mjCModel::DeleteElement(mjsElement* el) { - mjCBody *world = nullptr; - if (compiled) { - world = bodies_[0]; - ResetTreeLists(); - } + ResetTreeLists(); switch (el->elemtype) { case mjOBJ_BODY: + MakeTreeLists(); // rebuild lists that were reset at the beginning of the function throw mjCError(nullptr, "bodies cannot be deleted, use detach instead"); break; case mjOBJ_DEFAULT: + MakeTreeLists(); // rebuild lists that were reset at the beginning of the function throw mjCError(nullptr, "defaults cannot be deleted, use detach instead"); break; @@ -818,11 +803,9 @@ void mjCModel::DeleteElement(mjsElement* el) { break; } - if (compiled) { - ResetTreeLists(); // in case of a nested delete - MakeLists(world); - ProcessLists(/*checkrepeat=*/false); - } + ResetTreeLists(); // in case of a nested delete + MakeTreeLists(); + ProcessLists(/*checkrepeat=*/false); } @@ -1020,15 +1003,6 @@ void mjCModel::Clear() { nconmax = -1; nmocap = 0; - // pointer lists created by Compile - bodies_.clear(); - joints_.clear(); - geoms_.clear(); - sites_.clear(); - cameras_.clear(); - lights_.clear(); - frames_.clear(); - // internal variables hasImplicitPluginElem = false; compiled = false; @@ -1486,7 +1460,11 @@ mjSpec* mjCModel::GetSourceSpec() const { //------------------------------- COMPILER PHASES -------------------------------------------------- // make lists of objects in tree: bodies, geoms, joints, sites, cameras, lights -void mjCModel::MakeLists(mjCBody* body) { +void mjCModel::MakeTreeLists(mjCBody* body) { + if (body == nullptr) { + body = bodies_[0]; + } + // add this body if not world if (body != bodies_[0]) { bodies_.push_back(body); @@ -1501,7 +1479,7 @@ void mjCModel::MakeLists(mjCBody* body) { for (mjCFrame *frame : body->frames) frames_.push_back(frame); // recursive call to all child bodies - for (mjCBody* body : body->bodies) MakeLists(body); + for (mjCBody* body : body->bodies) MakeTreeLists(body); } @@ -3657,21 +3635,12 @@ template void mjCModel::RestoreState( // resolve keyframe references void mjCModel::StoreKeyframes(mjCModel* dest) { - bool resetlists = false; - if (this != dest && !key_pending_.empty()) { mju_warning( "Child model has pending keyframes. They will not be namespaced correctly. " "To prevent this, compile the child model before attaching it again."); } - // create tree lists if they are empty, occurs if an uncompiled model is attached - if (bodies_.size() == 1 && geoms_.empty() && sites_.empty() && joints_.empty() && - cameras_.empty() && lights_.empty() && frames_.empty()) { - MakeLists(bodies_[0]); - resetlists = true; - } - // do not change compilation quantities in case the user wants to recompile preserving the state if (!compiled) { SaveDofOffsets(/*computesize=*/true); @@ -3719,10 +3688,6 @@ void mjCModel::StoreKeyframes(mjCModel* dest) { key->spec_mpos_.data(), key->spec_mquat_.data()); } - if (resetlists) { - ResetTreeLists(); - } - if (!compiled) { nq = nv = na = nu = nmocap = 0; } @@ -4054,11 +4019,7 @@ static void warninghandler(const char* msg) { // compiler mjModel* mjCModel::Compile(const mjVFS* vfs, mjModel** m) { if (compiled) { - // clear kinematic tree - mjCBody* world = bodies_[0]; - ResetTreeLists(); Clear(); - bodies_.push_back(world); } CopyFromSpec(); @@ -4105,9 +4066,7 @@ mjModel* mjCModel::Compile(const mjVFS* vfs, mjModel** m) { // deallocate everything allocated in Compile mj_deleteModel(model); mj_deleteData(data); - mjCBody* world = bodies_[0]; Clear(); - bodies_.push_back(world); // save error info errInfo = err; @@ -4354,9 +4313,6 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) { AddKey(); } - // make lists of objects created in kinematic tree - MakeLists(bodies_[0]); - // clear subtreedofs for (int i=0; i < bodies_.size(); i++) { bodies_[i]->subtreedofs = 0; diff --git a/src/user/user_model.h b/src/user/user_model.h index 1078080f..c97216e5 100644 --- a/src/user/user_model.h +++ b/src/user/user_model.h @@ -334,9 +334,11 @@ class mjCModel : public mjCModel_, private mjSpec { // list of active plugins std::vector> active_plugins_; + // make lists of bodies and children + void MakeTreeLists(mjCBody* body = nullptr); + // compile phases void TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs); - void MakeLists(mjCBody* body); // make lists of bodies, geoms, joints, sites void SetNuser(); // set nuser fields void IndexAssets(bool discard); // convert asset names into indices void CheckEmptyNames(); // check empty names diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 18c573d2..a493bf30 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -1231,6 +1231,11 @@ mjCBody* mjCBody::AddBody(mjCDef* _def) { obj->classname = _def ? _def->name : classname; bodies.push_back(obj); + + // recompute lists + model->ResetTreeLists(); + model->MakeTreeLists(); + obj->parent = this; return obj; } @@ -1241,6 +1246,8 @@ mjCBody* mjCBody::AddBody(mjCDef* _def) { mjCFrame* mjCBody::AddFrame(mjCFrame* _frame) { mjCFrame* obj = new mjCFrame(model, _frame ? _frame : NULL); frames.push_back(obj); + model->ResetTreeLists(); + model->MakeTreeLists(); return obj; } @@ -1256,6 +1263,11 @@ mjCJoint* mjCBody::AddFreeJoint() { obj->body = this; joints.push_back(obj); + + // recompute lists + model->ResetTreeLists(); + model->MakeTreeLists(); + return obj; } @@ -1270,6 +1282,11 @@ mjCJoint* mjCBody::AddJoint(mjCDef* _def) { obj->body = this; joints.push_back(obj); + + // recompute lists + model->ResetTreeLists(); + model->MakeTreeLists(); + return obj; } @@ -1284,6 +1301,11 @@ mjCGeom* mjCBody::AddGeom(mjCDef* _def) { obj->body = this; geoms.push_back(obj); + + // recompute lists + model->ResetTreeLists(); + model->MakeTreeLists(); + return obj; } @@ -1298,6 +1320,11 @@ mjCSite* mjCBody::AddSite(mjCDef* _def) { obj->body = this; sites.push_back(obj); + + // recompute lists + model->ResetTreeLists(); + model->MakeTreeLists(); + return obj; } @@ -1312,6 +1339,11 @@ mjCCamera* mjCBody::AddCamera(mjCDef* _def) { obj->body = this; cameras.push_back(obj); + + // recompute lists + model->ResetTreeLists(); + model->MakeTreeLists(); + return obj; } @@ -1326,6 +1358,11 @@ mjCLight* mjCBody::AddLight(mjCDef* _def) { obj->body = this; lights.push_back(obj); + + // recompute lists + model->ResetTreeLists(); + model->MakeTreeLists(); + return obj; } @@ -1347,11 +1384,8 @@ mjCFrame* mjCBody::ToFrame() { std::remove_if(parent->bodies.begin(), parent->bodies.end(), [this](mjCBody* body) { return body == this; }), parent->bodies.end()); - if (model->IsCompiled()) { - mjCBody *world = model->bodies_[0]; - model->ResetTreeLists(); - model->MakeLists(world); - } + model->ResetTreeLists(); + model->MakeTreeLists(); return newframe; } From c931565fdceb9b6ed044cd2499dc4d038334766f Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 25 Mar 2025 09:35:14 -0700 Subject: [PATCH 005/191] Add internal function`mj_tendonDot`: time derivative of tendon Jacobian for one tendon. Notes: - Currently always uses dense math, even for sparse models. This should be easy to change in the future. - Does not support geom wrapping. This is possible but harder, requires derivatives of mju_wrap. PiperOrigin-RevId: 740378741 Change-Id: Id39ef2c4bfbb7ee11ec33c97d7d83140441cdab2 --- src/engine/engine_core_smooth.c | 122 ++++++++++++++++++++ src/engine/engine_core_smooth.h | 3 + test/engine/engine_core_smooth_test.cc | 54 +++++++++ test/engine/testdata/core_smooth/ten_J0.xml | 26 +++++ test/engine/testdata/core_smooth/ten_J1.xml | 29 +++++ test/engine/testdata/core_smooth/ten_J2.xml | 34 ++++++ test/engine/testdata/core_smooth/ten_J3.xml | 66 +++++++++++ 7 files changed, 334 insertions(+) create mode 100644 test/engine/testdata/core_smooth/ten_J0.xml create mode 100644 test/engine/testdata/core_smooth/ten_J1.xml create mode 100644 test/engine/testdata/core_smooth/ten_J2.xml create mode 100644 test/engine/testdata/core_smooth/ten_J3.xml diff --git a/src/engine/engine_core_smooth.c b/src/engine/engine_core_smooth.c index 9b453c3e..009860eb 100644 --- a/src/engine/engine_core_smooth.c +++ b/src/engine/engine_core_smooth.c @@ -861,6 +861,128 @@ void mj_tendon(const mjModel* m, mjData* d) { +// compute time derivative of dense tendon Jacobian for one tendon +void mj_tendonDot(const mjModel* m, mjData* d, int id, mjtNum* Jdot) { + int nv = m->nv; + + // allocate stack arrays + mjtNum *jac1, *jac2, *jacdif, *tmp; + mj_markStack(d); + jac1 = mjSTACKALLOC(d, 3*nv, mjtNum); + jac2 = mjSTACKALLOC(d, 3*nv, mjtNum); + jacdif = mjSTACKALLOC(d, 3*nv, mjtNum); + tmp = mjSTACKALLOC(d, nv, mjtNum); + + // return if tendon id is invalid + if (id < 0 || id >= m->ntendon) { + return; + } + + // clear output + mju_zero(Jdot, nv); + + // fixed tendon has zero Jdot: return + int adr = m->tendon_adr[id]; + if (m->wrap_type[adr] == mjWRAP_JOINT) { + return; + } + + // process spatial tendon + mjtNum divisor = 1; + int wraptype, j = 0; + int num = m->tendon_num[id]; + while (j < num-1) { + // get 1st and 2nd object + int type0 = m->wrap_type[adr+j+0]; + int type1 = m->wrap_type[adr+j+1]; + int id0 = m->wrap_objid[adr+j+0]; + int id1 = m->wrap_objid[adr+j+1]; + + // pulley + if (type0 == mjWRAP_PULLEY || type1 == mjWRAP_PULLEY) { + // get divisor, insert obj=-2 + if (type0 == mjWRAP_PULLEY) { + divisor = m->wrap_prm[adr+j]; + } + + // move to next + j++; + continue; + } + + // init sequence; assume it starts with site + mjtNum wpnt[6]; + mju_copy3(wpnt, d->site_xpos+3*id0); + mjtNum vel[6]; + mj_objectVelocity(m, d, mjOBJ_SITE, id0, vel, /*flg_local=*/0); + mjtNum wvel[6] = {vel[3], vel[4], vel[5], 0, 0, 0}; + int wbody[2]; + wbody[0] = m->site_bodyid[id0]; + + // second object is geom: process site-geom-site + if (type1 == mjWRAP_SPHERE || type1 == mjWRAP_CYLINDER) { + // TODO(tassa) support geom wrapping (requires derivatives of mju_wrap) + mjERROR("geom wrapping not supported"); + } else { + wraptype = mjWRAP_NONE; + } + + // complete sequence + wbody[1] = m->site_bodyid[id1]; + mju_copy3(wpnt+3, d->site_xpos+3*id1); + mj_objectVelocity(m, d, mjOBJ_SITE, id1, vel, /*flg_local=*/0); + mju_copy3(wvel+3, vel+3); + + // accumulate moments if consecutive points are in different bodies + if (wbody[0] != wbody[1]) { + // dpnt = 3D position difference, normalize + mjtNum dpnt[3]; + mju_sub3(dpnt, wpnt+3, wpnt); + mjtNum norm = mju_norm3(dpnt); + mju_scl3(dpnt, dpnt, 1/norm); + + // dvel = d / dt (dpnt) + mjtNum dvel[3]; + mju_sub3(dvel, wvel+3, wvel); + mjtNum dot = mju_dot3(dpnt, dvel); + mju_addToScl3(dvel, dpnt, -dot); + mju_scl3(dvel, dvel, 1/norm); + + // TODO(tassa ) write sparse branch, requires mj_jacDotSparse + // if (mj_isSparse(m)) { ... } + + // get endpoint JacobianDots, subtract + mj_jacDot(m, d, jac1, 0, wpnt, wbody[0]); + mj_jacDot(m, d, jac2, 0, wpnt+3, wbody[1]); + mju_sub(jacdif, jac2, jac1, 3*nv); + + // chain rule, first term: Jdot += d/dt(jac2 - jac1) * dpnt + mju_mulMatTVec(tmp, jacdif, dpnt, 3, nv); + + // add to existing + mju_addToScl(Jdot, tmp, 1/divisor, nv); + + // get endpoint Jacobians, subtract + mj_jac(m, d, jac1, 0, wpnt, wbody[0]); + mj_jac(m, d, jac2, 0, wpnt+3, wbody[1]); + mju_sub(jacdif, jac2, jac1, 3*nv); + + // chain rule, second term: Jdot += (jac2 - jac1) * d/dt(dpnt) + mju_mulMatTVec(tmp, jacdif, dvel, 3, nv); + + // add to existing + mju_addToScl(Jdot, tmp, 1/divisor, nv); + } + + // advance + j += (wraptype != mjWRAP_NONE ? 2 : 1); + } + + mj_freeStack(d); +} + + + // compute actuator/transmission lengths and moments void mj_transmission(const mjModel* m, mjData* d) { int nv = m->nv, nu = m->nu; diff --git a/src/engine/engine_core_smooth.h b/src/engine/engine_core_smooth.h index 7f1b9fb3..41d06775 100644 --- a/src/engine/engine_core_smooth.h +++ b/src/engine/engine_core_smooth.h @@ -39,6 +39,9 @@ MJAPI void mj_flex(const mjModel* m, mjData* d); // compute tendon lengths, velocities and moment arms MJAPI void mj_tendon(const mjModel* m, mjData* d); +// compute time derivative of dense tendon Jacobian for one tendon +MJAPI void mj_tendonDot(const mjModel* m, mjData* d, int id, mjtNum* Jdot); + // compute actuator transmission lengths and moments MJAPI void mj_transmission(const mjModel* m, mjData* d); diff --git a/test/engine/engine_core_smooth_test.cc b/test/engine/engine_core_smooth_test.cc index 3263acc5..8f24a99a 100644 --- a/test/engine/engine_core_smooth_test.cc +++ b/test/engine/engine_core_smooth_test.cc @@ -159,6 +159,60 @@ TEST_F(CoreSmoothTest, FixedTendonSortedIndices) { mj_deleteModel(model); } +static const char* const kTen_J0 = "engine/testdata/core_smooth/ten_J0.xml"; +static const char* const kTen_J1 = "engine/testdata/core_smooth/ten_J1.xml"; +static const char* const kTen_J2 = "engine/testdata/core_smooth/ten_J2.xml"; +static const char* const kTen_J3 = "engine/testdata/core_smooth/ten_J3.xml"; + +TEST_F(CoreSmoothTest, TendonJdot) { + for (const char* local_path : {kTen_J0, kTen_J1, kTen_J2, kTen_J3}) { + const std::string xml_path = GetTestDataFilePath(local_path); + char error[1024]; + mjModel* m = mj_loadXML(xml_path.c_str(), nullptr, error, sizeof(error)); + int nv = m->nv; + ASSERT_THAT(m, NotNull()) << "Failed to load model: " << error; + EXPECT_EQ(m->ntendon, 1); + mjData* d = mj_makeData(m); + + for (mjtJacobian sparsity : {mjJAC_DENSE, mjJAC_SPARSE}) { + m->opt.jacobian = sparsity; + + if (m->nkey) { + mj_resetDataKeyframe(m, d, 0); + } else { + mj_resetData(m, d); + while (d->time < 1) { + mj_step(m, d); + } + } + + mj_forward(m, d); + + // get current J and Jdot for the tendon + vector ten_J(d->ten_J, d->ten_J + nv); + vector ten_Jdot(nv, 0); + mj_tendonDot(m, d, 0, ten_Jdot.data()); + + // compute finite-differenced Jdot + mjtNum h = 1e-7; + mj_integratePos(m, d->qpos, d->qvel, h); + mj_kinematics(m, d); + mj_comPos(m, d); + mj_tendon(m, d); + vector ten_Jh(d->ten_J, d->ten_J + nv); + mju_subFrom(ten_Jh.data(), ten_J.data(), nv); + mju_scl(ten_Jh.data(), ten_Jh.data(), 1.0 / h, nv); + + // expect analytic and FD derivatives to be similar to eps precision + mjtNum eps = 1e-6; + EXPECT_THAT(ten_Jdot, Pointwise(DoubleNear(eps), ten_Jh)); + } + + mj_deleteData(d); + mj_deleteModel(m); + } +} + // --------------------------- connect constraint ------------------------------ // test that bodies hanging on connects lead to expected force sensor readings diff --git a/test/engine/testdata/core_smooth/ten_J0.xml b/test/engine/testdata/core_smooth/ten_J0.xml new file mode 100644 index 00000000..b4fec9ba --- /dev/null +++ b/test/engine/testdata/core_smooth/ten_J0.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/engine/testdata/core_smooth/ten_J1.xml b/test/engine/testdata/core_smooth/ten_J1.xml new file mode 100644 index 00000000..d5c3cf93 --- /dev/null +++ b/test/engine/testdata/core_smooth/ten_J1.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/engine/testdata/core_smooth/ten_J2.xml b/test/engine/testdata/core_smooth/ten_J2.xml new file mode 100644 index 00000000..e5777029 --- /dev/null +++ b/test/engine/testdata/core_smooth/ten_J2.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/engine/testdata/core_smooth/ten_J3.xml b/test/engine/testdata/core_smooth/ten_J3.xml new file mode 100644 index 00000000..520969d2 --- /dev/null +++ b/test/engine/testdata/core_smooth/ten_J3.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 157b0741162b53db4116d1859e85eda3ea414021 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Tue, 25 Mar 2025 09:35:44 -0700 Subject: [PATCH 006/191] Add signature to `mjSpec` and `mjModel` and use it to perform safe `bind` to `mjModel` and `mjData`. PiperOrigin-RevId: 740378879 Change-Id: If14b326942529494f172e7aedcae30195798b458 --- doc/includes/references.h | 7 ++++ include/mujoco/mjdata.h | 3 ++ include/mujoco/mjmodel.h | 3 ++ include/mujoco/mjspec.h | 2 + .../mujoco/codegen/generate_spec_bindings.py | 15 +++++++ python/mujoco/introspect/structs.py | 15 +++++++ python/mujoco/specs_test.py | 7 ++++ python/mujoco/structs.cc | 24 ++++++++++- src/engine/engine_io.c | 3 ++ src/user/user_flexcomp.cc | 2 + src/user/user_model.cc | 40 +++++++++++++++++++ src/user/user_model.h | 7 ++++ src/user/user_objects.cc | 36 +++++++++++++++++ src/user/user_objects.h | 1 + unity/Runtime/Bindings/MjBindings.cs | 2 + 15 files changed, 165 insertions(+), 2 deletions(-) diff --git a/doc/includes/references.h b/doc/includes/references.h index 39eece88..6b38571b 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -414,6 +414,9 @@ struct mjData_ { // thread pool pointer uintptr_t threadpool; + + // compilation signature + uint64_t signature; // also held by the mjSpec that compiled the model }; typedef struct mjData_ mjData; typedef enum mjtDisableBit_ { // disable default feature bitflags @@ -1451,6 +1454,9 @@ struct mjModel_ { // paths char* paths; // paths to assets, 0-terminated (npaths x 1) + + // compilation signature + uint64_t signature; // also held by the mjSpec that compiled this model }; typedef struct mjModel_ mjModel; struct mjResource_ { @@ -1713,6 +1719,7 @@ typedef enum mjtOrientation_ { // type of orientation specifier } mjtOrientation; typedef struct mjsElement_ { // element type, do not modify mjtObj elemtype; // element type + uint64_t signature; // compilation signature } mjsElement; typedef struct mjsCompiler_ { // compiler options mjtByte autolimits; // infer "limited" attribute based on range diff --git a/include/mujoco/mjdata.h b/include/mujoco/mjdata.h index ddb42215..2b5eb7c6 100644 --- a/include/mujoco/mjdata.h +++ b/include/mujoco/mjdata.h @@ -442,6 +442,9 @@ struct mjData_ { // thread pool pointer uintptr_t threadpool; + + // compilation signature + uint64_t signature; // also held by the mjSpec that compiled the model }; typedef struct mjData_ mjData; diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index 0099cf78..9f76205f 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -1155,6 +1155,9 @@ struct mjModel_ { // paths char* paths; // paths to assets, 0-terminated (npaths x 1) + + // compilation signature + uint64_t signature; // also held by the mjSpec that compiled this model }; typedef struct mjModel_ mjModel; diff --git a/include/mujoco/mjspec.h b/include/mujoco/mjspec.h index 52e96f41..3764257b 100644 --- a/include/mujoco/mjspec.h +++ b/include/mujoco/mjspec.h @@ -23,6 +23,7 @@ // this is a C-API #ifdef __cplusplus #include +#include #include #include @@ -119,6 +120,7 @@ typedef enum mjtOrientation_ { // type of orientation specifier typedef struct mjsElement_ { // element type, do not modify mjtObj elemtype; // element type + uint64_t signature; // compilation signature } mjsElement; diff --git a/python/mujoco/codegen/generate_spec_bindings.py b/python/mujoco/codegen/generate_spec_bindings.py index ba412275..e4617eab 100644 --- a/python/mujoco/codegen/generate_spec_bindings.py +++ b/python/mujoco/codegen/generate_spec_bindings.py @@ -613,12 +613,27 @@ def generate_find() -> None: print(code) +def generate_signature() -> None: + """Generate signature functions.""" + for key, _, _, _, _ in SPECS: + elem = key.removeprefix('mjs') + titlecase = 'Mjs' + elem + code = f"""\n + {key}.def_property_readonly("signature", + [](raw::{titlecase}& self) -> uint64_t {{ + return mjs_getSpec(self.element)->element->signature; + }}); + """ + print(code) + + def main(argv: Sequence[str]) -> None: if len(argv) > 1: raise app.UsageError('Too many command-line arguments.') generate() generate_add() generate_find() + generate_signature() if __name__ == '__main__': diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index fc75c798..2cd2ff27 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -4454,6 +4454,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ doc='paths to assets, 0-terminated', array_extent=('npaths',), ), + StructFieldDecl( + name='signature', + type=ValueType(name='uint64_t'), + doc='also held by the mjSpec that compiled this model', + ), ), )), ('mjThreadPool', @@ -6037,6 +6042,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='uintptr_t'), doc='thread pool pointer', ), + StructFieldDecl( + name='signature', + type=ValueType(name='uint64_t'), + doc='also held by the mjSpec that compiled the model', + ), ), )), ('mjvPerturb', @@ -9044,6 +9054,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='mjtObj'), doc='element type', ), + StructFieldDecl( + name='signature', + type=ValueType(name='uint64_t'), + doc='compilation signature', + ), ), )), ('mjsCompiler', diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py index c946f420..f0c3bb6c 100644 --- a/python/mujoco/specs_test.py +++ b/python/mujoco/specs_test.py @@ -1153,6 +1153,13 @@ class SpecsTest(absltest.TestCase): AttributeError, "object has no attribute 'invalid'" ): print(mj_model.bind(joints).invalid) + invalid_spec = mujoco.MjSpec() + invalid_spec.worldbody.add_body(name='main') + with self.assertRaisesRegex( + ValueError, + 'The mjSpec does not match mjModel. Please recompile the mjSpec.', + ): + print(mj_model.bind(invalid_spec.body('main'))) def test_incorrect_hfield_size(self): nrow = 300 diff --git a/python/mujoco/structs.cc b/python/mujoco/structs.cc index f9272721..6925b980 100644 --- a/python/mujoco/structs.cc +++ b/python/mujoco/structs.cc @@ -1710,6 +1710,10 @@ This is useful for example when the MJB is not available as a file on disk.)")); // Return the full bytes array of concatenated paths return m.paths_bytes; }); + mjModel.def_property_readonly( + "signature", [](const MjModelWrapper& m) -> const uint64_t& { + return m.get()->signature; + }); #define XGROUP(MjModelGroupedViews, field, nfield, FIELD_XMACROS) \ mjModel.def( \ @@ -1730,7 +1734,13 @@ This is useful for example when the MJB is not available as a file on disk.)")); mjModel.def( \ "bind_scalar", \ [](MjModelWrapper& m, spectype& spec) -> auto& { \ - return m.indexer().field##_by_name(mjs_getString(spec.name)); \ + if (mjs_getSpec(spec.element)->element->signature != \ + m.get()->signature) { \ + throw py::value_error( \ + "The mjSpec does not match mjModel. Please recompile " \ + "the mjSpec."); \ + } \ + return m.indexer().field(mjs_getId(spec.element)); \ }, \ py::return_value_policy::reference_internal, \ py::arg_v("spec", py::none())); @@ -2018,6 +2028,10 @@ This is useful for example when the MJB is not available as a file on disk.)")); std::istringstream input(b, std::ios::in | std::ios::binary); return MjDataWrapper::Deserialize(input); })); + mjData.def_property_readonly( + "signature", [](const MjDataWrapper& d) -> uint64_t { + return d.get()->signature; + }); #define X(type, var) \ mjData.def_property( \ @@ -2076,7 +2090,13 @@ This is useful for example when the MJB is not available as a file on disk.)")); mjData.def( \ "bind_scalar", \ [](MjDataWrapper& d, spectype& spec) -> auto& { \ - return d.indexer().field##_by_name(mjs_getString(spec.name)); \ + if (mjs_getSpec(spec.element)->element->signature != \ + d.get()->signature) { \ + throw py::value_error( \ + "The mjSpec does not match mjData. Please recompile "\ + "the mjSpec."); \ + } \ + return d.indexer().field(mjs_getId(spec.element)); \ }, \ py::return_value_policy::reference_internal, \ py::arg_v("spec", py::none())); diff --git a/src/engine/engine_io.c b/src/engine/engine_io.c index f925410c..ec1757f6 100644 --- a/src/engine/engine_io.c +++ b/src/engine/engine_io.c @@ -2014,6 +2014,9 @@ static void _resetData(const mjModel* m, mjData* d, unsigned char debug_value) { } } } + + // copy signature from model + d->signature = m->signature; } diff --git a/src/user/user_flexcomp.cc b/src/user/user_flexcomp.cc index 50c12d68..80ee69d4 100644 --- a/src/user/user_flexcomp.cc +++ b/src/user/user_flexcomp.cc @@ -403,12 +403,14 @@ bool mjCFlexcomp::Make(mjsBody* body, char* error, int error_sz) { mjCFlex* flex = model->AddFlex(); mjsFlex* pf = &flex->spec; int id = flex->id; + int uid = flex->uid; *flex = def.Flex(); flex->PointToLocal(); flex->model = model; flex->id = id; + flex->uid = uid; mjs_setString(pf->name, name.c_str()); mjs_setInt(pf->elem, element.data(), element.size()); mjs_setFloat(pf->texcoord, texcoord.data(), texcoord.size()); diff --git a/src/user/user_model.cc b/src/user/user_model.cc index 2a27d430..496fd2bd 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -201,6 +201,7 @@ mjCModel::mjCModel() { world->mass = 0; mjuu_zerovec(world->inertia, 3); world->id = 0; + world->uid = GetUid(); world->parent = nullptr; world->weldid = 0; world->name = "world"; @@ -213,6 +214,9 @@ mjCModel::mjCModel() { // the source spec is the model itself, overwritten in the copy constructor source_spec_ = &spec; + + // set the signature + spec.element->signature = 0; } @@ -289,6 +293,7 @@ void mjCModel::CopyList(std::vector& dest, // copy the element from the other model to this model if (deepcopy_) { source[i]->ForgetKeyframes(); + candidate->uid = GetUid(); } else { candidate->AddRef(); } @@ -499,6 +504,9 @@ mjCModel& mjCModel::operator+=(const mjCModel& other) { nq = nv = na = nu = nmocap = 0; } + // update signature before we reset the tree lists + spec.element->signature = Signature(); + PointToLocal(); return *this; } @@ -633,6 +641,9 @@ mjCModel& mjCModel::operator-=(const mjCBody& subtree) { RemoveFromList(sensors_, oldmodel); RemovePlugins(); + // update signature before we reset the tree lists + spec.element->signature = Signature(); + return *this; } @@ -803,6 +814,9 @@ void mjCModel::DeleteElement(mjsElement* el) { break; } + // update signature before we reset the tree lists + spec.element->signature = Signature(); + ResetTreeLists(); // in case of a nested delete MakeTreeLists(); ProcessLists(/*checkrepeat=*/false); @@ -1019,7 +1033,9 @@ template T* mjCModel::AddObject(vector& list, string type) { T* obj = new T(this); obj->id = (int)list.size(); + obj->uid = GetUid(); list.push_back(obj); + spec.element->signature = Signature(); return obj; } @@ -1030,7 +1046,9 @@ T* mjCModel::AddObjectDefault(vector& list, string type, mjCDef* def) { T* obj = new T(this, def ? def : defaults_[0]); obj->id = (int)list.size(); obj->classname = def ? def->name : "main"; + obj->uid = GetUid(); list.push_back(obj); + spec.element->signature = Signature(); return obj; } @@ -4572,6 +4590,28 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) { mju::strcpy_arr(errInfo.message, warningtext); errInfo.warning = true; } + + // save signature + m->signature = Signature(); +} + + + +uint64_t mjCModel::Signature() { + std::string uid_str; + for (int i = 0; i < mjNOBJECT; ++i) { + if (i == mjOBJ_XBODY || i == mjOBJ_UNKNOWN || i == mjOBJ_DOF) { + continue; + } + if (object_lists_[i] == nullptr) { + throw mjCError(0, "object list %s is null", std::to_string(i).c_str()); + } + uid_str += '|'; + for (mjCBase* object : *object_lists_[i]) { + uid_str += std::to_string(object->uid) + " "; + } + } + return mj_hashString(uid_str.c_str(), UINT64_MAX); } diff --git a/src/user/user_model.h b/src/user/user_model.h index c97216e5..81b94752 100644 --- a/src/user/user_model.h +++ b/src/user/user_model.h @@ -16,6 +16,7 @@ #define MUJOCO_SRC_USER_USER_MODEL_H_ #include +#include #include #include #include @@ -324,6 +325,9 @@ class mjCModel : public mjCModel_, private mjSpec { // set attached flag void SetAttached(bool deepcopy) { attached_ |= !deepcopy; } + // get new uid + int GetUid() { return uid_count_++; } + private: // settings for each defaults class std::vector defaults_; @@ -440,11 +444,14 @@ class mjCModel : public mjCModel_, private mjSpec { void MarkPluginInstance(std::unordered_map& instances, const std::vector& list); + // generate a signature for the model + uint64_t Signature(); mjListKeyMap ids; // map from object names to ids mjCError errInfo; // last error info std::vector key_pending_; // attached keyframes bool deepcopy_; // copy objects when attaching bool attached_ = false; // true if model is attached to a parent model + int uid_count_ = 0; // unique id count for all objects }; #endif // MUJOCO_SRC_USER_USER_MODEL_H_ diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index a493bf30..0af3548c 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -837,6 +837,7 @@ mjCBody::mjCBody(mjCModel* _model) { mjCBody::mjCBody(const mjCBody& other, mjCModel* _model) { model = _model; + uid = model->GetUid(); mjSpec* origin = model->FindSpec(other.compiler); compiler = origin ? &origin->compiler : &model->spec.compiler; *this = other; @@ -930,6 +931,7 @@ mjCBody& mjCBody::operator+=(const mjCFrame& other) { frames.back()->frame = other.frame; if (model->deepcopy_) { frames.back()->NameSpace(other_model); + frames.back()->uid = model->GetUid(); } else { frames.back()->AddRef(); } @@ -1022,6 +1024,8 @@ void mjCBody::CopyList(std::vector& dst, const std::vector& src, // increment refcount if shallow copy is made if (!model->deepcopy_) { dst.back()->AddRef(); + } else { + dst.back()->uid = model->GetUid(); } // set namespace @@ -1237,6 +1241,10 @@ mjCBody* mjCBody::AddBody(mjCDef* _def) { model->MakeTreeLists(); obj->parent = this; + + // update signature + obj->uid = model->GetUid(); + model->spec.element->signature = model->Signature(); return obj; } @@ -1248,6 +1256,10 @@ mjCFrame* mjCBody::AddFrame(mjCFrame* _frame) { frames.push_back(obj); model->ResetTreeLists(); model->MakeTreeLists(); + + // update signature + obj->uid = model->GetUid(); + model->spec.element->signature = model->Signature(); return obj; } @@ -1268,6 +1280,10 @@ mjCJoint* mjCBody::AddFreeJoint() { model->ResetTreeLists(); model->MakeTreeLists(); + + // update signature + obj->uid = model->GetUid(); + model->spec.element->signature = model->Signature(); return obj; } @@ -1287,6 +1303,10 @@ mjCJoint* mjCBody::AddJoint(mjCDef* _def) { model->ResetTreeLists(); model->MakeTreeLists(); + + // update signature + obj->uid = model->GetUid(); + model->spec.element->signature = model->Signature(); return obj; } @@ -1306,6 +1326,10 @@ mjCGeom* mjCBody::AddGeom(mjCDef* _def) { model->ResetTreeLists(); model->MakeTreeLists(); + + // update signature + obj->uid = model->GetUid(); + model->spec.element->signature = model->Signature(); return obj; } @@ -1325,6 +1349,10 @@ mjCSite* mjCBody::AddSite(mjCDef* _def) { model->ResetTreeLists(); model->MakeTreeLists(); + + // update signature + obj->uid = model->GetUid(); + model->spec.element->signature = model->Signature(); return obj; } @@ -1344,6 +1372,10 @@ mjCCamera* mjCBody::AddCamera(mjCDef* _def) { model->ResetTreeLists(); model->MakeTreeLists(); + + // update signature + obj->uid = model->GetUid(); + model->spec.element->signature = model->Signature(); return obj; } @@ -1363,6 +1395,10 @@ mjCLight* mjCBody::AddLight(mjCDef* _def) { model->ResetTreeLists(); model->MakeTreeLists(); + + // update signature + obj->uid = model->GetUid(); + model->spec.element->signature = model->Signature(); return obj; } diff --git a/src/user/user_objects.h b/src/user/user_objects.h index 47703182..6d9b146e 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -219,6 +219,7 @@ class mjCBoundingVolumeHierarchy : public mjCBoundingVolumeHierarchy_ { class mjCBase_ : public mjsElement { public: int id; // object id + int uid; // unique identifier std::string name; // object name std::string classname; // defaults class name std::string info; // error message info set by the user diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index 845c5c91..7aecffe5 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -5008,6 +5008,7 @@ public unsafe struct mjData_ { public double* efc_force; public int* efc_state; public UIntPtr threadpool; + public UInt64 signature; } [StructLayout(LayoutKind.Sequential)] @@ -5672,6 +5673,7 @@ public unsafe struct mjModel_ { public char* names; public int* names_map; public char* paths; + public UInt64 signature; } [StructLayout(LayoutKind.Sequential)] From 69f83492038ace73c9b87deba4318902d95fe0fd Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Tue, 25 Mar 2025 14:21:52 -0700 Subject: [PATCH 007/191] Add signature error checking at the end of compilation. Fixed some edge cases that caused the new error to be triggered. PiperOrigin-RevId: 740483759 Change-Id: I6ced3ba55476d5c18103e205151ecace94f2b29b --- src/user/user_model.cc | 31 +++++++++++++++++++++++++------ src/user/user_objects.cc | 1 + 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/user/user_model.cc b/src/user/user_model.cc index 496fd2bd..a594538a 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -245,6 +245,10 @@ mjCModel& mjCModel::operator=(const mjCModel& other) { mjCBody* world = new mjCBody(*other.bodies_[0], this); bodies_.push_back(world); + // update tree lists + ResetTreeLists(); + MakeTreeLists(); + // add everything else *this += other; @@ -259,6 +263,9 @@ mjCModel& mjCModel::operator=(const mjCModel& other) { for (int i=0; i < mjNOBJECT; i++) { ids[i] = other.ids[i]; } + + // update signature after we updated everything + spec.element->signature = Signature(); } deepcopy_ = other.deepcopy_; return *this; @@ -504,10 +511,11 @@ mjCModel& mjCModel::operator+=(const mjCModel& other) { nq = nv = na = nu = nmocap = 0; } - // update signature before we reset the tree lists - spec.element->signature = Signature(); - + // update pointers to local elements PointToLocal(); + + // update signature after we updated the tree lists and we updated the pointers + spec.element->signature = Signature(); return *this; } @@ -814,12 +822,12 @@ void mjCModel::DeleteElement(mjsElement* el) { break; } - // update signature before we reset the tree lists - spec.element->signature = Signature(); - ResetTreeLists(); // in case of a nested delete MakeTreeLists(); ProcessLists(/*checkrepeat=*/false); + + // update signature after we updated everything + spec.element->signature = Signature(); } @@ -4593,6 +4601,17 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) { // save signature m->signature = Signature(); + + // special cases that are not caused by user edits + if (compiler.fusestatic || compiler.discardvisual || + !spec.element->signature || !pairs_.empty() || !excludes_.empty()) { + spec.element->signature = m->signature; + } + + // check that the signature matches the spec + if (m->signature != spec.element->signature) { + throw mjCError(0, "signature mismatch"); // SHOULD NOT OCCUR + } } diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 0af3548c..5154af48 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -1422,6 +1422,7 @@ mjCFrame* mjCBody::ToFrame() { parent->bodies.end()); model->ResetTreeLists(); model->MakeTreeLists(); + model->spec.element->signature = model->Signature(); return newframe; } From d6943731b284ee51375147a3f7e520259cf2beb5 Mon Sep 17 00:00:00 2001 From: Kyle Bayes Date: Tue, 25 Mar 2025 14:55:25 -0700 Subject: [PATCH 008/191] Fix bug in nativeccd multiccd. PiperOrigin-RevId: 740495321 Change-Id: I7eaa0da8514e27c233d49567c276d1d238808d1f --- src/engine/engine_collision_gjk.c | 2 +- test/engine/engine_collision_gjk_test.cc | 40 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/engine/engine_collision_gjk.c b/src/engine/engine_collision_gjk.c index fb13c666..16859bf5 100644 --- a/src/engine/engine_collision_gjk.c +++ b/src/engine/engine_collision_gjk.c @@ -1546,7 +1546,7 @@ static mjtNum planeNormal(mjtNum res[3], const mjtNum v1[3], const mjtNum v2[3], // find what side of a plane a point p lies static int halfspace(const mjtNum a[3], const mjtNum n[3], const mjtNum p[3]) { mjtNum diff[3] = {p[0] - a[0], p[1] - a[1], p[2] - a[2]}; - return dot3(diff, n) > 0; + return dot3(diff, n) >= 0.0; } diff --git a/test/engine/engine_collision_gjk_test.cc b/test/engine/engine_collision_gjk_test.cc index 437d314d..6db923f2 100644 --- a/test/engine/engine_collision_gjk_test.cc +++ b/test/engine/engine_collision_gjk_test.cc @@ -951,6 +951,46 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD9) { mj_deleteModel(model); } +TEST_F(MjGjkTest, BoxBoxMultiCCD10) { + static constexpr char xml[] = R"( + + + + + + )"; + + std::array error; + mjModel* model = LoadModelFromString(xml, error.data(), error.size()); + ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error.data(); + + mjData* data = mj_makeData(model); + mj_forward(model, data); + + mjtNum* xpos = data->geom_xpos; + + xpos[0] = -0.1034859999999999946584949839234468527138; + xpos[1] = -0.0765140000000000264357424839545274153352; + xpos[2] = 0.1257628745456405572333835607423679903150; + + xpos = data->geom_xpos + 3; + + xpos[0] = -0.1034859999999999946584949839234468527138; + xpos[1] = -0.0765140000000000264357424839545274153352; + xpos[2] = 0.1751399999999999623767621415026951581240; + + int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + std::vector dir, pos; + mjtNum dist; + int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2, 0, 8); + + EXPECT_EQ(ncons, 4); + + mj_deleteData(data); + mj_deleteModel(model); +} + TEST_F(MjGjkTest, SmallBoxMesh) { static constexpr char xml[] = R"( From 5c955b8fe264e5235c2e2c89a6532259e80cae4c Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Wed, 26 Mar 2025 05:35:33 -0700 Subject: [PATCH 009/191] Add frame support to the cable composite. Also cleanup user_composite.cc includes. PiperOrigin-RevId: 740730596 Change-Id: I05df59926d8e61de45fd6b2a3a63121b7a390448 --- src/user/user_composite.cc | 18 +++++++----------- src/user/user_composite.h | 1 + src/xml/xml_native_reader.cc | 5 +++-- src/xml/xml_native_reader.h | 3 ++- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/user/user_composite.cc b/src/user/user_composite.cc index 0c09164b..98c3ed41 100644 --- a/src/user/user_composite.cc +++ b/src/user/user_composite.cc @@ -12,25 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include #include "user/user_composite.h" -#include #include -#include -#include #include -#include -#include #include -#include #include -#include #include +#include #include #include "cc/array_safety.h" -#include "engine/engine_io.h" #include "engine/engine_util_blas.h" #include "engine/engine_util_errmem.h" #include "engine/engine_util_misc.h" @@ -40,9 +32,9 @@ #include "user/user_util.h" namespace { + namespace mju = ::mujoco::util; -using mujoco::user::VectorToString; -using mujoco::user::StringToVector; + } // namespace // strncpy with 0, return false @@ -60,6 +52,7 @@ mjCComposite::mjCComposite(void) { type = mjCOMPTYPE_PARTICLE; count[0] = count[1] = count[2] = 1; mjuu_setvec(offset, 0, 0, 0); + frame = nullptr; // plugin variables mjs_defaultPlugin(&plugin); @@ -381,6 +374,9 @@ mjsBody* mjCComposite::AddCableBody(mjCModel* model, mjsBody* body, int ix, offset[1]+uservert[3*ix+1], offset[2]+uservert[3*ix+2]); mjuu_copyvec(body->quat, this_quat, 4); + if (frame) { + mjs_setFrame(body->element, frame); + } } else { mjuu_setvec(body->pos, length_prev, 0, 0); double negquat[4] = {prev_quat[0], -prev_quat[1], -prev_quat[2], -prev_quat[3]}; diff --git a/src/user/user_composite.h b/src/user/user_composite.h index 383dd3a2..0f826760 100644 --- a/src/user/user_composite.h +++ b/src/user/user_composite.h @@ -80,6 +80,7 @@ class mjCComposite { std::vector uservert; // user-specified vertex positions double size[3]; // rope size (meaning depends on the shape) mjtCompShape curve[3]; // geometric shape + mjsFrame* frame; // frame where the composite is defined // body names used in the skin std::vector username; diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 4f941c15..44efc4ec 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -2388,7 +2388,7 @@ void mjXReader::OneActuator(XMLElement* elem, mjsActuator* actuator) { // make composite -void mjXReader::OneComposite(XMLElement* elem, mjsBody* body, const mjsDefault* def) { +void mjXReader::OneComposite(XMLElement* elem, mjsBody* body, mjsFrame* frame, const mjsDefault* def) { string text; int n; @@ -2402,6 +2402,7 @@ void mjXReader::OneComposite(XMLElement* elem, mjsBody* body, const mjsDefault* } ReadAttr(elem, "count", 3, comp.count, text, false, false); ReadAttr(elem, "offset", 3, comp.offset, text); + comp.frame = frame; // plugin XMLElement* eplugin = FirstChildElement(elem, "plugin"); @@ -3467,7 +3468,7 @@ void mjXReader::Body(XMLElement* section, mjsBody* body, mjsFrame* frame, // composite sub-element else if (name == "composite") { // parse composite - OneComposite(elem, body, def); + OneComposite(elem, body, frame, def); } // flexcomp sub-element diff --git a/src/xml/xml_native_reader.h b/src/xml/xml_native_reader.h index 41aacbbb..0e568f99 100644 --- a/src/xml/xml_native_reader.h +++ b/src/xml/xml_native_reader.h @@ -80,7 +80,8 @@ class mjXReader : public mjXBase { void OneEquality(tinyxml2::XMLElement* elem, mjsEquality* pequality); void OneTendon(tinyxml2::XMLElement* elem, mjsTendon* ptendon); void OneActuator(tinyxml2::XMLElement* elem, mjsActuator* pactuator); - void OneComposite(tinyxml2::XMLElement* elem, mjsBody* pbody, const mjsDefault* def); + void OneComposite(tinyxml2::XMLElement* elem, mjsBody* pbody, mjsFrame* pframe, + const mjsDefault* def); void OneFlexcomp(tinyxml2::XMLElement* elem, mjsBody* pbody, const mjVFS* vfs); void OnePlugin(tinyxml2::XMLElement* elem, mjsPlugin* plugin); From 644b42bee39348a456202840bf6cbe84200dbab0 Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Wed, 26 Mar 2025 05:35:54 -0700 Subject: [PATCH 010/191] Add mujoco/experimental and move USD work there. PiperOrigin-RevId: 740730698 Change-Id: If9c68798cda502d6d7632906a034437e2557802e --- .../usd/plugins/mjcf/mjcf_file_format.cc | 201 ++++ .../usd/plugins/mjcf/mjcf_file_format.h | 85 ++ .../usd/plugins/mjcf/mujoco_to_usd.cc | 1000 +++++++++++++++++ .../usd/plugins/mjcf/mujoco_to_usd.h | 32 + .../usd/plugins/mjcf/plugInfo.json | 25 + src/experimental/usd/plugins/mjcf/utils.cc | 199 ++++ src/experimental/usd/plugins/mjcf/utils.h | 127 +++ test/experimental/usd/plugins/mjcf/fixture.cc | 64 ++ test/experimental/usd/plugins/mjcf/fixture.h | 67 ++ .../usd/plugins/mjcf/mjcf_file_format_test.cc | 387 +++++++ .../usd/plugins/mjcf/testdata/materials.xml | 15 + .../usd/plugins/mjcf/testdata/mesh_obj.xml | 10 + .../mjcf/testdata/meshes/tetrahedron.obj | 16 + .../plugins/mjcf/testdata/textures/cube.png | Bin 0 -> 6888 bytes 14 files changed, 2228 insertions(+) create mode 100644 src/experimental/usd/plugins/mjcf/mjcf_file_format.cc create mode 100644 src/experimental/usd/plugins/mjcf/mjcf_file_format.h create mode 100644 src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc create mode 100644 src/experimental/usd/plugins/mjcf/mujoco_to_usd.h create mode 100644 src/experimental/usd/plugins/mjcf/plugInfo.json create mode 100644 src/experimental/usd/plugins/mjcf/utils.cc create mode 100644 src/experimental/usd/plugins/mjcf/utils.h create mode 100644 test/experimental/usd/plugins/mjcf/fixture.cc create mode 100644 test/experimental/usd/plugins/mjcf/fixture.h create mode 100644 test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc create mode 100644 test/experimental/usd/plugins/mjcf/testdata/materials.xml create mode 100644 test/experimental/usd/plugins/mjcf/testdata/mesh_obj.xml create mode 100644 test/experimental/usd/plugins/mjcf/testdata/meshes/tetrahedron.obj create mode 100644 test/experimental/usd/plugins/mjcf/testdata/textures/cube.png diff --git a/src/experimental/usd/plugins/mjcf/mjcf_file_format.cc b/src/experimental/usd/plugins/mjcf/mjcf_file_format.cc new file mode 100644 index 00000000..8c5858c9 --- /dev/null +++ b/src/experimental/usd/plugins/mjcf/mjcf_file_format.cc @@ -0,0 +1,201 @@ +// Copyright 2025 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. + +#include "mjcf/mjcf_file_format.h" + +#include +#include +#include +#include + +#include +#include "mjcf/mujoco_to_usd.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "tinyxml2.h" + +PXR_NAMESPACE_OPEN_SCOPE + +TF_DEFINE_PUBLIC_TOKENS(UsdMjcfFileFormatTokens, USD_MJCF_FILE_FORMAT_TOKENS); + +TF_REGISTRY_FUNCTION(TfType) { + SDF_DEFINE_FILE_FORMAT(UsdMjcfFileFormat, SdfFileFormat); +} + +enum ErrorCodes { XmlParsingError }; +TF_REGISTRY_FUNCTION(TfEnum) { + TF_ADD_ENUM_NAME(XmlParsingError, "Error when parsing XML."); +}; + +namespace { + +void ResolveMjcfDependencies(const std::string &xml_string, + const std::string &resolved_path); + +void AccumulateFilesRecursive(std::unordered_set &files, + tinyxml2::XMLElement *elem, + const std::string &resolved_path) { + // get filename + const char *file = elem->Attribute("file"); + + if (file != nullptr) { + auto identifier = pxr::ArGetResolver().CreateIdentifier( + std::string(file), pxr::ArResolvedPath(resolved_path)); + if (!strcasecmp(elem->Value(), "include") || + !strcasecmp(elem->Value(), "model")) { + auto include_resolved_path = pxr::ArGetResolver().Resolve(identifier); + auto asset = pxr::ArGetResolver().OpenAsset(include_resolved_path); + ResolveMjcfDependencies(asset->GetBuffer().get(), include_resolved_path); + + // Neither of these elements should have children. + return; + } + + files.insert(identifier); + } + + if (!strcasecmp(elem->Value(), "texture")) { + static const char *attributes[] = {"fileright", "fileup", "fileleft", + "filedown", "filefront", "fileback"}; + for (const auto &attribute : attributes) { + const char *file = elem->Attribute(attribute); + if (file != nullptr) { + auto identifier = pxr::ArGetResolver().CreateIdentifier( + std::string(file), pxr::ArResolvedPath(resolved_path)); + files.insert(identifier); + } + } + } + + tinyxml2::XMLElement *child = elem->FirstChildElement(); + for (; child; child = child->NextSiblingElement()) { + AccumulateFilesRecursive(files, child, resolved_path); + } +} + +void ResolveMjcfDependencies(const std::string &xml_string, + const std::string &resolved_path) { + // load XML file or parse string + tinyxml2::XMLDocument doc; + doc.Parse(xml_string.c_str()); + + // error checking + if (doc.Error()) { + TF_ERROR(XmlParsingError, "%d:\n%s\n", doc.ErrorID(), doc.ErrorStr()); + return; + } + + // get top-level element + tinyxml2::XMLElement *root = doc.RootElement(); + if (!root) { + TF_ERROR(XmlParsingError, "XML root element not found"); + return; + } + + // Accumulate file dependencies. + std::unordered_set files = {}; + AccumulateFilesRecursive(files, root, resolved_path); + + auto open_asset = [resolved_path](const std::string &identifier) { + pxr::ArGetResolver().OpenAsset(pxr::ArGetResolver().Resolve(identifier)); + }; + // Open all assets in parallel. + pxr::WorkParallelForEach(files.begin(), files.end(), open_asset); +} +} // namespace + +UsdMjcfFileFormat::UsdMjcfFileFormat() + : SdfFileFormat( + UsdMjcfFileFormatTokens->Id, UsdMjcfFileFormatTokens->Version, + UsdMjcfFileFormatTokens->Target, UsdMjcfFileFormatTokens->Id) {} + +UsdMjcfFileFormat::~UsdMjcfFileFormat() {} + +bool UsdMjcfFileFormat::CanRead(const std::string &filePath) const { + auto extension = pxr::TfGetExtension(filePath); + if (extension.empty()) { + return false; + } + + return extension == this->GetFormatId(); +} + +bool UsdMjcfFileFormat::ReadImpl(pxr::SdfLayer *layer, mjSpec *spec) const { + auto data = InitData(layer->GetFileFormatArguments()); + + auto success = mujoco::usd::WriteSpecToData(spec, data); + mj_deleteSpec(spec); + if (!success) { + return false; + } + + _SetLayerData(layer, data); + + return true; +} + +bool UsdMjcfFileFormat::ReadFromString(pxr::SdfLayer *layer, + const std::string &str) const { + std::array error; + mjSpec *spec = + mj_parseXMLString(str.c_str(), nullptr, error.data(), error.size()); + if (spec == nullptr) { + TF_WARN(XmlParsingError, "%s", error.data()); + return false; + } + + return ReadImpl(layer, spec); +} + +bool UsdMjcfFileFormat::Read(pxr::SdfLayer *layer, + const std::string &resolved_path, + bool metadata_only) const { + // Resolved all dependencies so that they are accessible when parsing + // the XML. + std::shared_ptr asset = + pxr::ArGetResolver().OpenAsset(pxr::ArResolvedPath(resolved_path)); + auto buffer = asset->GetBuffer(); + ResolveMjcfDependencies(buffer.get(), resolved_path); + + // Parse to USD. + std::array error; + mjSpec *spec = + mj_parseXML(resolved_path.c_str(), nullptr, error.data(), error.size()); + if (spec == nullptr) { + TF_WARN(XmlParsingError, "%s", error.data()); + return false; + } + return ReadImpl(layer, spec); +} + +bool UsdMjcfFileFormat::WriteToString(const SdfLayer &layer, std::string *str, + const std::string &comment) const { + return SdfFileFormat::FindById(pxr::UsdUsdaFileFormatTokens->Id) + ->WriteToString(layer, str, comment); +} + +PXR_NAMESPACE_CLOSE_SCOPE diff --git a/src/experimental/usd/plugins/mjcf/mjcf_file_format.h b/src/experimental/usd/plugins/mjcf/mjcf_file_format.h new file mode 100644 index 00000000..c5776ec4 --- /dev/null +++ b/src/experimental/usd/plugins/mjcf/mjcf_file_format.h @@ -0,0 +1,85 @@ +// Copyright 2025 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. + +#ifndef MUJOCO_SRC_EXPERIMENTAL_USD_PLUGINS_MJCF_MJCF_FILE_FORMAT_H_ +#define MUJOCO_SRC_EXPERIMENTAL_USD_PLUGINS_MJCF_MJCF_FILE_FORMAT_H_ + +#include + +#include +#include +#include +#include +#include +#include +#include + +PXR_NAMESPACE_OPEN_SCOPE + +// The Id should realistically be mjcf, but the id and extension need to match. +// So near term it just assumes the only .xml file we would import is MJCF. +#define USD_MJCF_FILE_FORMAT_TOKENS \ + ((Id, "xml"))((Version, "1.0"))((Target, "usd")) + +TF_DECLARE_PUBLIC_TOKENS(UsdMjcfFileFormatTokens, USD_MJCF_FILE_FORMAT_TOKENS); + +TF_DECLARE_WEAK_AND_REF_PTRS(UsdMjcfFileFormat); + +class UsdMjcfFileFormat : public SdfFileFormat { + public: + using SdfFileFormat::FileFormatArguments; + + // Returns true if 'file' can be read by this format plugin. + USD_API + bool CanRead(const std::string &file) const override; + + // Reads scene description from the asset specified by resolved_path into + // 'layer'. + // + // metadataOnly is a flag that asks for only the layer metadata to be read in, + // which can be much faster if that is all that is required but currently we + // ignore it. + // + // Returns true if the asset is successfully read into layer, false otherwise. + USD_API + bool Read(pxr::SdfLayer *layer, const std::string &resolved_path, + bool metadata_only) const override; + + // Reads data in the string 'str' into 'layer'. + // + // If the file is successfully read, this method returns true. Otherwise, + // false is returned and errors are posted. + USD_API + bool ReadFromString(SdfLayer *layer, const std::string &str) const override; + + // Writes the contents in 'layer' to 'str'. This just forwards to the usda + // implementation. + USD_API + bool WriteToString(const SdfLayer &layer, std::string *str, + const std::string &comment) const override; + + protected: + SDF_FILE_FORMAT_FACTORY_ACCESS; + + UsdMjcfFileFormat(); + virtual ~UsdMjcfFileFormat(); + + private: + // Function delegated to by Read and ReadFromString. + bool ReadImpl(SdfLayer *layer, mjSpec *spec) const; +}; + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif // MUJOCO_SRC_EXPERIMENTAL_USD_PLUGINS_MJCF_MJCF_FILE_FORMAT_H_ diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc new file mode 100644 index 00000000..f1c8e5ee --- /dev/null +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc @@ -0,0 +1,1000 @@ +// Copyright 2025 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. + +#include "mjcf/mujoco_to_usd.h" + +#include +#include +#include +#include +#include + +#include +#include "mjcf/utils.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// The ID of the World in mjModel and mjData. +static constexpr int kWorldIndex = 0; + +// Using to satisfy TF_DEFINE_PRIVATE_TOKENS macro below and avoid operating in +// PXR_NS. +using pxr::TfToken; +template +using TfStaticData = pxr::TfStaticData; + +// clang-format off +TF_DEFINE_PRIVATE_TOKENS(kTokens, + // Xform ops + ((body, "Body")) + ((body_name, "mujoco:body_name")) + ((geom, "Geom")) + ((light, "Light")) + ((meshScope, "MeshSources")) + ((materialsScope, "Materials")) + ((surface, "PreviewSurface")) + ((world, "World")) + ((xformOpTransform, "xformOp:transform")) + ((xformOpScale, "xformOp:scale")) + (st) + ((primvarsSt, "primvars:st")) + ((outputsSt, "outputs:st")) + ((inputsSt, "inputs:st")) + ((inputsVarname, "inputs:varname")) + ((inputsFile, "inputs:file")) + ((inputsWrapS, "inputs:wrapS")) + ((inputsWrapT, "inputs:wrapT")) + ((inputsDiffuseColor, "inputs:diffuseColor")) + ((outputsRgb, "outputs:rgb")) + ((inputsMetallic, "inputs:metallic")) + (repeat) + ); + +// Using to satisfy TF_REGISTRY_FUNCTION macro below and avoid operating in PXR_NS. +using pxr::TfEnum; +using pxr::Tf_RegistryStaticInit; +using pxr::Tf_RegistryInit; +using pxr::TfEnum; +template +using Arch_PerLibInit = pxr::Arch_PerLibInit; +enum ErrorCodes { UnsupportedGeomTypeError, MujocoCompilationError }; + +TF_REGISTRY_FUNCTION(pxr::TfEnum) { + TF_ADD_ENUM_NAME(UnsupportedGeomTypeError, "UsdGeom type is unsupported.") + TF_ADD_ENUM_NAME(MujocoCompilationError, "Mujoco spec failed to compile.") +} + +// Usings to satisfy TF_ERROR macro. +using pxr::TfCallContext; +using pxr::Tf_PostErrorHelper; +// clang-format on + +using mujoco::usd::AddAttributeConnection; +using mujoco::usd::AddPrimInherit; +using mujoco::usd::AddPrimReference; +using mujoco::usd::ApplyApiSchema; +using mujoco::usd::CreateAttributeSpec; +using mujoco::usd::CreateClassSpec; +using mujoco::usd::CreatePrimSpec; +using mujoco::usd::CreateRelationshipSpec; +using mujoco::usd::SetAttributeDefault; +using mujoco::usd::SetAttributeMetadata; +using mujoco::usd::SetLayerMetadata; +using mujoco::usd::SetPrimKind; +using mujoco::usd::SetPrimMetadata; + +pxr::GfMatrix4d MujocoPosQuatToTransform(double *pos, double *quat) { + pxr::GfQuatd quaternion = pxr::GfQuatd::GetIdentity(); + quaternion.SetReal(quat[0]); + quaternion.SetImaginary(quat[1], quat[2], quat[3]); + pxr::GfRotation rotation(quaternion); + + pxr::GfVec3d translation(0.0, 0.0, 0.0); + translation.Set(pos[0], pos[1], pos[2]); + + pxr::GfMatrix4d transform; + transform.SetTransform(rotation, translation); + return transform; +} + +} // namespace + +class ModelWriter { + public: + ModelWriter(mjSpec *spec, mjModel *model, pxr::SdfAbstractDataRefPtr &data) + : spec_(spec), model_(model), data_(data), class_path_("/Bad_Path") { + body_paths_ = std::vector(model->nbody); + body_xforms_ = std::vector(model->nbody); + } + ~ModelWriter() { mj_deleteModel(model_); } + + void Write() { + // Create top level class holder. + class_path_ = CreateClassSpec(data_, pxr::SdfPath::AbsoluteRootPath(), + pxr::TfToken("__class__")); + + // Create the world body. + body_paths_[kWorldIndex] = WriteWorldBody(kWorldIndex); + body_xforms_[kWorldIndex] = pxr::GfMatrix4d().SetIdentity(); + + SetLayerMetadata(data_, pxr::SdfFieldKeys->Documentation, + "Generated by mujoco model writer."); + // Mujoco is Z up by default. + SetLayerMetadata(data_, pxr::UsdGeomTokens->upAxis, pxr::UsdGeomTokens->z); + // Mujoco is authored in meters by default. + SetLayerMetadata(data_, pxr::UsdGeomTokens->metersPerUnit, + pxr::UsdGeomLinearUnits::meters); + + // Set the world body to be the default prim for referencing/payloads. + SetLayerMetadata(data_, pxr::SdfFieldKeys->DefaultPrim, + body_paths_[kWorldIndex].GetNameToken()); + + // Author mesh scope + mesh prims to be referenced. + WriteMeshes(); + WriteMaterials(); + WriteBodies(); + } + + private: + mjSpec *spec_; + mjModel *model_; + + // This is a handle to the Sdf data to be written into the generated USD + // layer. + pxr::SdfAbstractDataRefPtr &data_; + // Path to top level class spec that all classes should be children of. + pxr::SdfPath class_path_; + // Mapping from Mujoco body id to SdfPath. + std::vector body_paths_; + // Mapping from Mujoco body id to world space transform. + std::vector body_xforms_; + // Mapping from mesh names to Mesh prim path. + std::unordered_map mesh_paths_; + + // Given a name index and a parent prim path this returns a + // token such that appending it to the parent prim path does not + // identify an existing prim spec. + // + // This is necessary since mujoco does not require names for elements + // so we must differentiate between elements of the same type. + // + // For example: + // + // + // + // + // + // + // + // We expect that the occurrence of this happens little enough that linear + // searching is plenty efficient. + pxr::TfToken GetAvailablePrimName(const std::string base_name, + const pxr::TfToken fallback_name, + const pxr::SdfPath &parent_path) { + const auto valid_base_name = pxr::TfMakeValidIdentifier( + base_name.empty() ? fallback_name : base_name); + std::string name = valid_base_name; + pxr::SdfPath test_path = parent_path.AppendChild(pxr::TfToken(name)); + int count = 1; + while (data_->HasSpec(test_path) && + data_->GetSpecType(test_path) == pxr::SdfSpecType::SdfSpecTypePrim) { + name = pxr::TfStringPrintf("%s_%d", valid_base_name.c_str(), count++); + test_path = parent_path.AppendChild(pxr::TfToken(name)); + } + return pxr::TfToken(name); + } + + // This function, conversely to GetAvailablePrimName will not handle + // collisions. This is useful when looking up a prim path that might exist or + // a path that you know must be unique. + pxr::TfToken GetValidPrimName(const std::string name) { + return pxr::TfToken(pxr::TfMakeValidIdentifier(name)); + } + + struct BodyPathComponents { + pxr::SdfPath parent_path; + pxr::TfToken body_name; + }; + + pxr::SdfPath CreateParentIfNotExists(mjsBody *body, + const pxr::SdfPath &world_path, + pxr::SdfAbstractDataRefPtr &data) { + // To allow for easier scene authoring and modification, we want to + // place MJCF bodies belonging to the same kinematic chain under some + // identity parent Xform prim. This allows users to move the entire + // asset. + // + // We cannot simply recreate the MJCF kinematic tree structure + // because in USD it is assumed that children move rigidly with their + // parents. This is not true in MJCF if you have joints. We could perhaps + // use a more complex heuristic where we evaluate a common tree prefix + // in MJCF that is effectively welded together but for now we choose + // simplicity. + + // In the trivial case where the parent of body is already the world + // body we want to create a parent xform of the same name. + // So if the MJCF has a child of the world body called "root" we will + // create a parent Xform at /World/root and the actual body will be + // created at /World/root/root. + mjsBody *last_parent = body; + mjsBody *parent = mjs_getParent(body->element); + while (mjs_getId(parent->element) != kWorldIndex) { + last_parent = parent; + parent = mjs_getParent(parent->element); + } + + pxr::TfToken last_parent_name = GetValidPrimName(*last_parent->name); + pxr::SdfPath parent_xform_path = world_path.AppendChild(last_parent_name); + if (!data->HasSpec(parent_xform_path)) { + pxr::SdfPath prim_path = CreatePrimSpec( + data, world_path, last_parent_name, pxr::UsdGeomTokens->Xform); + + SetPrimKind(data_, prim_path, pxr::KindTokens->component); + } + return parent_xform_path; + } + + void WriteScaleXformOp(const pxr::SdfPath &prim_path, + const pxr::GfVec3f &scale) { + pxr::SdfPath scale_attr_path = + CreateAttributeSpec(data_, prim_path, kTokens->xformOpScale, + pxr::SdfValueTypeNames->Float3); + SetAttributeDefault(data_, scale_attr_path, scale); + } + + void WriteTransformXformOp(const pxr::SdfPath &prim_path, + const pxr::GfMatrix4d &transform) { + pxr::SdfPath transform_op_path = + CreateAttributeSpec(data_, prim_path, kTokens->xformOpTransform, + pxr::SdfValueTypeNames->Matrix4d); + SetAttributeDefault(data_, transform_op_path, transform); + } + + void WriteXformOpOrder(const pxr::SdfPath &prim_path, + const pxr::VtArray &order) { + pxr::SdfPath xform_op_order_path = + CreateAttributeSpec(data_, prim_path, pxr::UsdGeomTokens->xformOpOrder, + pxr::SdfValueTypeNames->TokenArray); + SetAttributeDefault(data_, xform_op_order_path, order); + } + + void PrependToXformOpOrder(const pxr::SdfPath &prim_path, + const pxr::VtArray &order) { + auto xform_op_order_path = + prim_path.AppendProperty(pxr::UsdGeomTokens->xformOpOrder); + if (!data_->HasSpec(xform_op_order_path)) { + WriteXformOpOrder(prim_path, order); + return; + } + + auto existing_order = + data_->Get(xform_op_order_path, pxr::SdfFieldKeys->Default) + .UncheckedGet>(); + + pxr::VtArray new_order(order.size() + existing_order.size()); + std::copy(order.begin(), order.end(), new_order.begin()); + std::copy(existing_order.begin(), existing_order.end(), + new_order.begin() + order.size()); + + SetAttributeDefault(data_, xform_op_order_path, new_order); + } + + void WriteMesh(const mjsMesh *mesh, const pxr::SdfPath &parent_path) { + auto name = GetAvailablePrimName(*mesh->name, pxr::UsdGeomTokens->Mesh, + parent_path); + pxr::SdfPath subcomponent_path = + CreatePrimSpec(data_, parent_path, name, pxr::UsdGeomTokens->Xform); + pxr::SdfPath mesh_path = + CreatePrimSpec(data_, subcomponent_path, pxr::UsdGeomTokens->Mesh, + pxr::UsdGeomTokens->Mesh); + mesh_paths_[*mesh->name] = subcomponent_path; + + // NOTE: The geometry data taken from the spec is the post-compilation + // data after it has been mjCMesh::Compile'd. So don't be surprised if + // things like user defined vertices have moved due to re-centering to + // CoM and other modifications (see mjCMesh::Process for other xforms). + int mesh_id = mjs_getId(mesh->element); + int vert_start_offset = model_->mesh_vertadr[mesh_id] * 3; + int nvert = model_->mesh_vertnum[mesh_id]; + pxr::VtArray points; + points.reserve(nvert); + for (int i = vert_start_offset; i < vert_start_offset + nvert * 3; i += 3) { + points.emplace_back(&model_->mesh_vert[i]); + } + + pxr::SdfPath points_attr_path = + CreateAttributeSpec(data_, mesh_path, pxr::UsdGeomTokens->points, + pxr::SdfValueTypeNames->Vector3fArray); + SetAttributeDefault(data_, points_attr_path, points); + + // NOTE: nface is never 0. + int nface = model_->mesh_facenum[mesh_id]; + pxr::VtArray faces; + faces.reserve(nface * 3); + int face_start_offset = model_->mesh_faceadr[mesh_id] * 3; + for (int i = face_start_offset; i < face_start_offset + nface * 3; i += 3) { + faces.push_back(model_->mesh_face[i]); + faces.push_back(model_->mesh_face[i + 1]); + faces.push_back(model_->mesh_face[i + 2]); + } + pxr::SdfPath face_vertex_idx_attr_path = CreateAttributeSpec( + data_, mesh_path, pxr::UsdGeomTokens->faceVertexIndices, + pxr::SdfValueTypeNames->IntArray); + SetAttributeDefault(data_, face_vertex_idx_attr_path, faces); + + pxr::VtArray vertex_counts; + for (int i = 0; i < nface; ++i) { + // Mujoco is always triangles. + vertex_counts.push_back(3); + } + pxr::SdfPath face_vertex_counts_attr_path = CreateAttributeSpec( + data_, mesh_path, pxr::UsdGeomTokens->faceVertexCounts, + pxr::SdfValueTypeNames->IntArray); + SetAttributeDefault(data_, face_vertex_counts_attr_path, vertex_counts); + + if (model_->mesh_normalnum[mesh_id]) { + // We have to convert from Mujoco's indexed normals to USD's faceVarying + // normals. + pxr::VtArray normals; + normals.reserve(nface * 3); + int normal_start_adr = model_->mesh_normaladr[mesh_id]; + int face_start_offset = model_->mesh_faceadr[mesh_id] * 3; + for (int i = face_start_offset; i < face_start_offset + nface * 3; ++i) { + int normal_adr = normal_start_adr + model_->mesh_facenormal[i]; + normals.emplace_back(&model_->mesh_normal[normal_adr * 3]); + } + pxr::SdfPath normals_attr_path = + CreateAttributeSpec(data_, mesh_path, pxr::UsdGeomTokens->normals, + pxr::SdfValueTypeNames->Vector3fArray); + SetAttributeDefault(data_, normals_attr_path, normals); + SetAttributeMetadata(data_, normals_attr_path, + pxr::UsdGeomTokens->interpolation, + pxr::UsdGeomTokens->faceVarying); + } + + if (model_->mesh_texcoordnum[mesh_id]) { + // We have to convert from Mujoco's indexed texcoords to USD's faceVarying + // texcoords. + pxr::VtArray texcoords; + texcoords.reserve(nface * 3); + int texcoord_start_adr = model_->mesh_texcoordadr[mesh_id]; + int face_start_offset = model_->mesh_faceadr[mesh_id] * 3; + for (int i = face_start_offset; i < face_start_offset + nface * 3; ++i) { + int texcoord_adr = texcoord_start_adr + model_->mesh_facetexcoord[i]; + // Invert the V coordinate, Mujoco assumes OpenGL 0,0 is top left. + // But USD UVs use image bottom left 0,0 convention. + pxr::GfVec2f uv(&model_->mesh_texcoord[texcoord_adr * 2]); + uv[1] = 1.0f - uv[1]; + texcoords.push_back(uv); + } + + pxr::SdfPath texcoords_attr_path = + CreateAttributeSpec(data_, mesh_path, kTokens->primvarsSt, + pxr::SdfValueTypeNames->TexCoord2fArray); + SetAttributeDefault(data_, texcoords_attr_path, texcoords); + SetAttributeMetadata(data_, texcoords_attr_path, + pxr::UsdGeomTokens->interpolation, + pxr::UsdGeomTokens->faceVarying); + } + + // Default subdivision scheme is catmull clark so explicitly set it + // to none here. + pxr::SdfPath subdivision_scheme_path = CreateAttributeSpec( + data_, mesh_path, pxr::UsdGeomTokens->subdivisionScheme, + pxr::SdfValueTypeNames->Token); + SetAttributeDefault(data_, subdivision_scheme_path, + pxr::UsdGeomTokens->none); + } + + void WriteMeshes() { + // Create a scope for the meshes to keep things organized + pxr::SdfPath scope_path = + CreatePrimSpec(data_, body_paths_[kWorldIndex], kTokens->meshScope, + pxr::UsdGeomTokens->Scope); + + // Make the mesh scope invisible since they will be referenced by the bits + // that should be visible. + SetPrimMetadata(data_, scope_path, pxr::SdfFieldKeys->Active, false); + + mjsMesh *mesh = mjs_asMesh(mjs_firstElement(spec_, mjOBJ_MESH)); + while (mesh) { + WriteMesh(mesh, scope_path); + mesh = mjs_asMesh(mjs_nextElement(spec_, mesh->element)); + } + } + + pxr::SdfPath AddTextureShader(const pxr::SdfPath &material_path, + const char *texture_file) { + // Shader "uvmap" + pxr::SdfPath uvmap_shader_path = + CreatePrimSpec(data_, material_path, pxr::TfToken("uvmap"), + pxr::UsdShadeTokens->Shader); + + pxr::SdfPath uvmap_info_id_attr = CreateAttributeSpec( + data_, uvmap_shader_path, pxr::UsdShadeTokens->infoId, + pxr::SdfValueTypeNames->Token, pxr::SdfVariabilityUniform); + SetAttributeDefault(data_, uvmap_info_id_attr, + pxr::UsdImagingTokens->UsdPrimvarReader_float2); + + pxr::SdfPath uvmap_varname_attr = + CreateAttributeSpec(data_, uvmap_shader_path, kTokens->inputsVarname, + pxr::SdfValueTypeNames->Token); + SetAttributeDefault(data_, uvmap_varname_attr, kTokens->st); + + pxr::SdfPath uvmap_st_output_attr = + CreateAttributeSpec(data_, uvmap_shader_path, kTokens->outputsSt, + pxr::SdfValueTypeNames->Float2); + + // Shader "texture" + pxr::SdfPath texture_shader_path = + CreatePrimSpec(data_, material_path, pxr::TfToken("texture"), + pxr::UsdShadeTokens->Shader); + pxr::SdfPath texture_info_id_attr = CreateAttributeSpec( + data_, texture_shader_path, pxr::UsdShadeTokens->infoId, + pxr::SdfValueTypeNames->Token, pxr::SdfVariabilityUniform); + SetAttributeDefault(data_, texture_info_id_attr, + pxr::UsdImagingTokens->UsdUVTexture); + + pxr::SdfPath texture_file_attr = + CreateAttributeSpec(data_, texture_shader_path, kTokens->inputsFile, + pxr::SdfValueTypeNames->Asset); + SetAttributeDefault(data_, texture_file_attr, + pxr::SdfAssetPath(texture_file)); + + pxr::SdfPath texture_st_input_attr = + CreateAttributeSpec(data_, texture_shader_path, kTokens->inputsSt, + pxr::SdfValueTypeNames->Float2); + AddAttributeConnection(data_, texture_st_input_attr, uvmap_st_output_attr); + + pxr::SdfPath texture_wrap_s_attr = + CreateAttributeSpec(data_, texture_shader_path, kTokens->inputsWrapS, + pxr::SdfValueTypeNames->Token); + SetAttributeDefault(data_, texture_wrap_s_attr, kTokens->repeat); + + pxr::SdfPath texture_wrap_t_attr = + CreateAttributeSpec(data_, texture_shader_path, kTokens->inputsWrapT, + pxr::SdfValueTypeNames->Token); + SetAttributeDefault(data_, texture_wrap_t_attr, kTokens->repeat); + + pxr::SdfPath texture_rgb_output_attr = + CreateAttributeSpec(data_, texture_shader_path, kTokens->outputsRgb, + pxr::SdfValueTypeNames->Float3); + + return texture_rgb_output_attr; + } + + void WriteMaterial(mjsMaterial *material, const pxr::SdfPath &parent_path) { + auto name = GetAvailablePrimName( + *material->name, pxr::UsdShadeTokens->Material, parent_path); + pxr::SdfPath material_path = + CreatePrimSpec(data_, parent_path, name, pxr::UsdShadeTokens->Material); + + // Shader "PreviewSurface" + pxr::SdfPath preview_surface_shader_path = CreatePrimSpec( + data_, material_path, kTokens->surface, pxr::UsdShadeTokens->Shader); + + pxr::SdfPath info_id_attr = CreateAttributeSpec( + data_, preview_surface_shader_path, pxr::UsdShadeTokens->infoId, + pxr::SdfValueTypeNames->Token, pxr::SdfVariabilityUniform); + SetAttributeDefault(data_, info_id_attr, + pxr::UsdImagingTokens->UsdPreviewSurface); + + pxr::SdfPath surface_output_attr = CreateAttributeSpec( + data_, preview_surface_shader_path, pxr::UsdShadeTokens->outputsSurface, + pxr::SdfValueTypeNames->Token); + + pxr::SdfPath displacement_output_attr = + CreateAttributeSpec(data_, preview_surface_shader_path, + pxr::UsdShadeTokens->outputsDisplacement, + pxr::SdfValueTypeNames->Token); + + pxr::SdfPath diffuse_color_attr = CreateAttributeSpec( + data_, preview_surface_shader_path, kTokens->inputsDiffuseColor, + pxr::SdfValueTypeNames->Color3f); + + // Find the main texture if specified. + std::string main_texture_name = (*material->textures)[mjTEXROLE_RGB]; + mjsTexture *main_texture = mjs_asTexture( + mjs_findElement(spec_, mjOBJ_TEXTURE, main_texture_name.c_str())); + if (main_texture) { + // Create the texture shader and connect it to the diffuse color + // attribute. + pxr::SdfPath texture_rgb_output_attr = + AddTextureShader(material_path, main_texture->file->c_str()); + AddAttributeConnection(data_, diffuse_color_attr, + texture_rgb_output_attr); + } else { + // If no texture is specified, use the rgba diffuse color. + SetAttributeDefault(data_, diffuse_color_attr, + pxr::GfVec3f(material->rgba[0], material->rgba[1], + material->rgba[2])); + } + + pxr::SdfPath metallic_attr = CreateAttributeSpec( + data_, preview_surface_shader_path, kTokens->inputsMetallic, + pxr::SdfValueTypeNames->Float); + SetAttributeDefault(data_, metallic_attr, material->metallic); + + pxr::SdfPath material_surface_output_attr = CreateAttributeSpec( + data_, material_path, pxr::UsdShadeTokens->outputsSurface, + pxr::SdfValueTypeNames->Token); + + AddAttributeConnection(data_, material_surface_output_attr, + surface_output_attr); + + pxr::SdfPath material_displacement_output_attr = CreateAttributeSpec( + data_, material_path, pxr::UsdShadeTokens->outputsDisplacement, + pxr::SdfValueTypeNames->Token); + + AddAttributeConnection(data_, material_displacement_output_attr, + displacement_output_attr); + } + + void WriteMaterials() { + // Create a scope for the meshes to keep things organized + pxr::SdfPath scope_path = + CreatePrimSpec(data_, body_paths_[kWorldIndex], kTokens->materialsScope, + pxr::UsdGeomTokens->Scope); + + mjsMaterial *material = + mjs_asMaterial(mjs_firstElement(spec_, mjOBJ_MATERIAL)); + while (material) { + WriteMaterial(material, scope_path); + material = mjs_asMaterial(mjs_nextElement(spec_, material->element)); + } + } + + pxr::SdfPath WriteMeshGeom(const mjsGeom *geom, + const pxr::SdfPath &body_path) { + std::string mj_name = geom->name->empty() ? *geom->meshname : *geom->name; + auto name = + GetAvailablePrimName(mj_name, pxr::UsdGeomTokens->Mesh, body_path); + pxr::SdfPath subcomponent_path = + CreatePrimSpec(data_, body_path, name, pxr::UsdGeomTokens->Xform); + + // Reference the mesh asset written in WriteMeshes. + AddPrimReference(data_, subcomponent_path, mesh_paths_[*geom->meshname]); + + return subcomponent_path; + } + + pxr::SdfPath WriteBoxGeom(const mjsGeom *geom, + const pxr::SdfPath &body_path) { + auto name = + GetAvailablePrimName(*geom->name, pxr::UsdGeomTokens->Cube, body_path); + pxr::SdfPath box_path = + CreatePrimSpec(data_, body_path, name, pxr::UsdGeomTokens->Cube); + + int geom_idx = mjs_getId(geom->element); + mjtNum *geom_size = &model_->geom_size[geom_idx * 3]; + + // MuJoCo uses half sizes. + pxr::SdfPath size_attr_path = + CreateAttributeSpec(data_, box_path, pxr::UsdGeomTokens->size, + pxr::SdfValueTypeNames->Float); + pxr::GfVec3f scale(static_cast(geom_size[0]), + static_cast(geom_size[1]), + static_cast(geom_size[2])); + SetAttributeDefault(data_, size_attr_path, 2.0); + + pxr::SdfPath extent_attr_path = + CreateAttributeSpec(data_, box_path, pxr::UsdGeomTokens->extent, + pxr::SdfValueTypeNames->Float3Array); + SetAttributeDefault( + data_, extent_attr_path, + pxr::VtArray({ + pxr::GfVec3f(-geom_size[0], -geom_size[1], -geom_size[2]), + pxr::GfVec3f(geom_size[0], geom_size[1], geom_size[2]), + })); + + WriteScaleXformOp(box_path, scale); + WriteXformOpOrder(box_path, + pxr::VtArray{kTokens->xformOpScale}); + return box_path; + } + + pxr::SdfPath WriteCapsuleGeom(const mjsGeom *geom, + const pxr::SdfPath &body_path) { + auto name = GetAvailablePrimName(*geom->name, pxr::UsdGeomTokens->Capsule, + body_path); + pxr::SdfPath capsule_path = + CreatePrimSpec(data_, body_path, name, pxr::UsdGeomTokens->Capsule); + + int geom_idx = mjs_getId(geom->element); + mjtNum *geom_size = &model_->geom_size[geom_idx * 3]; + + // MuJoCo uses half sizes. + pxr::SdfPath radius_attr_path = + CreateAttributeSpec(data_, capsule_path, pxr::UsdGeomTokens->radius, + pxr::SdfValueTypeNames->Float); + SetAttributeDefault(data_, radius_attr_path, geom_size[0] * 2); + + pxr::SdfPath height_attr_path = + CreateAttributeSpec(data_, capsule_path, pxr::UsdGeomTokens->height, + pxr::SdfValueTypeNames->Float); + SetAttributeDefault(data_, height_attr_path, geom_size[1] * 2); + return capsule_path; + } + + pxr::SdfPath WriteCylinderGeom(const mjsGeom *geom, + const pxr::SdfPath &body_path) { + auto name = GetAvailablePrimName(*geom->name, pxr::UsdGeomTokens->Cylinder, + body_path); + pxr::SdfPath cylinder_path = + CreatePrimSpec(data_, body_path, name, pxr::UsdGeomTokens->Cylinder); + + int geom_idx = mjs_getId(geom->element); + mjtNum *geom_size = &model_->geom_size[geom_idx * 3]; + + // MuJoCo uses half sizes. + pxr::SdfPath radius_attr_path = + CreateAttributeSpec(data_, cylinder_path, pxr::UsdGeomTokens->radius, + pxr::SdfValueTypeNames->Float); + SetAttributeDefault(data_, radius_attr_path, geom_size[0] * 2); + + pxr::SdfPath height_attr_path = + CreateAttributeSpec(data_, cylinder_path, pxr::UsdGeomTokens->height, + pxr::SdfValueTypeNames->Float); + SetAttributeDefault(data_, height_attr_path, geom_size[1] * 2); + return cylinder_path; + } + + pxr::SdfPath WriteEllipsoidGeom(const mjsGeom *geom, + const pxr::SdfPath &body_path) { + auto name = GetAvailablePrimName(*geom->name, pxr::UsdGeomTokens->Sphere, + body_path); + pxr::SdfPath ellipsoid_path = + CreatePrimSpec(data_, body_path, name, pxr::UsdGeomTokens->Sphere); + + int geom_idx = mjs_getId(geom->element); + mjtNum *geom_size = &model_->geom_size[geom_idx * 3]; + + pxr::GfVec3f scale = {static_cast(geom_size[0] * 2), + static_cast(geom_size[1] * 2), + static_cast(geom_size[2] * 2)}; + + // MuJoCo uses half sizes. + pxr::SdfPath radius_attr_path = + CreateAttributeSpec(data_, ellipsoid_path, pxr::UsdGeomTokens->radius, + pxr::SdfValueTypeNames->Float); + SetAttributeDefault(data_, radius_attr_path, 1.0f); + + WriteScaleXformOp(ellipsoid_path, scale); + WriteXformOpOrder(ellipsoid_path, + pxr::VtArray{kTokens->xformOpScale}); + return ellipsoid_path; + } + + pxr::SdfPath WriteSphereGeom(const mjsGeom *geom, + const pxr::SdfPath &body_path) { + auto name = GetAvailablePrimName(*geom->name, pxr::UsdGeomTokens->Sphere, + body_path); + pxr::SdfPath sphere_path = + CreatePrimSpec(data_, body_path, name, pxr::UsdGeomTokens->Sphere); + + int geom_idx = mjs_getId(geom->element); + mjtNum *geom_size = &model_->geom_size[geom_idx * 3]; + + // MuJoCo uses half sizes. + pxr::SdfPath radius_attr_path = + CreateAttributeSpec(data_, sphere_path, pxr::UsdGeomTokens->radius, + pxr::SdfValueTypeNames->Float); + SetAttributeDefault(data_, radius_attr_path, geom_size[0] * 2); + return sphere_path; + } + + void WriteGeom(mjsGeom *geom, const mjsBody *body) { + const int body_id = mjs_getId(body->element); + const auto &body_path = body_paths_[body_id]; + auto name = GetAvailablePrimName(*geom->name, kTokens->geom, body_path); + + pxr::SdfPath geom_path; + int geom_id = mjs_getId(geom->element); + switch (geom->type) { + case mjGEOM_MESH: + geom_path = WriteMeshGeom(geom, body_path); + break; + case mjGEOM_BOX: + geom_path = WriteBoxGeom(geom, body_path); + break; + case mjGEOM_CAPSULE: + geom_path = WriteCapsuleGeom(geom, body_path); + break; + case mjGEOM_CYLINDER: + geom_path = WriteCylinderGeom(geom, body_path); + break; + case mjGEOM_ELLIPSOID: + geom_path = WriteEllipsoidGeom(geom, body_path); + break; + case mjGEOM_SPHERE: + geom_path = WriteSphereGeom(geom, body_path); + break; + default: + TF_WARN(UnsupportedGeomTypeError, "Unsupported geom type for geom %d", + geom_id); + return; + } + + mjsDefault *spec_default = mjs_getDefault(geom->element); + pxr::TfToken valid_class_name = GetValidPrimName(*spec_default->name); + pxr::SdfPath geom_class_path = class_path_.AppendChild(valid_class_name); + if (!data_->HasSpec(geom_class_path)) { + pxr::SdfPath class_path = + CreateClassSpec(data_, class_path_, valid_class_name); + auto visibility_attr = + CreateAttributeSpec(data_, class_path, pxr::UsdGeomTokens->visibility, + pxr::SdfValueTypeNames->Token); + SetAttributeDefault(data_, visibility_attr, + pxr::UsdGeomTokens->inherited); + } + + // Bind material if it exists. + if (!geom->material->empty()) { + pxr::SdfPath material_path = + body_paths_[kWorldIndex] + .AppendChild(kTokens->materialsScope) + .AppendChild(GetValidPrimName(*geom->material)); + if (data_->HasSpec(material_path)) { + ApplyApiSchema(data_, geom_path, + pxr::UsdShadeTokens->MaterialBindingAPI); + // Bind the material to this geom. + CreateRelationshipSpec(data_, geom_path, + pxr::UsdShadeTokens->materialBinding, + material_path, pxr::SdfVariabilityUniform); + } + } + + if (body_id == kWorldIndex) { + SetPrimKind(data_, geom_path, pxr::KindTokens->component); + } + // Inherit from class. + AddPrimInherit(data_, geom_path, geom_class_path); + + auto transform = MujocoPosQuatToTransform(&model_->geom_pos[3 * geom_id], + &model_->geom_quat[4 * geom_id]); + WriteTransformXformOp(geom_path, transform); + + PrependToXformOpOrder( + geom_path, pxr::VtArray{kTokens->xformOpTransform}); + } + + void WriteGeoms(mjsBody *body) { + mjsGeom *geom = mjs_asGeom(mjs_firstChild(body, mjOBJ_GEOM, false)); + while (geom) { + WriteGeom(geom, body); + geom = mjs_asGeom(mjs_nextChild(body, geom->element, false)); + } + } + + void WriteCamera(mjsCamera *spec_cam, const mjsBody *body) { + const auto &body_path = body_paths_[mjs_getId(body->element)]; + auto name = GetAvailablePrimName(*spec_cam->name, + pxr::UsdGeomTokens->Camera, body_path); + // Create a root Xform for the world body with the model name if it exists + // otherwise called 'World'. + pxr::SdfPath camera_path = + CreatePrimSpec(data_, body_path, name, pxr::UsdGeomTokens->Camera); + + int cam_id = mjs_getId(spec_cam->element); + auto transform = MujocoPosQuatToTransform(&model_->cam_pos[3 * cam_id], + &model_->cam_quat[4 * cam_id]); + WriteTransformXformOp(camera_path, transform); + WriteXformOpOrder(camera_path, + pxr::VtArray{kTokens->xformOpTransform}); + + // If the camera intrinsics are specified, then it is important that we + // reproduce the code in mujoco/src/engine/engine_vis_visualize.c + const float *cam_sensorsize = &model_->cam_sensorsize[cam_id * 2]; + bool use_intrinsic = cam_sensorsize[1] > 0.0f; + float znear = spec_->visual.map.znear * model_->stat.extent * 100; + float zfar = spec_->visual.map.zfar * model_->stat.extent * 100; + mjtNum fovy = model_->cam_fovy[cam_id]; + const float *cam_intrinsic = &model_->cam_intrinsic[cam_id * 4]; + + const float aspect_ratio = + use_intrinsic ? cam_sensorsize[0] / cam_sensorsize[1] : 4.0f / 3; + float vertical_apperture = + 2 * znear * + (use_intrinsic ? 1.0f / cam_intrinsic[1] * + (cam_sensorsize[1] / 2.f - cam_intrinsic[3]) + : mju_tan((fovy / 2) * (M_PI / 180.0))); + float horizontal_aperture = + use_intrinsic ? 2 * znear / cam_intrinsic[0] * + (cam_sensorsize[0] / 2.f - cam_intrinsic[2]) + : vertical_apperture * aspect_ratio; + + pxr::SdfPath clipping_range_attr_path = CreateAttributeSpec( + data_, camera_path, pxr::UsdGeomTokens->clippingRange, + pxr::SdfValueTypeNames->Float2); + SetAttributeDefault(data_, clipping_range_attr_path, + pxr::GfVec2f(znear, zfar)); + pxr::SdfPath focal_length_attr_path = + CreateAttributeSpec(data_, camera_path, pxr::UsdGeomTokens->focalLength, + pxr::SdfValueTypeNames->Float); + SetAttributeDefault(data_, focal_length_attr_path, znear); + + pxr::SdfPath vertical_aperture_attr_path = CreateAttributeSpec( + data_, camera_path, pxr::UsdGeomTokens->verticalAperture, + pxr::SdfValueTypeNames->Float); + SetAttributeDefault(data_, vertical_aperture_attr_path, vertical_apperture); + + pxr::SdfPath horizontal_aperture_attr_path = CreateAttributeSpec( + data_, camera_path, pxr::UsdGeomTokens->horizontalAperture, + pxr::SdfValueTypeNames->Float); + SetAttributeDefault(data_, horizontal_aperture_attr_path, + horizontal_aperture); + } + + void WriteCameras(mjsBody *body) { + mjsCamera *cam = mjs_asCamera(mjs_firstChild(body, mjOBJ_CAMERA, false)); + while (cam) { + WriteCamera(cam, body); + cam = mjs_asCamera(mjs_nextChild(body, cam->element, false)); + } + } + + void WriteLight(mjsLight *light, const mjsBody *body) { + const auto &body_path = body_paths_[mjs_getId(body->element)]; + auto name = GetAvailablePrimName(*light->name, kTokens->light, body_path); + // Create a root Xform for the world body with the model name if it exists + // otherwise called 'World'. + pxr::SdfPath light_path = + CreatePrimSpec(data_, body_path, name, pxr::UsdLuxTokens->SphereLight); + + int light_id = mjs_getId(light->element); + auto transform = MujocoPosQuatToTransform(&model_->light_pos[3 * light_id], + &model_->light_dir[4 * light_id]); + WriteTransformXformOp(light_path, transform); + WriteXformOpOrder(light_path, + pxr::VtArray{kTokens->xformOpTransform}); + } + + void WriteLights(mjsBody *body) { + mjsLight *light = mjs_asLight(mjs_firstChild(body, mjOBJ_LIGHT, false)); + while (light) { + WriteLight(light, body); + light = mjs_asLight(mjs_nextChild(body, light->element, false)); + } + } + + void WriteBody(mjsBody *body) { + int body_id = mjs_getId(body->element); + pxr::SdfPath parent_path = + CreateParentIfNotExists(body, body_paths_[kWorldIndex], data_); + pxr::TfToken body_name = GetValidPrimName(*body->name); + + // Create Xform prim for body. + pxr::SdfPath body_path = CreatePrimSpec(data_, parent_path, body_name, + pxr::UsdGeomTokens->Xform); + // The parent_path will be a component which makes the actual articulated + // bodies subcomponents. + SetPrimKind(data_, body_path, pxr::KindTokens->subcomponent); + + // Create classes if necessary + mjsDefault *spec_default = mjs_getDefault(body->element); + + pxr::TfToken body_class_name = GetValidPrimName(*spec_default->name); + pxr::SdfPath body_class_path = class_path_.AppendChild(body_class_name); + if (!data_->HasSpec(body_class_path)) { + CreateClassSpec(data_, class_path_, body_class_name); + } + + // Create XformOp attribute for body transform. + pxr::SdfPath xform_op_path = + CreateAttributeSpec(data_, body_path, kTokens->xformOpTransform, + pxr::SdfValueTypeNames->Matrix4d); + + // Make sure to account for the parent since UsdPhysics doesn't support + // nested bodies! + auto parent_xform = body_xforms_[model_->body_parentid[body_id]]; + // mjModel will have all frames already accounted for so no need to worry + // about them here. + body_xforms_[body_id] = + MujocoPosQuatToTransform(&model_->body_pos[body_id * 3], + &model_->body_quat[body_id * 4]) * + parent_xform; + SetAttributeDefault(data_, xform_op_path, body_xforms_[body_id]); + + // Create XformOpOrder attribute for body transform order. + // For us this is simply the transform we authored above. + WriteXformOpOrder(body_path, + pxr::VtArray{kTokens->xformOpTransform}); + + pxr::VtDictionary customData; + customData[kTokens->body_name] = *body->name; + SetPrimMetadata(data_, body_path, pxr::SdfFieldKeys->CustomData, + customData); + + body_paths_[body_id] = body_path; + } + + void WriteBodies() { + mjsBody *body = mjs_asBody(mjs_firstElement(spec_, mjOBJ_BODY)); + while (body) { + // Only write a rigidbody if we are not the world body. + // We fall through since the world body might have static + // geom children. + if (mjs_getId(body->element) != kWorldIndex) { + WriteBody(body); + } + WriteGeoms(body); + WriteCameras(body); + WriteLights(body); + body = mjs_asBody(mjs_nextElement(spec_, body->element)); + } + } + + pxr::SdfPath WriteWorldBody(const size_t body_index) { + // Create a root Xform for the world body with the model name if it exists + // otherwise called 'World'. + auto name = GetAvailablePrimName(*spec_->modelname, kTokens->world, + pxr::SdfPath::AbsoluteRootPath()); + pxr::SdfPath world_group_path = + CreatePrimSpec(data_, pxr::SdfPath::AbsoluteRootPath(), name, + pxr::UsdGeomTokens->Xform); + SetPrimKind(data_, world_group_path, pxr::KindTokens->group); + return world_group_path; + } +}; + +namespace mujoco { +namespace usd { + +bool WriteSpecToData(mjSpec *spec, pxr::SdfAbstractDataRefPtr &data) { + // Create pseudo root first. + data->CreateSpec(pxr::SdfPath::AbsoluteRootPath(), + pxr::SdfSpecTypePseudoRoot); + + mjModel *model = mj_compile(spec, nullptr); + if (model == nullptr) { + TF_ERROR(MujocoCompilationError, "%s", mjs_getError(spec)); + return false; + } + + ModelWriter(spec, model, data).Write(); + + return true; +} + +} // namespace usd +} // namespace mujoco diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.h b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.h new file mode 100644 index 00000000..4c5934c6 --- /dev/null +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.h @@ -0,0 +1,32 @@ +// Copyright 2025 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. + +#ifndef MUJOCO_SRC_EXPERIMENTAL_USD_PLUGINS_MJCF_MUJOCO_TO_USD_H_ +#define MUJOCO_SRC_EXPERIMENTAL_USD_PLUGINS_MJCF_MUJOCO_TO_USD_H_ + +#include +#include + +namespace mujoco { +namespace usd { +// Given an mjSpec, write it to a SdfAbstractData. +// +// Args: +// spec: mjSpec built programmatically or via parsed XML. +// data: SdfAbstractDataRefPtr that will be written to. +bool WriteSpecToData(mjSpec* spec, pxr::SdfAbstractDataRefPtr& data); +} // namespace usd +} // namespace mujoco + +#endif // MUJOCO_SRC_EXPERIMENTAL_USD_PLUGINS_MJCF_MUJOCO_TO_USD_H_ diff --git a/src/experimental/usd/plugins/mjcf/plugInfo.json b/src/experimental/usd/plugins/mjcf/plugInfo.json new file mode 100644 index 00000000..25869b76 --- /dev/null +++ b/src/experimental/usd/plugins/mjcf/plugInfo.json @@ -0,0 +1,25 @@ +{ + "Plugins": [ + { + "Info": { + "Types": { + "UsdMjcfFileFormat": { + "bases": ["SdfFileFormat"], + "displayName": "MJCF USD File Format", + "extensions": ["xml"], + "formatId": "xml", + "primary": true, + "supportsReading": true, + "supportsWriting": false, + "target": "usd" + } + } + }, + "LibraryPath": "", + "Name": "usdMjcf", + "ResourcePath": "", + "Root": ".", + "Type": "library" + } + ] +} diff --git a/src/experimental/usd/plugins/mjcf/utils.cc b/src/experimental/usd/plugins/mjcf/utils.cc new file mode 100644 index 00000000..880cce55 --- /dev/null +++ b/src/experimental/usd/plugins/mjcf/utils.cc @@ -0,0 +1,199 @@ +// Copyright 2025 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. + +#include "mjcf/utils.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace { + +template +void AppendChild(pxr::SdfAbstractDataRefPtr& data, const pxr::SdfPath& specPath, + const pxr::TfToken& childKey, const T& child) { + // Get existing children. + std::vector children; + pxr::SdfAbstractDataTypedValue> getter(&children); + data->Has(specPath, childKey, &getter); + + children.push_back(child); + data->Set(specPath, childKey, + pxr::SdfAbstractDataConstTypedValue>(&children)); +} + +template +void AppendListOp(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& spec_path, const pxr::TfToken& field, + const T& item) { + // Get existing list op. + pxr::SdfListOp list_op; + pxr::SdfAbstractDataTypedValue> getter(&list_op); + data->Has(spec_path, field, &getter); + + auto items = list_op.GetExplicitItems(); + items.push_back(item); + list_op.SetExplicitItems(items); + data->Set(spec_path, field, + pxr::SdfAbstractDataConstTypedValue>(&list_op)); +} + +template +void PrependListOp(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& spec_path, const pxr::TfToken& field, + const T& item) { + // Get existing list op. + pxr::SdfListOp listOp; + pxr::SdfAbstractDataTypedValue> getter(&listOp); + data->Has(spec_path, field, &getter); + + auto prependedItems = listOp.GetPrependedItems(); + prependedItems.insert(prependedItems.begin(), item); + listOp.SetPrependedItems(prependedItems); + data->Set(spec_path, field, + pxr::SdfAbstractDataConstTypedValue>(&listOp)); +} +} // namespace + +namespace mujoco { +namespace usd { + +pxr::SdfPath CreatePrimSpec(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& parent_path, + const pxr::TfToken& name, const pxr::TfToken& type, + pxr::SdfSpecifier specifier) { + const pxr::SdfPath prim_path = parent_path.AppendChild(name); + data->CreateSpec(prim_path, pxr::SdfSpecTypePrim); + data->Set(prim_path, pxr::SdfFieldKeys->Specifier, + pxr::SdfAbstractDataConstTypedValue(&specifier)); + if (!type.IsEmpty()) { + data->Set(prim_path, pxr::SdfFieldKeys->TypeName, + pxr::SdfAbstractDataConstTypedValue(&type)); + } + + AppendChild(data, parent_path, pxr::SdfChildrenKeys->PrimChildren, name); + + return prim_path; +} + +pxr::SdfPath CreateAttributeSpec(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& prim_path, + const pxr::TfToken& name, + const pxr::SdfValueTypeName& type_name, + pxr::SdfVariability variability) { + const pxr::SdfPath propertyPath = prim_path.AppendProperty(name); + data->CreateSpec(propertyPath, pxr::SdfSpecTypeAttribute); + + pxr::TfToken typeNameToken = type_name.GetAsToken(); + data->Set(propertyPath, pxr::SdfFieldKeys->TypeName, + pxr::SdfAbstractDataConstTypedValue(&typeNameToken)); + if (variability != pxr::SdfVariabilityVarying) { + data->Set( + propertyPath, pxr::SdfFieldKeys->Variability, + pxr::SdfAbstractDataConstTypedValue(&variability)); + } + + AppendChild(data, prim_path, pxr::SdfChildrenKeys->PropertyChildren, name); + + return propertyPath; +} + +pxr::SdfPath CreateRelationshipSpec(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& prim_path, + const pxr::TfToken& relationship_name, + const pxr::SdfPath& relationship_path, + pxr::SdfVariability variability) { + pxr::SdfPath prop_path = prim_path.AppendProperty(relationship_name); + data->CreateSpec(prop_path, pxr::SdfSpecTypeRelationship); + if (variability != pxr::SdfVariabilityVarying) { + data->Set( + prop_path, pxr::SdfFieldKeys->Variability, + pxr::SdfAbstractDataConstTypedValue(&variability)); + } + + AppendChild(data, prim_path, pxr::SdfChildrenKeys->PropertyChildren, + relationship_name); + + AppendChild(data, prop_path, pxr::SdfChildrenKeys->RelationshipTargetChildren, + relationship_path); + AppendListOp(data, prop_path, pxr::SdfFieldKeys->TargetPaths, + relationship_path); + + pxr::SdfPath target_path = prop_path.AppendTarget(relationship_path); + data->CreateSpec(target_path, pxr::SdfSpecTypeRelationshipTarget); + + return prop_path; +} + +pxr::SdfPath CreateClassSpec(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& prim_path, + const pxr::TfToken& class_name) { + pxr::SdfPath class_path = prim_path.AppendChild(class_name); + pxr::SdfSpecifier class_specifier = pxr::SdfSpecifier::SdfSpecifierClass; + data->CreateSpec(class_path, pxr::SdfSpecTypePrim); + data->Set( + class_path, pxr::SdfFieldKeys->Specifier, + pxr::SdfAbstractDataConstTypedValue(&class_specifier)); + + AppendChild(data, prim_path, pxr::SdfChildrenKeys->PrimChildren, class_name); + + return class_path; +} + +void AddAttributeConnection(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& attribute_path, + const pxr::SdfPath& target_attribute_path) { + AppendChild(data, attribute_path, pxr::SdfChildrenKeys->ConnectionChildren, + target_attribute_path); + AppendListOp(data, attribute_path, pxr::SdfFieldKeys->ConnectionPaths, + target_attribute_path); + + data->CreateSpec(attribute_path.AppendTarget(target_attribute_path), + pxr::SdfSpecTypeConnection); +} + +void AddPrimReference(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& prim_path, + const pxr::SdfPath& referenced_prim_path) { + PrependListOp(data, prim_path, pxr::SdfFieldKeys->References, + pxr::SdfReference("", referenced_prim_path)); +} + +void AddPrimInherit(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& prim_path, + const pxr::SdfPath& class_path) { + PrependListOp(data, prim_path, pxr::SdfFieldKeys->InheritPaths, class_path); +} + +void ApplyApiSchema(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& prim_path, + const pxr::TfToken& schema_name) { + PrependListOp(data, prim_path, pxr::UsdTokens->apiSchemas, schema_name); +} + +void SetPrimKind(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& prim_path, pxr::TfToken kind) { + SetPrimMetadata(data, prim_path, pxr::TfToken("kind"), kind); +} + +} // namespace usd +} // namespace mujoco diff --git a/src/experimental/usd/plugins/mjcf/utils.h b/src/experimental/usd/plugins/mjcf/utils.h new file mode 100644 index 00000000..4a2643ec --- /dev/null +++ b/src/experimental/usd/plugins/mjcf/utils.h @@ -0,0 +1,127 @@ +// Copyright 2025 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. + +#ifndef MUJOCO_SRC_EXPERIMENTAL_USD_PLUGINS_MJCF_UTILS_H_ +#define MUJOCO_SRC_EXPERIMENTAL_USD_PLUGINS_MJCF_UTILS_H_ + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace mujoco { +namespace usd { + +// Create a prim spec and append it as a child of parent_path. +pxr::SdfPath CreatePrimSpec( + pxr::SdfAbstractDataRefPtr& data, const pxr::SdfPath& parent_path, + const pxr::TfToken& name, const pxr::TfToken& type = pxr::TfToken(), + pxr::SdfSpecifier specifier = pxr::SdfSpecifier::SdfSpecifierDef); + +// Create an attribute spec and append it as a child of parent_path. +// By default the attribute will be varying. +pxr::SdfPath CreateAttributeSpec( + pxr::SdfAbstractDataRefPtr& data, const pxr::SdfPath& prim_path, + const pxr::TfToken& name, const pxr::SdfValueTypeName& type_name, + pxr::SdfVariability variability = pxr::SdfVariabilityVarying); + +// Create a relationship spec and append it as a child of prim_path. +pxr::SdfPath CreateRelationshipSpec( + pxr::SdfAbstractDataRefPtr& data, const pxr::SdfPath& prim_path, + const pxr::TfToken& relationship_name, + const pxr::SdfPath& relationship_path, + pxr::SdfVariability variability = pxr::SdfVariabilityVarying); + +pxr::SdfPath CreateClassSpec(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& prim_path, + const pxr::TfToken& class_name); + +void AddAttributeConnection(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& attribute_path, + const pxr::SdfPath& target_attribute_path); + +void AddPrimReference(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& prim_path, + const pxr::SdfPath& referenced_prim_path); + +void AddPrimInherit(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& prim_path, + const pxr::SdfPath& class_path); + +void ApplyApiSchema(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& prim_path, + const pxr::TfToken& schema_name); + +void SetPrimKind(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& prim_path, pxr::TfToken kind); + +// Set the value specified by key on any field at field_path. +template +void SetField(pxr::SdfAbstractDataRefPtr& data, const pxr::SdfPath& field_path, + const pxr::TfToken key, T&& value) { + using Deduced = typename std::remove_reference_t; + const auto typed_val = pxr::SdfAbstractDataConstTypedValue(&value); + const pxr::SdfAbstractDataConstValue& untyped_val = typed_val; + + data->Set(field_path, key, untyped_val); +} + +// Set the value specified by key on an attribute spec at attribute_path. +template +void SetAttribute(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& attribute_path, const pxr::TfToken key, + T&& value) { + SetField(data, attribute_path, key, value); +} + +// Set the value specified by key on a prim spec at prim_path. +template +void SetPrimMetadata(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& prim_path, const pxr::TfToken key, + T&& value) { + SetAttribute(data, prim_path, key, value); +} + +// Set the value specified by key on an attribute spec at attribute_path. +template +void SetAttributeMetadata(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& attribute_path, + const pxr::TfToken key, T&& value) { + SetAttribute(data, attribute_path, key, value); +} + +// Set the default value on an attribute spec at attribute_path. +template +void SetAttributeDefault(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& attribute_path, + T&& default_value) { + SetAttribute(data, attribute_path, pxr::SdfFieldKeys->Default, default_value); +} + +// Set the value specified by key on the root layer. +template +void SetLayerMetadata(pxr::SdfAbstractDataRefPtr& data, const pxr::TfToken& key, + T&& value) { + SetAttribute(data, pxr::SdfPath::AbsoluteRootPath(), key, value); +} + +} // namespace usd +} // namespace mujoco + +#endif // MUJOCO_SRC_EXPERIMENTAL_USD_PLUGINS_MJCF_UTILS_H_ diff --git a/test/experimental/usd/plugins/mjcf/fixture.cc b/test/experimental/usd/plugins/mjcf/fixture.cc new file mode 100644 index 00000000..19cd8e6e --- /dev/null +++ b/test/experimental/usd/plugins/mjcf/fixture.cc @@ -0,0 +1,64 @@ +// Copyright 2025 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. + +#include "test/experimental/usd/plugins/mjcf/fixture.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace mujoco { + +using pxr::SdfPath; + +pxr::SdfLayerRefPtr LoadLayer(const std::string& xml) { + auto layer = pxr::SdfLayer::CreateAnonymous( + "test_layer", pxr::SdfFileFormat::FindByExtension("xml")); + layer->ImportFromString(xml); + EXPECT_THAT(layer, testing::NotNull()); + return layer; +} + +template <> +void ExpectAttributeEqual(pxr::UsdStageRefPtr stage, + const char* path, + const pxr::SdfAssetPath& value) { + auto attr = stage->GetAttributeAtPath(pxr::SdfPath(path)); + EXPECT_TRUE(attr.IsValid()); + pxr::SdfAssetPath attr_value; + attr.Get(&attr_value); + EXPECT_EQ(attr_value.GetAssetPath(), value.GetAssetPath()); +} + +void ExpectAttributeHasConnection(pxr::UsdStageRefPtr stage, const char* path, + const char* connection_path) { + auto attr = stage->GetAttributeAtPath(SdfPath(path)); + EXPECT_TRUE(attr.IsValid()); + pxr::SdfPathVector sources; + attr.GetConnections(&sources); + EXPECT_EQ(sources.size(), 1); + EXPECT_EQ(sources[0], SdfPath(connection_path)); +} +// +} // namespace mujoco diff --git a/test/experimental/usd/plugins/mjcf/fixture.h b/test/experimental/usd/plugins/mjcf/fixture.h new file mode 100644 index 00000000..64985fae --- /dev/null +++ b/test/experimental/usd/plugins/mjcf/fixture.h @@ -0,0 +1,67 @@ +// Copyright 2025 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. + +#ifndef MUJOCO_TEST_EXPERIMENTAL_USD_PLUGINS_MJCF_FIXTURE_H_ +#define MUJOCO_TEST_EXPERIMENTAL_USD_PLUGINS_MJCF_FIXTURE_H_ + +#include + +#include +#include "test/fixture.h" +#include +#include +#include +#include +#include +#include + +#define EXPECT_PRIM_VALID(stage, path) \ + EXPECT_TRUE((stage)->GetPrimAtPath(SdfPath(path)).IsValid()); + +#define EXPECT_PRIM_KIND(stage, path, kind) \ + { \ + pxr::TfToken prim_kind; \ + pxr::UsdModelAPI::Get(stage, SdfPath(path)).GetKind(&prim_kind); \ + EXPECT_EQ(kind, prim_kind); \ + } +namespace mujoco { + +pxr::SdfLayerRefPtr LoadLayer(const std::string& xml); + +template +void ExpectAttributeEqual(pxr::UsdStageRefPtr stage, const char* path, + const T& value) { + auto attr = stage->GetAttributeAtPath(pxr::SdfPath(path)); + EXPECT_TRUE(attr.IsValid()); + T attr_value; + attr.Get(&attr_value); + EXPECT_EQ(attr_value, value); +} + +// Specialization for SdfAssetPath, so that we can compare only the asset path +// and not care about whatever the resolved path is. +// Otherwise the default operator== would fail because it tests for equality of +// the asset path AND the resolved path. +template <> +void ExpectAttributeEqual(pxr::UsdStageRefPtr stage, + const char* path, + const pxr::SdfAssetPath& value); + +void ExpectAttributeHasConnection(pxr::UsdStageRefPtr stage, const char* path, + const char* connection_path); + +using MjcfSdfFileFormatPluginTest = MujocoTest; + +} // namespace mujoco +#endif // MUJOCO_TEST_EXPERIMENTAL_USD_PLUGINS_MJCF_FIXTURE_H_ diff --git a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc new file mode 100644 index 00000000..8ca9bdba --- /dev/null +++ b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc @@ -0,0 +1,387 @@ +// Copyright 2025 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. + +#include +#include + +#include +#include +#include +#include "test/experimental/usd/plugins/mjcf/fixture.h" +#include "test/fixture.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +PXR_NAMESPACE_OPEN_SCOPE +// clang-format off +TF_DEFINE_PRIVATE_TOKENS(_tokens, + (st) + ); +// clang-format on +PXR_NAMESPACE_CLOSE_SCOPE + +namespace mujoco { +namespace { + +using pxr::SdfPath; + +static const char* kMaterialsPath = + "experimental/usd/plugins/mjcf/testdata/materials.xml"; +static const char* kMeshObjPath = + "experimental/usd/plugins/mjcf/testdata/mesh_obj.xml"; + +TEST_F(MjcfSdfFileFormatPluginTest, TestClassAuthored) { + static constexpr char kXml[] = R"( + + + + + + + + + + + + )"; + + pxr::SdfLayerRefPtr layer = LoadLayer(kXml); + + auto stage = pxr::UsdStage::Open(layer); + EXPECT_PRIM_VALID(stage, "/__class__"); + EXPECT_PRIM_VALID(stage, "/__class__/test"); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestBasicMeshSources) { + static constexpr char kXml[] = R"( + + + + + + + + + + + )"; + + pxr::SdfLayerRefPtr layer = LoadLayer(kXml); + + auto stage = pxr::UsdStage::Open(layer); + EXPECT_PRIM_VALID(stage, "/mesh_test"); + EXPECT_PRIM_VALID(stage, "/mesh_test/test_body/test_body/tetrahedron"); + EXPECT_PRIM_VALID(stage, "/mesh_test/test_body/test_body/tetrahedron/Mesh"); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestMaterials) { + const std::string xml_path = GetTestDataFilePath(kMaterialsPath); + + auto stage = pxr::UsdStage::Open(xml_path); + EXPECT_THAT(stage, testing::NotNull()); + + EXPECT_PRIM_VALID(stage, "/mesh_test"); + EXPECT_PRIM_VALID(stage, "/mesh_test/Materials"); + + EXPECT_PRIM_VALID(stage, "/mesh_test/Materials/material_red"); + EXPECT_PRIM_VALID(stage, "/mesh_test/Materials/material_red/PreviewSurface"); + ExpectAttributeEqual( + stage, + "/mesh_test/Materials/material_red/PreviewSurface.inputs:diffuseColor", + pxr::GfVec3f(0.8, 0, 0)); + + EXPECT_PRIM_VALID(stage, "/mesh_test/Materials/material_texture"); + EXPECT_PRIM_VALID(stage, + "/mesh_test/Materials/material_texture/PreviewSurface"); + EXPECT_PRIM_VALID(stage, "/mesh_test/Materials/material_texture/uvmap"); + EXPECT_PRIM_VALID(stage, "/mesh_test/Materials/material_texture/texture"); + ExpectAttributeHasConnection( + stage, + "/mesh_test/Materials/material_texture/" + "PreviewSurface.inputs:diffuseColor", + "/mesh_test/Materials/material_texture/texture.outputs:rgb"); + ExpectAttributeEqual( + stage, "/mesh_test/Materials/material_texture/texture.inputs:file", + pxr::SdfAssetPath("textures/cube.png")); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestFaceVaryingMeshSourcesSimpleMjcfMesh) { + static constexpr char kXml[] = R"( + + + + + + + + + + + )"; + + pxr::SdfLayerRefPtr layer = LoadLayer(kXml); + + auto stage = pxr::UsdStage::Open(layer); + + auto mesh = pxr::UsdGeomMesh::Get( + stage, SdfPath("/mesh_test/test_body/test_body/tetrahedron/Mesh")); + ASSERT_TRUE(mesh); + pxr::VtArray face_vertex_counts; + mesh.GetFaceVertexCountsAttr().Get(&face_vertex_counts); + EXPECT_EQ(face_vertex_counts.size(), 4); + EXPECT_EQ(face_vertex_counts, pxr::VtArray({3, 3, 3, 3})); + + pxr::VtArray face_vertex_indices; + mesh.GetFaceVertexIndicesAttr().Get(&face_vertex_indices); + EXPECT_EQ(face_vertex_indices.size(), 12); + EXPECT_EQ(face_vertex_indices, + pxr::VtArray({0, 3, 2, 0, 1, 3, 0, 2, 1, 1, 2, 3})); + + pxr::VtArray normals; + mesh.GetNormalsAttr().Get(&normals); + EXPECT_EQ(normals.size(), face_vertex_indices.size()); + // We can't directly check the normals values because they are altered by + // Mujoco's compiling step. So we at least check that normals with the same + // original index are the same. + for (int i = 0; i < face_vertex_indices.size(); ++i) { + for (int j = i + 1; j < face_vertex_indices.size(); ++j) { + if (face_vertex_indices[i] == face_vertex_indices[j]) { + EXPECT_EQ(normals[i], normals[j]); + } + } + } + + auto primvars_api = pxr::UsdGeomPrimvarsAPI(mesh.GetPrim()); + + pxr::VtArray texcoords; + EXPECT_TRUE(primvars_api.HasPrimvar(pxr::_tokens->st)); + auto primvar_st = primvars_api.GetPrimvar(pxr::_tokens->st); + primvar_st.Get(&texcoords); + EXPECT_EQ(texcoords.size(), face_vertex_indices.size()); + + // Check the faceVarying texcoords against the manually indexed source + // texcoords. + pxr::VtArray source_texcoords{ + {0.5, 0.5}, {0, 0.5}, {1, 0}, {1, 1}}; + for (int i = 0; i < face_vertex_indices.size(); ++i) { + EXPECT_EQ(texcoords[i], source_texcoords[face_vertex_indices[i]]); + } +} + +TEST_F(MjcfSdfFileFormatPluginTest, + TestFaceVaryingMeshSourcesObjWithIndexedNormals) { + const std::string xml_path = GetTestDataFilePath(kMeshObjPath); + + auto stage = pxr::UsdStage::Open(xml_path); + EXPECT_THAT(stage, testing::NotNull()); + + auto mesh = pxr::UsdGeomMesh::Get( + stage, SdfPath("/mesh_test/test_body/test_body/mesh/Mesh")); + ASSERT_TRUE(mesh); + pxr::VtArray face_vertex_counts; + mesh.GetFaceVertexCountsAttr().Get(&face_vertex_counts); + EXPECT_EQ(face_vertex_counts.size(), 4); + EXPECT_EQ(face_vertex_counts, pxr::VtArray({3, 3, 3, 3})); + + pxr::VtArray face_vertex_indices; + mesh.GetFaceVertexIndicesAttr().Get(&face_vertex_indices); + EXPECT_EQ(face_vertex_indices.size(), 12); + EXPECT_EQ(face_vertex_indices, + pxr::VtArray({0, 3, 2, 0, 1, 3, 0, 2, 1, 1, 2, 3})); + + pxr::VtArray normals; + mesh.GetNormalsAttr().Get(&normals); + EXPECT_EQ(normals.size(), face_vertex_indices.size()); + // We can't directly check the normals values because they are altered by + // Mujoco's compiling step. + // We also can't access the normals indexing data, and can't use the vertex + // indexing data here because they are separate. + // So we check that the first half of the normals are the same, then the + // second half, as set in the OBJ file. + pxr::GfVec3f first_half_normal = normals[0]; + pxr::GfVec3f second_half_normal = normals[face_vertex_indices.size() / 2]; + EXPECT_NE(first_half_normal, second_half_normal); + int i = 0; + for (; i < face_vertex_indices.size() / 2; ++i) { + EXPECT_EQ(normals[i], first_half_normal); + } + for (; i < face_vertex_indices.size(); ++i) { + EXPECT_EQ(normals[i], second_half_normal); + } + + auto primvars_api = pxr::UsdGeomPrimvarsAPI(mesh.GetPrim()); + + pxr::VtArray texcoords; + EXPECT_TRUE(primvars_api.HasPrimvar(pxr::_tokens->st)); + auto primvar_st = primvars_api.GetPrimvar(pxr::_tokens->st); + primvar_st.Get(&texcoords); + EXPECT_EQ(texcoords.size(), face_vertex_indices.size()); + + // Check the faceVarying texcoords against the manually indexed source + // texcoords. + // NOTE: For OBJ we must use different indices for the texcoords than for the + // vertices! + std::vector source_face_texcoord_indices{0, 1, 2, 1, 2, 3, + 2, 3, 0, 3, 0, 1}; + pxr::VtArray source_texcoords{ + {0.5, 0.5}, {0, 0.5}, {1, 0}, {1, 1}}; + // NOTE: The v component of the texcoords is flipped when Mujoco loads the + // OBJ. + for (auto& uv : source_texcoords) { + uv[1] = 1 - uv[1]; + } + for (int i = 0; i < source_face_texcoord_indices.size(); ++i) { + EXPECT_EQ(texcoords[i], source_texcoords[source_face_texcoord_indices[i]]); + } +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestBody) { + static constexpr char kXml[] = R"( + + + + + + + + + + + + + + + + + + )"; + + pxr::SdfLayerRefPtr layer = LoadLayer(kXml); + + auto stage = pxr::UsdStage::Open(layer); + EXPECT_PRIM_VALID(stage, "/body_test"); + EXPECT_PRIM_VALID(stage, "/body_test/test_body"); + EXPECT_PRIM_VALID(stage, "/body_test/test_body/test_body"); + EXPECT_PRIM_VALID(stage, "/body_test/test_body/test_body_2"); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestBasicParenting) { + static constexpr char kXml[] = R"( + + + + + + + + + + + )"; + + pxr::SdfLayerRefPtr layer = LoadLayer(kXml); + + auto stage = pxr::UsdStage::Open(layer); + EXPECT_PRIM_VALID(stage, "/test/root"); + EXPECT_PRIM_VALID(stage, "/test/root/root"); + EXPECT_PRIM_VALID(stage, "/test/root/root_body_1"); + EXPECT_PRIM_VALID(stage, "/test/root/root_body_2"); + EXPECT_PRIM_VALID(stage, "/test/root/root_body_3"); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestJointsDoNotAffectParenting) { + static constexpr char kXml[] = R"( + + + + + + + + + + + + + + + + + + )"; + + pxr::SdfLayerRefPtr layer = LoadLayer(kXml); + + auto stage = pxr::UsdStage::Open(layer); + EXPECT_PRIM_VALID(stage, "/test/root"); + EXPECT_PRIM_VALID(stage, "/test/root/root"); + EXPECT_PRIM_VALID(stage, "/test/root/middle"); + EXPECT_PRIM_VALID(stage, "/test/root/tet"); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestKindAuthoring) { + static constexpr char kXml[] = R"( + + + + + + + + + + + + + + + + + + )"; + + pxr::SdfLayerRefPtr layer = LoadLayer(kXml); + + auto stage = pxr::UsdStage::Open(layer); + EXPECT_PRIM_KIND(stage, "/test", pxr::KindTokens->group); + EXPECT_PRIM_KIND(stage, "/test/root", pxr::KindTokens->component); + EXPECT_PRIM_KIND(stage, "/test/root/root", pxr::KindTokens->subcomponent); + EXPECT_PRIM_KIND(stage, "/test/root/middle", pxr::KindTokens->subcomponent); + EXPECT_PRIM_KIND(stage, "/test/root/tet", pxr::KindTokens->subcomponent); +} + +} // namespace +} // namespace mujoco diff --git a/test/experimental/usd/plugins/mjcf/testdata/materials.xml b/test/experimental/usd/plugins/mjcf/testdata/materials.xml new file mode 100644 index 00000000..735a789d --- /dev/null +++ b/test/experimental/usd/plugins/mjcf/testdata/materials.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/test/experimental/usd/plugins/mjcf/testdata/mesh_obj.xml b/test/experimental/usd/plugins/mjcf/testdata/mesh_obj.xml new file mode 100644 index 00000000..11465088 --- /dev/null +++ b/test/experimental/usd/plugins/mjcf/testdata/mesh_obj.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/test/experimental/usd/plugins/mjcf/testdata/meshes/tetrahedron.obj b/test/experimental/usd/plugins/mjcf/testdata/meshes/tetrahedron.obj new file mode 100644 index 00000000..98ab2e51 --- /dev/null +++ b/test/experimental/usd/plugins/mjcf/testdata/meshes/tetrahedron.obj @@ -0,0 +1,16 @@ +v 0 1 0 +v 0 0 0 +v 1 0 1 +v 1 0 -1 +vn 1 0 0 +vn -1 0 0 +vt 0.5 0.5 +vt 0 0.5 +vt 1 1 +vt 1 0 + +f 1/1/1 4/2/1 3/3/1 +f 1/2/1 2/3/1 4/4/1 +f 1/3/2 3/4/2 2/1/2 +f 2/4/2 3/1/2 4/2/2 + diff --git a/test/experimental/usd/plugins/mjcf/testdata/textures/cube.png b/test/experimental/usd/plugins/mjcf/testdata/textures/cube.png new file mode 100644 index 0000000000000000000000000000000000000000..8c244eca6405e36009a8cf885df2f6487315bbb2 GIT binary patch literal 6888 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 ZZg$RQK;>7N3*+AbFw{5Id!a+R{y$HZ@S6Yt literal 0 HcmV?d00001 From e8c67ca586e23ac62e8f8ef58e82945036edcbf2 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Wed, 26 Mar 2025 14:43:55 -0700 Subject: [PATCH 011/191] Allow orientation for user composite. PiperOrigin-RevId: 740909723 Change-Id: Id38a55114d4257a461bbced57d35d6bb42a2597d --- doc/XMLreference.rst | 9 +++++++-- doc/XMLschema.rst | 2 ++ doc/changelog.rst | 5 +++++ src/user/user_composite.cc | 12 ++++++++---- src/user/user_composite.h | 3 ++- src/xml/xml_native_reader.cc | 5 +++-- 6 files changed, 27 insertions(+), 9 deletions(-) diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index ca7258f8..5d052472 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -3003,8 +3003,13 @@ cable, which produces an inextensible chain of bodies connected with ball joints .. _body-composite-offset: :at:`offset`: :at-val:`real(3), "0 0 0"` - It specifies a 3D offset from the center of the parent body to the center of the grid of elements. The offset is - expressed in the local coordinate frame of the parent body. + It specifies a 3D offset from the center of the parent body to the center of the first body of the cable. The offset + is expressed in the local coordinate frame of the parent body. + +.. _body-composite-quat: + +:at:`quat`: :at-val:`real(4), "1 0 0 0"` + It specifies a quaternion that rotates the first body frame. The quaternion is expressed in the parent body frame. .. _body-composite-vertex: diff --git a/doc/XMLschema.rst b/doc/XMLschema.rst index 0f71f6a3..6cbc0121 100644 --- a/doc/XMLschema.rst +++ b/doc/XMLschema.rst @@ -344,6 +344,8 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | | | | | :ref:`vertex` | :ref:`initial` | :ref:`curve` | :ref:`size` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +| | | | :ref:`quat` | | | | | +| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_2| composite |br| |_2| |L| | | .. table:: | | :ref:`joint | \* | :class: mjcf-attributes | diff --git a/doc/changelog.rst b/doc/changelog.rst index 05d68306..fd565e76 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -11,6 +11,11 @@ Upcoming version (not yet released) - The default value of the flag for toggling :ref:`internal flex contacts` was changed from "true" to "false". This feature has proven to be counterintuitive for users. +General +^^^^^^^ +- Add :ref:`orientation` parameter to :ref:`composite`. Moreover, allow the + composite to be the direct child of a frame. + Bug fixes ^^^^^^^^^ - :ref:`mj_jacDot` was missing a term that accounts for the motion of the point with respect to diff --git a/src/user/user_composite.cc b/src/user/user_composite.cc index 98c3ed41..6cd21b51 100644 --- a/src/user/user_composite.cc +++ b/src/user/user_composite.cc @@ -52,6 +52,7 @@ mjCComposite::mjCComposite(void) { type = mjCOMPTYPE_PARTICLE; count[0] = count[1] = count[2] = 1; mjuu_setvec(offset, 0, 0, 0); + mjuu_setvec(quat, 1, 0, 0, 0); frame = nullptr; // plugin variables @@ -260,19 +261,20 @@ bool mjCComposite::MakeCable(mjCModel* model, mjsBody* body, char* error, int er // populate uservert if not specified if (uservert.empty()) { for (int ix=0; ix < count[0]; ix++) { + double v[3]; for (int k=0; k < 3; k++) { switch (curve[k]) { case mjCOMPSHAPE_LINE: - uservert.push_back(ix*size[0]/(count[0]-1)); + v[k] = ix*size[0]/(count[0]-1); break; case mjCOMPSHAPE_COS: - uservert.push_back(size[1]*cos(mjPI*ix*size[2]/(count[0]-1))); + v[k] = size[1]*cos(mjPI*ix*size[2]/(count[0]-1)); break; case mjCOMPSHAPE_SIN: - uservert.push_back(size[1]*sin(mjPI*ix*size[2]/(count[0]-1))); + v[k] = size[1]*sin(mjPI*ix*size[2]/(count[0]-1)); break; case mjCOMPSHAPE_ZERO: - uservert.push_back(0); + v[k] = 0; break; default: // SHOULD NOT OCCUR @@ -280,6 +282,8 @@ bool mjCComposite::MakeCable(mjCModel* model, mjsBody* body, char* error, int er break; } } + mjuu_rotVecQuat(v, v, quat); + uservert.insert(uservert.end(), v, v+3); } } diff --git a/src/user/user_composite.h b/src/user/user_composite.h index 0f826760..17378004 100644 --- a/src/user/user_composite.h +++ b/src/user/user_composite.h @@ -73,7 +73,8 @@ class mjCComposite { std::string prefix; // name prefix mjtCompType type; // composite type int count[3]; // geom count in each dimension - double offset[3]; // position offset for particle and grid + double offset[3]; // position offset + double quat[4]; // quaternion offset // currently used only for cable std::string initial; // root boundary type diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 44efc4ec..272b5a2c 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -287,8 +287,8 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { {"<"}, {"config", "*", "2", "key", "value"}, {">"}, - {"composite", "*", "8", "prefix", "type", "count", "offset", - "vertex", "initial", "curve", "size"}, + {"composite", "*", "9", "prefix", "type", "count", "offset", + "vertex", "initial", "curve", "size", "quat"}, {"<"}, {"joint", "*", "17", "kind", "group", "stiffness", "damping", "armature", "solreffix", "solimpfix", "type", "axis", @@ -2402,6 +2402,7 @@ void mjXReader::OneComposite(XMLElement* elem, mjsBody* body, mjsFrame* frame, c } ReadAttr(elem, "count", 3, comp.count, text, false, false); ReadAttr(elem, "offset", 3, comp.offset, text); + ReadAttr(elem, "quat", 4, comp.quat, text); comp.frame = frame; // plugin From 0abf184d49b7e3c45fb403a2dc0f6b38cd388090 Mon Sep 17 00:00:00 2001 From: Taylor Howell Date: Thu, 27 Mar 2025 11:04:40 -0700 Subject: [PATCH 012/191] Align comment in mjmodel.h. PiperOrigin-RevId: 741214316 Change-Id: I449c4a6e1c70b30d60636e11f26fb0bc8653f2a5 --- doc/includes/references.h | 2 +- include/mujoco/mjmodel.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/includes/references.h b/doc/includes/references.h index 6b38571b..355b5183 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -910,7 +910,7 @@ struct mjModel_ { int ncam; // number of cameras int nlight; // number of lights int nflex; // number of flexes - int nflexnode; // number of dofs in all flexes + int nflexnode; // number of dofs in all flexes int nflexvert; // number of vertices in all flexes int nflexedge; // number of edges in all flexes int nflexelem; // number of elements in all flexes diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index 9f76205f..185d31de 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -611,7 +611,7 @@ struct mjModel_ { int ncam; // number of cameras int nlight; // number of lights int nflex; // number of flexes - int nflexnode; // number of dofs in all flexes + int nflexnode; // number of dofs in all flexes int nflexvert; // number of vertices in all flexes int nflexedge; // number of edges in all flexes int nflexelem; // number of elements in all flexes From 97092facd639e501f4e3821beb8842aa61dc4dda Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 27 Mar 2025 19:35:36 -0400 Subject: [PATCH 013/191] resolve PR comments --- python/mujoco/simulate.cc | 19 +++++----- python/mujoco/viewer.py | 75 ++++++++++++++++++++++++++++----------- simulate/simulate.h | 2 +- 3 files changed, 67 insertions(+), 29 deletions(-) diff --git a/python/mujoco/simulate.cc b/python/mujoco/simulate.cc index 868bec28..326879cc 100644 --- a/python/mujoco/simulate.cc +++ b/python/mujoco/simulate.cc @@ -149,7 +149,7 @@ class SimulateWrapper { void ClearFigures() { simulate_->user_figures_.clear(); } - void SetOverlayText( + void SetText( const std::vector>& overlay_texts) { // Collection of [font, gridpos, text1, text2] tuples for overlay text std::vector> user_overlay_text; @@ -161,21 +161,24 @@ class SimulateWrapper { simulate_->user_text_ = user_overlay_text; } - void ClearOverlayText() { simulate_->user_text_.clear(); } + void ClearText() { simulate_->user_text_.clear(); } void SetImages( - const std::vector>>& viewport_images + const std::vector> viewports_images ) { // Clear previous images to prevent memory leaks ClearImages(); - for (const auto& [viewport, image] : viewport_images) { + for (const auto& [viewport, image] : viewports_images) { auto buf = image.request(); + if (buf.ndim != 3) { + throw std::invalid_argument("image must have 3 dimensions (H, W, C)"); + } if (static_cast(buf.shape[2]) != 3) { throw std::invalid_argument("image must have 3 channels"); } - if (buf.ndim != 3) { - throw std::invalid_argument("image must have 3 dimensions (H, W, C)"); + if (buf.itemsize != sizeof(unsigned char)) { + throw std::invalid_argument("image must be uint8 format"); } // Calculate size of the image data @@ -300,9 +303,9 @@ PYBIND11_MODULE(_simulate, pymodule) { .def("set_figures", &SimulateWrapper::SetFigures, py::arg("viewports_figures")) .def("clear_figures", &SimulateWrapper::ClearFigures) - .def("overlay_text", &SimulateWrapper::SetOverlayText, + .def("set_text", &SimulateWrapper::SetText, py::arg("overlay_texts")) - .def("clear_overlay_text", &SimulateWrapper::ClearOverlayText) + .def("clear_text", &SimulateWrapper::ClearText) .def("set_images", &SimulateWrapper::SetImages, py::arg("viewports_images")) .def("clear_images", &SimulateWrapper::ClearImages) diff --git a/python/mujoco/viewer.py b/python/mujoco/viewer.py index 21a1eeea..527fda50 100644 --- a/python/mujoco/viewer.py +++ b/python/mujoco/viewer.py @@ -117,10 +117,21 @@ class Handle: return None def set_figures( - self, viewports_figures: List[Tuple[mujoco.MjrRect, mujoco.MjvFigure]] + self, viewports_figures: Union[Tuple[mujoco.MjrRect, mujoco.MjvFigure], + List[Tuple[mujoco.MjrRect, mujoco.MjvFigure]]] ): + """Overlay figures on the viewer. + + Args: + viewports_figures: Single tuple or list of tuples of (viewport, figure) + viewport: Rectangle defining position and size of the figure + figure: MjvFigure object containing the figure data to display + """ sim = self._sim() if sim is not None: + # Convert single tuple to list if needed + if isinstance(viewports_figures, tuple): + viewports_figures = [viewports_figures] sim.set_figures(viewports_figures) def clear_figures(self): @@ -128,43 +139,67 @@ class Handle: if sim is not None: sim.clear_figures() - def overlay_text(self, overlay_texts: List[Tuple[int, int, str, str]]): + def set_text(self, overlay_texts: Union[Tuple[Optional[int], Optional[int], Optional[str], Optional[str]], + List[Tuple[Optional[int], Optional[int], Optional[str], Optional[str]]]]): """Overlay text on the viewer. Args: - overlay_texts: List of tuples of (font, gridpos, text1, text2) - let: + overlay_texts: Single tuple or list of tuples of (font, gridpos, text1, text2) font: Font style from mujoco.mjtFontScale gridpos: Position of text box from mujoco.mjtGridPos - text1: Left text column - text2: Right text column + text1: Left text column, defaults to empty string if None + text2: Right text column, defaults to empty string if None """ sim = self._sim() if sim is not None: - sim.overlay_text(overlay_texts) + # Convert single tuple to list if needed + if isinstance(overlay_texts, tuple): + overlay_texts = [overlay_texts] + + # Convert None values to empty strings + default_font = mujoco.mjtFontScale.mjFONTSCALE_150 + default_gridpos = mujoco.mjtGridPos.mjGRID_TOPLEFT + processed_texts = [( + default_font if font is None else font, + default_gridpos if gridpos is None else gridpos, + "" if text1 is None else text1, + "" if text2 is None else text2) + for font, gridpos, text1, text2 in overlay_texts] + + sim.set_text(processed_texts) - def clear_overlay_text(self): + def clear_text(self): sim = self._sim() if sim is not None: - sim.clear_overlay_text() + sim.clear_text() def set_images( - self, viewports_images: List[Tuple[mujoco.MjrRect, np.ndarray]] + self, viewports_images: Union[Tuple[mujoco.MjrRect, np.ndarray], + List[Tuple[mujoco.MjrRect, np.ndarray]]] ): + """Overlay images on the viewer. + + Args: + viewports_images: Single tuple or list of tuples of (viewport, image) + viewport: Rectangle defining position and size of the image + image: RGB image with shape (height, width, 3) + """ sim = self._sim() if sim is not None: - # Nearest neighbor resize - resize = lambda a, s: a[(np.arange(s[0]) * a.shape[0]) // s[0]][ - :, (np.arange(s[1]) * a.shape[1]) // s[1] - ] - resized_viewports_images = [] + # Convert single tuple to list if needed + if isinstance(viewports_images, tuple): + viewports_images = [viewports_images] + + processed_images = [] for viewport, image in viewports_images: targ_shape = (viewport.height, viewport.width) - resized = resize(image, targ_shape) - resized = np.flip(resized, axis=0) - resized = np.ascontiguousarray(resized) - resized_viewports_images.append((viewport, resized)) - sim.set_images(resized_viewports_images) + # Check if image is already the correct shape + if image.shape[:2] != targ_shape: + raise ValueError(f"Image shape {image.shape[:2]} does not match target shape {targ_shape}") + flipped = np.flip(image, axis=0) + contiguous = np.ascontiguousarray(flipped) + processed_images.append((viewport, contiguous)) + sim.set_images(processed_images) def clear_images(self): sim = self._sim() diff --git a/simulate/simulate.h b/simulate/simulate.h index 0bf6ad25..00dc5fe5 100644 --- a/simulate/simulate.h +++ b/simulate/simulate.h @@ -249,7 +249,7 @@ class Simulate { mjvFigure figsize = {}; mjvFigure figsensor = {}; - // additional user-defined visualization geoms (used in passive mode) + // additional user-defined visualization mjvScene* user_scn = nullptr; mjtByte user_scn_flags_prev_[mjNRNDFLAG]; std::vector> user_figures_; From e272e467708e33f08278766eb7f12d01e1a98fc6 Mon Sep 17 00:00:00 2001 From: Kevin Zakka Date: Fri, 28 Mar 2025 00:37:19 -0700 Subject: [PATCH 014/191] Rename simulate "copy pose" button to "copy state", also save "qvel', `ctrl`, `mpos` and `mquat` if present. SHIFT-click will save values at full precision. Example formatting: ``` # default precision # full precision ``` PiperOrigin-RevId: 741427042 Change-Id: Ib33f3e6215b7933d9267fc4c222d7a0abc75dbb4 --- simulate/simulate.cc | 83 +++++++++++++++++++++++++++++++++++++------- simulate/simulate.h | 5 +-- 2 files changed, 74 insertions(+), 14 deletions(-) diff --git a/simulate/simulate.cc b/simulate/simulate.cc index bf4b4a13..2f5ee146 100644 --- a/simulate/simulate.cc +++ b/simulate/simulate.cc @@ -1141,17 +1141,74 @@ void AlignAndScaleView(mj::Simulate* sim, const mjModel* m) { } -// copy qpos to clipboard as key -void CopyPose(mj::Simulate* sim, const mjModel* m, const mjData* d) { - char clipboard[5000] = ""); + + // qvel + mju::strcat_arr(clipboard, "\"\n qvel=\""); + for (int i = 0; i < m->nv; i++) { + mju::sprintf_arr(buf, format, d->qvel[i]); + if (i < m->nv-1) mju::strcat_arr(buf, " "); + mju::strcat_arr(clipboard, buf); + } + + // act + if (m->na > 0) { + mju::strcat_arr(clipboard, "\"\n act=\""); + for (int i = 0; i < m->na; i++) { + mju::sprintf_arr(buf, format, d->act[i]); + if (i < m->na-1) mju::strcat_arr(buf, " "); + mju::strcat_arr(clipboard, buf); + } + } + + // ctrl + if (m->nu > 0) { + mju::strcat_arr(clipboard, "\"\n ctrl=\""); + for (int i = 0; i < m->nu; i++) { + mju::sprintf_arr(buf, format, d->ctrl[i]); + if (i < m->nu-1) mju::strcat_arr(buf, " "); + mju::strcat_arr(clipboard, buf); + } + } + + if (m->nmocap > 0) { + // mocap_pos + mju::strcat_arr(clipboard, "\"\n mpos=\""); + for (int i = 0; i < 3*m->nmocap; i++) { + mju::sprintf_arr(buf, format, d->mocap_pos[i]); + if (i < 3*m->nmocap-1) mju::strcat_arr(buf, " "); + mju::strcat_arr(clipboard, buf); + } + + // mocap_quat + mju::strcat_arr(clipboard, "\"\n mquat=\""); + for (int i = 0; i < 4*m->nmocap; i++) { + mju::sprintf_arr(buf, format, d->mocap_quat[i]); + if (i < 4*m->nmocap-1) mju::strcat_arr(buf, " "); + mju::strcat_arr(clipboard, buf); + } + } + + mju::strcat_arr(clipboard, "\"\n/>"); // copy to clipboard sim->platform_ui->SetClipboardString(clipboard); @@ -1412,8 +1469,9 @@ void UiEvent(mjuiState* state) { sim->pending_.align = true; break; - case 4: // Copy pose - sim->pending_.copy_pose = true; + case 4: // Copy key + sim->pending_.copy_key = true; + sim->pending_.copy_key_full_precision = sim->platform_ui->IsShiftKeyPressed(); break; case 5: // Adjust key @@ -1967,9 +2025,10 @@ void Simulate::Sync() { pending_.align = false; } - if (pending_.copy_pose) { - CopyPose(this, m_, d_); - pending_.copy_pose = false; + if (pending_.copy_key) { + CopyKey(this, m_, d_, pending_.copy_key_full_precision); + pending_.copy_key = false; + pending_.copy_key_full_precision = false; } if (pending_.load_from_history) { diff --git a/simulate/simulate.h b/simulate/simulate.h index cd654192..23bdb604 100644 --- a/simulate/simulate.h +++ b/simulate/simulate.h @@ -144,7 +144,8 @@ class Simulate { std::optional print_data; bool reset; bool align; - bool copy_pose; + bool copy_key; + bool copy_key_full_precision; bool load_from_history; bool load_key; bool save_key; @@ -293,7 +294,7 @@ class Simulate { {mjITEM_BUTTON, "Reset", 2, nullptr, " #259"}, {mjITEM_BUTTON, "Reload", 5, nullptr, "CL"}, {mjITEM_BUTTON, "Align", 2, nullptr, "CA"}, - {mjITEM_BUTTON, "Copy pose", 2, nullptr, "CC"}, + {mjITEM_BUTTON, "Copy state", 2, nullptr, "CC"}, {mjITEM_SLIDERINT, "Key", 3, &this->key, "0 0"}, {mjITEM_BUTTON, "Load key", 3}, {mjITEM_BUTTON, "Save key", 3}, From 6e19035aabf75c6f1de5101cd437d56231d70452 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Fri, 28 Mar 2025 01:57:56 -0700 Subject: [PATCH 015/191] Add delete and detach docs. PiperOrigin-RevId: 741444657 Change-Id: I4207eaf74524e8a3ca46130967c6fb0dd62b4d64 --- doc/python.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/python.rst b/doc/python.rst index edb2067b..62541701 100644 --- a/doc/python.rst +++ b/doc/python.rst @@ -601,6 +601,14 @@ Lists of all elements in a spec can be accessed using named properties, using th ``equalities``, ``tendons``, ``actuators``, ``skins``, ``textures``, ``texts``, ``tuples``, ``flexes``, ``hfields``, ``keys``, ``numerics``, ``excludes``, ``sensors``, ``plugins``. +Element removal +^^^^^^^^^^^^^^^ +For elements that can have children (bodies and defaults), the methods ``spec.detach_body(body)`` and +``spec.detach_default(def)`` remove, respectively, ``body`` and ``def`` from the spec, together with all of their +children. When detaching body subtrees, all elements which reference elements in the subtree, will also be removed. For +all other elements, the method ``delete()`` removes the corresponding element from the spec, e.g. +``spec.geom('my_geom').delete()`` will remove the geom named "my_geom" and all of the elements that reference it. + Tree traversal ^^^^^^^^^^^^^^ Traversal of the kinematic tree is aided by the following methods which return tree-related lists of elements: From 01d4c4675369359f672ba4ed98929e773c474840 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Fri, 28 Mar 2025 06:26:30 -0700 Subject: [PATCH 016/191] Store the `mjsCompiler` -> appended `mjSpec` map when appending an `mjSpec`. Previously, we stored the source `mjSpec` during a copy as a hack for having access to the compiler options, but this is not robust since we cannot guarantee that 1) the source `mjSpec` is not destroyed before we need to look up the compiler options nor 2) that the `mjSpec` was appended without a copy. While 2) could be solved by simply handling an additional case in `mjCModel::FindSpec`, using a map also solves 1) and it is easier to understand. PiperOrigin-RevId: 741504264 Change-Id: Iab1bfd9e61299a94fa8caf3a244c067b09d54384 --- src/user/user_model.cc | 29 +++++++++++------------ src/user/user_model.h | 13 ++++------- src/user/user_objects.cc | 4 ++-- test/user/user_api_test.cc | 48 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 25 deletions(-) diff --git a/src/user/user_model.cc b/src/user/user_model.cc index a594538a..a63f13c9 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -212,9 +212,6 @@ mjCModel::mjCModel() { // create mjCBase lists from children lists CreateObjectLists(); - // the source spec is the model itself, overwritten in the copy constructor - source_spec_ = &spec; - // set the signature spec.element->signature = 0; } @@ -223,7 +220,6 @@ mjCModel::mjCModel() { mjCModel::mjCModel(const mjCModel& other) { CreateObjectLists(); - source_spec_ = (mjSpec*)&other.spec; *this = other; } @@ -239,6 +235,7 @@ mjCModel& mjCModel::operator=(const mjCModel& other) { // copy attached specs first so that we can resolve references to them for (const auto* s : other.specs_) { specs_.push_back(mj_copySpec(s)); + compiler2spec_[&s->compiler] = specs_.back(); } // the world copy constructor takes care of copying the tree @@ -1165,9 +1162,13 @@ mjCPlugin* mjCModel::AddPlugin() { // append spec to spec -void mjCModel::AppendSpec(mjSpec* spec) { +void mjCModel::AppendSpec(mjSpec* spec, const mjsCompiler* compiler_) { // TODO: check if the spec is already in the list specs_.push_back(spec); + + if (compiler_) { + compiler2spec_[compiler_] = spec; + } } @@ -1461,10 +1462,15 @@ mjSpec* mjCModel::FindSpec(std::string name) const { // find spec by mjsCompiler pointer -mjSpec* mjCModel::FindSpec(const mjsCompiler* compiler_) const { - if (&GetSourceSpec()->compiler == compiler_) { - return (mjSpec*)&spec; +mjSpec* mjCModel::FindSpec(const mjsCompiler* compiler_) { + if (compiler_ == &spec.compiler) { + return &spec; } + + if (compiler2spec_.find(compiler_) != compiler2spec_.end()) { + return compiler2spec_[compiler_]; + } + for (auto s : specs_) { mjSpec* source = static_cast(s->element)->FindSpec(compiler_); if (source) { @@ -1476,13 +1482,6 @@ mjSpec* mjCModel::FindSpec(const mjsCompiler* compiler_) const { -// get the spec from which this model was created -mjSpec* mjCModel::GetSourceSpec() const { - return source_spec_; -} - - - //------------------------------- COMPILER PHASES -------------------------------------------------- // make lists of objects in tree: bodies, geoms, joints, sites, cameras, lights diff --git a/src/user/user_model.h b/src/user/user_model.h index 81b94752..c55384bf 100644 --- a/src/user/user_model.h +++ b/src/user/user_model.h @@ -215,7 +215,9 @@ class mjCModel : public mjCModel_, private mjSpec { mjCTuple* AddTuple(); mjCKey* AddKey(); mjCPlugin* AddPlugin(); - void AppendSpec(mjSpec* spec); + + // append spec to this model, optionally map compiler options to the appended spec + void AppendSpec(mjSpec* spec, const mjsCompiler* compiler = nullptr); // delete elements marked as discard=true template void Delete(std::vector& elements, @@ -248,7 +250,7 @@ class mjCModel : public mjCModel_, private mjSpec { mjCBase* FindObject(mjtObj type, std::string name) const; // find object given type and name mjCBase* FindTree(mjCBody* body, mjtObj type, std::string name); // find tree object given name mjSpec* FindSpec(std::string name) const; // find spec given name - mjSpec* FindSpec(const mjsCompiler* compiler_) const; // find spec given mjsCompiler + mjSpec* FindSpec(const mjsCompiler* compiler_); // find spec given mjsCompiler void ActivatePlugin(const mjpPlugin* plugin, int slot); // activate plugin // accessors @@ -316,9 +318,6 @@ class mjCModel : public mjCModel_, private mjSpec { // map from default class name to default class pointer std::unordered_map def_map; - // get the spec from which this model was created - mjSpec* GetSourceSpec() const; - // set deepcopy flag void SetDeepCopy(bool deepcopy) { deepcopy_ = deepcopy; } @@ -332,9 +331,6 @@ class mjCModel : public mjCModel_, private mjSpec { // settings for each defaults class std::vector defaults_; - // spec from which this model was created in copy constructor - mjSpec* source_spec_; - // list of active plugins std::vector> active_plugins_; @@ -453,5 +449,6 @@ class mjCModel : public mjCModel_, private mjSpec { bool deepcopy_; // copy objects when attaching bool attached_ = false; // true if model is attached to a parent model int uid_count_ = 0; // unique id count for all objects + std::unordered_map compiler2spec_; // map from compiler to spec }; #endif // MUJOCO_SRC_USER_USER_MODEL_H_ diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 5154af48..1e9cf407 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -904,7 +904,7 @@ mjCBody& mjCBody::operator+=(const mjCBody& other) { mjCBody& mjCBody::operator+=(const mjCFrame& other) { // append a copy of the attached spec if (other.model != model && !model->FindSpec(mjs_getString(other.model->spec.modelname))) { - model->AppendSpec(mj_copySpec(&other.model->spec)); + model->AppendSpec(mj_copySpec(&other.model->spec), &other.model->spec.compiler); } // create a copy of the subtree that contains the frame @@ -2038,7 +2038,7 @@ mjCFrame& mjCFrame::operator=(const mjCFrame& other) { mjCFrame& mjCFrame::operator+=(const mjCBody& other) { // append a copy of the attached spec if (other.model != model && !model->FindSpec(mjs_getString(other.model->spec.modelname))) { - model->AppendSpec(mj_copySpec(&other.model->spec)); + model->AppendSpec(mj_copySpec(&other.model->spec), &other.model->spec.compiler); } // apply namespace and store keyframes in the source model diff --git a/test/user/user_api_test.cc b/test/user/user_api_test.cc index 84febf8e..3837c423 100644 --- a/test/user/user_api_test.cc +++ b/test/user/user_api_test.cc @@ -2514,6 +2514,54 @@ TEST_F(MujocoTest, DifferentUnitsAllowed) { mj_deleteModel(copied_model); } +TEST_F(MujocoTest, DifferentOptionsInAttachedFrame) { + static constexpr char xml_parent[] = R"( + + + + )"; + + static constexpr char xml_child[] = R"( + + + + + + + + + )"; + + // load specs and compile child + mjSpec* parent = mj_parseXMLString(xml_parent, 0, nullptr, 0); + EXPECT_THAT(parent, NotNull()); + mjSpec* child = mj_parseXMLString(xml_child, 0, nullptr, 0); + EXPECT_THAT(child, NotNull()); + mjModel* m_child = mj_compile(child, 0); + EXPECT_THAT(m_child, NotNull()); + + // attach child frame to parent worldbody + mjsBody* world = mjs_findBody(parent, "world"); + EXPECT_THAT(world, NotNull()); + mjsFrame* child_frame = mjs_findFrame(child, "child"); + EXPECT_THAT(child_frame, NotNull()); + mjsFrame* attached_frame = mjs_attachFrame(world, child_frame, "child-", ""); + EXPECT_THAT(attached_frame, NotNull()); + + // wrap the child frame in the parent frame and compile + mjModel* m_attached = mj_compile(parent, 0); + EXPECT_THAT(m_attached, NotNull()); + EXPECT_NEAR(m_attached->site_quat[0], m_child->site_quat[0], 1e-6); + EXPECT_NEAR(m_attached->site_quat[1], m_child->site_quat[1], 1e-6); + EXPECT_NEAR(m_attached->site_quat[2], m_child->site_quat[2], 1e-6); + EXPECT_NEAR(m_attached->site_quat[3], m_child->site_quat[3], 1e-6); + + mj_deleteSpec(parent); + mj_deleteSpec(child); + mj_deleteModel(m_child); + mj_deleteModel(m_attached); +} + TEST_F(MujocoTest, CopyAttachedSpec) { static constexpr char xml_parent[] = R"( From 1f590c12750fa8ec8b61e1b052b6e809319535c9 Mon Sep 17 00:00:00 2001 From: Maxime <84160692+maxzand@users.noreply.github.com> Date: Fri, 28 Mar 2025 19:50:52 -0400 Subject: [PATCH 017/191] Fixed contact force legend and added legends for accel and velocity --- python/tutorial.ipynb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/tutorial.ipynb b/python/tutorial.ipynb index 0c7be7be..c0933859 100644 --- a/python/tutorial.ipynb +++ b/python/tutorial.ipynb @@ -1360,16 +1360,18 @@ "lines = ax[0,0].plot(sim_time, force)\n", "ax[0,0].set_title('contact force')\n", "ax[0,0].set_ylabel('Newton')\n", - "ax[0,0].legend(iter(lines), ('normal z', 'friction x', 'friction y'));\n", + "ax[0,0].legend(lines, ('normal z', 'friction x', 'friction y'));\n", "\n", "ax[1,0].plot(sim_time, acceleration)\n", "ax[1,0].set_title('acceleration')\n", "ax[1,0].set_ylabel('(meter,radian)/s/s')\n", + "ax[1,0].legend(['ax', 'ay', 'az', 'αx', 'αy', 'αz'])\n", "\n", "ax[2,0].plot(sim_time, velocity)\n", "ax[2,0].set_title('velocity')\n", "ax[2,0].set_ylabel('(meter,radian)/s')\n", "ax[2,0].set_xlabel('second')\n", + "ax[2,0].legend(['vx', 'vy', 'vz', 'ωx', 'ωy', 'ωz'])\n", "\n", "ax[0,1].plot(sim_time, ncon)\n", "ax[0,1].set_title('number of contacts')\n", From 65500cfbdc15244d0cb6e80d831fa6f31eea29bd Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 30 Mar 2025 22:19:55 -0400 Subject: [PATCH 018/191] improve variable and function names --- python/mujoco/simulate.cc | 20 ++++++++++---------- python/mujoco/viewer.py | 16 ++++++++-------- simulate/simulate.cc | 2 +- simulate/simulate.h | 2 +- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/python/mujoco/simulate.cc b/python/mujoco/simulate.cc index 326879cc..9c1e7bec 100644 --- a/python/mujoco/simulate.cc +++ b/python/mujoco/simulate.cc @@ -149,19 +149,19 @@ class SimulateWrapper { void ClearFigures() { simulate_->user_figures_.clear(); } - void SetText( - const std::vector>& overlay_texts) { + void SetTexts( + const std::vector>& texts) { // Collection of [font, gridpos, text1, text2] tuples for overlay text - std::vector> user_overlay_text; - for (const auto& [font, gridpos, text1, text2] : overlay_texts) { - user_overlay_text.push_back(std::make_tuple(font, gridpos, text1, text2)); + std::vector> user_texts; + for (const auto& [font, gridpos, text1, text2] : texts) { + user_texts.push_back(std::make_tuple(font, gridpos, text1, text2)); } - // Set them all at once to prevent overlay text flickering. - simulate_->user_text_ = user_overlay_text; + // Set them all at once to prevent text flickering. + simulate_->user_texts_ = user_texts; } - void ClearText() { simulate_->user_text_.clear(); } + void ClearTexts() { simulate_->user_texts_.clear(); } void SetImages( const std::vector> viewports_images @@ -303,9 +303,9 @@ PYBIND11_MODULE(_simulate, pymodule) { .def("set_figures", &SimulateWrapper::SetFigures, py::arg("viewports_figures")) .def("clear_figures", &SimulateWrapper::ClearFigures) - .def("set_text", &SimulateWrapper::SetText, + .def("set_texts", &SimulateWrapper::SetTexts, py::arg("overlay_texts")) - .def("clear_text", &SimulateWrapper::ClearText) + .def("clear_texts", &SimulateWrapper::ClearTexts) .def("set_images", &SimulateWrapper::SetImages, py::arg("viewports_images")) .def("clear_images", &SimulateWrapper::ClearImages) diff --git a/python/mujoco/viewer.py b/python/mujoco/viewer.py index 527fda50..e78e1229 100644 --- a/python/mujoco/viewer.py +++ b/python/mujoco/viewer.py @@ -139,12 +139,12 @@ class Handle: if sim is not None: sim.clear_figures() - def set_text(self, overlay_texts: Union[Tuple[Optional[int], Optional[int], Optional[str], Optional[str]], + def set_texts(self, texts: Union[Tuple[Optional[int], Optional[int], Optional[str], Optional[str]], List[Tuple[Optional[int], Optional[int], Optional[str], Optional[str]]]]): """Overlay text on the viewer. Args: - overlay_texts: Single tuple or list of tuples of (font, gridpos, text1, text2) + texts: Single tuple or list of tuples of (font, gridpos, text1, text2) font: Font style from mujoco.mjtFontScale gridpos: Position of text box from mujoco.mjtGridPos text1: Left text column, defaults to empty string if None @@ -153,8 +153,8 @@ class Handle: sim = self._sim() if sim is not None: # Convert single tuple to list if needed - if isinstance(overlay_texts, tuple): - overlay_texts = [overlay_texts] + if isinstance(texts, tuple): + texts = [texts] # Convert None values to empty strings default_font = mujoco.mjtFontScale.mjFONTSCALE_150 @@ -164,14 +164,14 @@ class Handle: default_gridpos if gridpos is None else gridpos, "" if text1 is None else text1, "" if text2 is None else text2) - for font, gridpos, text1, text2 in overlay_texts] + for font, gridpos, text1, text2 in texts] - sim.set_text(processed_texts) + sim.set_texts(processed_texts) - def clear_text(self): + def clear_texts(self): sim = self._sim() if sim is not None: - sim.clear_text() + sim.clear_texts() def set_images( self, viewports_images: Union[Tuple[mujoco.MjrRect, np.ndarray], diff --git a/simulate/simulate.cc b/simulate/simulate.cc index 57e3eaaa..87023d50 100644 --- a/simulate/simulate.cc +++ b/simulate/simulate.cc @@ -2606,7 +2606,7 @@ void Simulate::Render() { } // overlay text - for (auto& [font, gridpos, text1, text2] : this->user_text_) { + for (auto& [font, gridpos, text1, text2] : this->user_texts_) { ShowOverlayText(this, rect, font, gridpos, text1, text2); } diff --git a/simulate/simulate.h b/simulate/simulate.h index 00dc5fe5..a8c9da58 100644 --- a/simulate/simulate.h +++ b/simulate/simulate.h @@ -253,7 +253,7 @@ class Simulate { mjvScene* user_scn = nullptr; mjtByte user_scn_flags_prev_[mjNRNDFLAG]; std::vector> user_figures_; - std::vector> user_text_; + std::vector> user_texts_; std::vector> user_images_; // OpenGL rendering and UI From 8bcfe07e14c78f3ac95f31208dff31c89d747f4a Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 31 Mar 2025 06:40:20 -0700 Subject: [PATCH 019/191] Fix potential division by zero in mj_tendonDot. PiperOrigin-RevId: 742246626 Change-Id: I1f897bd0ca28e0efdffc5a93fadca35221eaeb38 --- src/engine/engine_core_smooth.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/engine/engine_core_smooth.c b/src/engine/engine_core_smooth.c index 009860eb..22870d7b 100644 --- a/src/engine/engine_core_smooth.c +++ b/src/engine/engine_core_smooth.c @@ -938,15 +938,14 @@ void mj_tendonDot(const mjModel* m, mjData* d, int id, mjtNum* Jdot) { // dpnt = 3D position difference, normalize mjtNum dpnt[3]; mju_sub3(dpnt, wpnt+3, wpnt); - mjtNum norm = mju_norm3(dpnt); - mju_scl3(dpnt, dpnt, 1/norm); + mjtNum norm = mju_normalize3(dpnt); // dvel = d / dt (dpnt) mjtNum dvel[3]; mju_sub3(dvel, wvel+3, wvel); mjtNum dot = mju_dot3(dpnt, dvel); mju_addToScl3(dvel, dpnt, -dot); - mju_scl3(dvel, dvel, 1/norm); + mju_scl3(dvel, dvel, norm > mjMINVAL ? 1/norm : 0); // TODO(tassa ) write sparse branch, requires mj_jacDotSparse // if (mj_isSparse(m)) { ... } From 400a379b702fbf1d2224be20cebf071c6645133f Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 31 Mar 2025 11:04:11 -0700 Subject: [PATCH 020/191] Update `doc` requirements. PiperOrigin-RevId: 742324590 Change-Id: I55d6535190a2281aec04ff16bd3f82f6c85870f9 --- doc/requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index f5bddde1..aa0c90a4 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,6 +1,6 @@ Sphinx==5.3.0 furo==2022.9.29 -sphinxcontrib-bibtex==2.6.1 +sphinxcontrib-bibtex==2.6.3 sphinxcontrib-katex==0.9.4 sphinxcontrib-youtube==1.2.0 sphinx-copybutton==0.5.2 @@ -10,7 +10,7 @@ sphinx-toolbox==3.8.2 nbsphinx==0.9.1 pandoc==1.1.0 pygments==2.15.0 -jq==1.4.1 +jq==1.8.0 Jinja2~=3.0 wheel # see https://github.com/aws/aws-sam-cli/issues/3661 regarding markupsafe From 05f3e914b0a02a1dc07f33f29d0c63abb414031b Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 1 Apr 2025 01:16:06 -0700 Subject: [PATCH 021/191] Improve armature documentation PiperOrigin-RevId: 742583729 Change-Id: I18d1c95a91f4d70b842c680d4a0f4d0b04476894 --- doc/XMLreference.rst | 20 +++++++++++++++--- doc/images/XMLreference/armature.gif | Bin 0 -> 137047 bytes doc/images/XMLreference/armature_dark.gif | Bin 0 -> 149304 bytes test/engine/testdata/armature_equivalence.xml | 15 ++++++++++--- 4 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 doc/images/XMLreference/armature.gif create mode 100644 doc/images/XMLreference/armature_dark.gif diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 5d052472..1c1854bb 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -2151,14 +2151,28 @@ rotations as unit quaternions. corresponding to mjModel.qpos_spring is also used to compute the spring reference lengths of all tendons, stored in mjModel.tendon_lengthspring. This is because :ref:`tendons ` can also have springs. +.. image:: images/XMLreference/armature.gif + :width: 40% + :align: right + :class: only-light + :target: https://github.com/google-deepmind/mujoco/blob/main/test/engine/testdata/armature_equivalence.xml +.. image:: images/XMLreference/armature_dark.gif + :width: 40% + :align: right + :class: only-dark + :target: https://github.com/google-deepmind/mujoco/blob/main/test/engine/testdata/armature_equivalence.xml + .. _body-joint-armature: :at:`armature`: :at-val:`real, "0"` Additional inertia associated with movement of the joint that is not due to body mass. This added inertia is usually due to a rotor (a.k.a `armature `__) spinning faster than the - joint itself due to a geared transmission; in this case the added inertia is known as "reflected inertia" and its - value is the rotational inertia of the spinning element multiplied by the square of the gear ratio. The value applies - to all degrees of freedom created by this joint. + joint itself due to a geared transmission. In the illustration, we compare (*left*) a 2-dof system with an armature + body (purple box), coupled with a gear ratio of :math:`3` to the pendulum using a :ref:`joint + equality` constraint, and (*right*) a simple 1-dof pendulum with an equivalent :at:`armature`. + Because the gear ratio appears twice, multiplying both forces and lengths, the effect is known as "reflected + inertia" and the equivalent value is the inertia of the spinning body multiplied by the *square of the gear ratio*, + in this case :math:`9=3^2`. The value applies to all degrees of freedom created by this joint. Besides increasing the realism of joints with geared transmission, positive :at:`armature` significantly improves simulation stability, even for small values, and is a recommended possible fix when encountering stability issues. diff --git a/doc/images/XMLreference/armature.gif b/doc/images/XMLreference/armature.gif new file mode 100644 index 0000000000000000000000000000000000000000..93f0e5833e58bfebba5b01f17979470b22b72e0a GIT binary patch literal 137047 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 literal 0 HcmV?d00001 diff --git a/doc/images/XMLreference/armature_dark.gif b/doc/images/XMLreference/armature_dark.gif new file mode 100644 index 0000000000000000000000000000000000000000..a478ff5c29d22b90a5a65442810fabde1342707e GIT binary patch literal 149304 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 xoAchKeoY7&HlrNJ>}a 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 literal 0 HcmV?d00001 diff --git a/test/engine/testdata/armature_equivalence.xml b/test/engine/testdata/armature_equivalence.xml index 78f16e02..c770488a 100644 --- a/test/engine/testdata/armature_equivalence.xml +++ b/test/engine/testdata/armature_equivalence.xml @@ -1,4 +1,13 @@ + + + + + + + + + @@ -10,7 +19,7 @@ - + @@ -27,8 +36,8 @@ - + - + From ebd30493c8301091f4a8c4d4dc35a96b1165a7ca Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Tue, 1 Apr 2025 01:44:42 -0700 Subject: [PATCH 022/191] Allow nameless bind in MJX. PiperOrigin-RevId: 742592221 Change-Id: I9490aebd64e3a31219c552d5b6eb55236680f10d --- doc/changelog.rst | 6 +- doc/python.rst | 14 +++-- mjx/mujoco/mjx/_src/support.py | 57 ++++++------------- mjx/mujoco/mjx/_src/support_test.py | 54 +++++++++--------- mjx/mujoco/mjx/_src/types.py | 2 + .../mujoco/codegen/generate_spec_bindings.py | 17 ++++++ 6 files changed, 78 insertions(+), 72 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index fd565e76..ad3f10cc 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -23,10 +23,14 @@ Bug fixes - Fixed a bug that caused the parent frame of elements in the child worldbody to be incorrectly set when attaching an mjSpec to a frame or a site. +Python bindings +^^^^^^^^^^^^^^^ +- Added support for nameless :ref:`mjSpec` objects in the ``bind`` method, see the corresponding :ref:`section` + in the documentation. + Version 3.3.0 (Feb 26, 2025) ---------------------------- - Feature promotion ^^^^^^^^^^^^^^^^^ .. youtube:: qJFbx-FR7Bc diff --git a/doc/python.rst b/doc/python.rst index 62541701..85a54b73 100644 --- a/doc/python.rst +++ b/doc/python.rst @@ -628,8 +628,10 @@ Parent: The parent body of a given element -- including bodies and frames -- can be accessed via the ``parent`` property. For example, the parent of a site can be accessed via ``site.parent``. -Relationship to ``PyMJCF`` --------------------------- +.. _PyMJCF: + +Relationship to ``PyMJCF`` and ``bind`` +--------------------------------------- `dm_control `__'s `PyMJCF `__ module provides similar @@ -645,9 +647,8 @@ includes a reimplementation of the ``PyMJCF`` example in the ``dm_control`` ``PyMJCF`` provides a notion of "binding", giving access to :ref:`mjModel` and :ref:`mjData` values via a helper class. In the native API, the helper class is not needed, so it is possible to directly bind an ``mjs`` object to -:ref:`mjModel` and :ref:`mjData`. This requires the objects to have a non-empty name. For example, say we have multiple -geoms containing the string "torso" in their name. We want to get their Cartesian positions in the XY plane from -``mjData``. This can be done as follows: +:ref:`mjModel` and :ref:`mjData`. For example, say we have multiple geoms containing the string "torso" in their name. +We want to get their Cartesian positions in the XY plane from ``mjData``. This can be done as follows: .. code-block:: python @@ -655,6 +656,9 @@ geoms containing the string "torso" in their name. We want to get their Cartesia pos_x = [torso.xpos[0] for torso in torsos] pos_y = [torso.xpos[1] for torso in torsos] +Using the ``bind`` method requires the :ref:`mjModel` and :ref:`mjData` to be compiled from the :`ref:`mjSpec`. If +objects are added or removed from the :ref:`mjSpec` since the last compilation, an error is raised. + Notes ----- diff --git a/mjx/mujoco/mjx/_src/support.py b/mjx/mujoco/mjx/_src/support.py index 591ee0ee..90c517ef 100644 --- a/mjx/mujoco/mjx/_src/support.py +++ b/mjx/mujoco/mjx/_src/support.py @@ -295,73 +295,56 @@ class BindModel(object): self.prefix = '' ids = [] for spec in specs: - if not spec.name: - raise KeyError(f'cannot bind spec with empty name') + if model.signature != spec.signature: + raise ValueError( + 'mjSpec signature does not match mjx.Model signature:' + f' {spec.signature} != {model.signature}' + ) + elif spec.id < 0: + raise KeyError(f'invalid id: {spec.id}') elif isinstance(spec, mujoco.MjsBody): self.prefix = 'body_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_BODY, spec.name) elif isinstance(spec, mujoco.MjsJoint): self.prefix = 'jnt_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_JOINT, spec.name) elif isinstance(spec, mujoco.MjsGeom): self.prefix = 'geom_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_GEOM, spec.name) elif isinstance(spec, mujoco.MjsSite): self.prefix = 'site_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_SITE, spec.name) elif isinstance(spec, mujoco.MjsLight): self.prefix = 'light_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_LIGHT, spec.name) elif isinstance(spec, mujoco.MjsCamera): self.prefix = 'cam_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_CAMERA, spec.name) elif isinstance(spec, mujoco.MjsMesh): self.prefix = 'mesh_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_MESH, spec.name) elif isinstance(spec, mujoco.MjsHField): self.prefix = 'hfield_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_HFIELD, spec.name) elif isinstance(spec, mujoco.MjsPair): self.prefix = 'pair_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_PAIR, spec.name) elif isinstance(spec, mujoco.MjsTendon): self.prefix = 'tendon_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_TENDON, spec.name) elif isinstance(spec, mujoco.MjsActuator): self.prefix = 'actuator_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_ACTUATOR, spec.name) elif isinstance(spec, mujoco.MjsSensor): self.prefix = 'sensor_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_SENSOR, spec.name) elif isinstance(spec, mujoco.MjsNumeric): self.prefix = 'numeric_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_NUMERIC, spec.name) elif isinstance(spec, mujoco.MjsText): self.prefix = 'text_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_TEXT, spec.name) elif isinstance(spec, mujoco.MjsTuple): self.prefix = 'tuple_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_TUPLE, spec.name) elif isinstance(spec, mujoco.MjsKey): self.prefix = 'key_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_KEY, spec.name) elif isinstance(spec, mujoco.MjsEquality): self.prefix = 'eq_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_EQUALITY, spec.name) elif isinstance(spec, mujoco.MjsExclude): self.prefix = 'exclude_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_EXCLUDE, spec.name) elif isinstance(spec, mujoco.MjsSkin): self.prefix = 'skin_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_SKIN, spec.name) elif isinstance(spec, mujoco.MjsMaterial): self.prefix = 'material_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_MATERIAL, spec.name) else: raise ValueError('invalid spec type') - if idx < 0: - raise KeyError(f'invalid name: {spec.name}') # pytype: disable=attribute-error - ids.append(idx) + ids.append(spec.id) if len(ids) == 1: self.id = ids[0] else: @@ -402,42 +385,36 @@ class BindData(object): self.prefix = '' ids = [] for spec in specs: - if not spec.name: - raise KeyError(f'cannot bind spec with empty name') + if model.signature != spec.signature: + raise ValueError( + 'mjSpec signature does not match mjx.Model signature:' + f' {spec.signature} != {model.signature}' + ) + if spec.id < 0: + raise KeyError(f'invalid id: {spec.id}') elif isinstance(spec, mujoco.MjsBody): - idx = name2id(model, mujoco.mjtObj.mjOBJ_BODY, spec.name) + pass elif isinstance(spec, mujoco.MjsJoint): self.prefix = 'jnt_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_JOINT, spec.name) elif isinstance(spec, mujoco.MjsGeom): self.prefix = 'geom_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_GEOM, spec.name) elif isinstance(spec, mujoco.MjsSite): self.prefix = 'site_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_SITE, spec.name) elif isinstance(spec, mujoco.MjsLight): self.prefix = 'light_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_LIGHT, spec.name) elif isinstance(spec, mujoco.MjsCamera): self.prefix = 'cam_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_CAMERA, spec.name) elif isinstance(spec, mujoco.MjsTendon): self.prefix = 'ten_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_TENDON, spec.name) elif isinstance(spec, mujoco.MjsActuator): self.prefix = 'actuator_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_ACTUATOR, spec.name) elif isinstance(spec, mujoco.MjsSensor): self.prefix = 'sensor_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_SENSOR, spec.name) elif isinstance(spec, mujoco.MjsEquality): self.prefix = 'eq_' - idx = name2id(model, mujoco.mjtObj.mjOBJ_EQUALITY, spec.name) else: raise ValueError('invalid spec type') - if idx < 0: - raise KeyError(f'invalid name: {spec.name}') # pytype: disable=attribute-error - ids.append(idx) + ids.append(spec.id) if len(ids) == 1: self.id = ids[0] else: diff --git a/mjx/mujoco/mjx/_src/support_test.py b/mjx/mujoco/mjx/_src/support_test.py index 5948f9e1..1fd1971b 100644 --- a/mjx/mujoco/mjx/_src/support_test.py +++ b/mjx/mujoco/mjx/_src/support_test.py @@ -176,15 +176,15 @@ class SupportTest(parameterized.TestCase): - - - + + + - - - + + + """ @@ -309,29 +309,28 @@ class SupportTest(parameterized.TestCase): dx7.bind(mx, body).xfrc_applied, [0, 0, 0, 0, 0, 0] ) - # test invalid name - with self.assertRaises( - AttributeError, msg='ctrl is not available for this type' + # test attribute and type mismatches + with self.assertRaisesRegex( + AttributeError, 'ctrl is not available for this type' ): print(dx.bind(mx, s.geoms).ctrl) - with self.assertRaises( - KeyError, msg='actuator_actuator_ctrl' - ): + with self.assertRaises(KeyError): print(dx.bind(mx, s.actuators).actuator_ctrl) - with self.assertRaises( - AttributeError, msg='actuator_actuator_ctrl' + with self.assertRaisesRegex( + AttributeError, + "'Data' object has no attribute 'actuator_actuator_ctrl'", ): print(dx.bind(mx, s.actuators).set('actuator_ctrl', [1, 2, 3])) - with self.assertRaises( - AttributeError, msg='qpos, qvel, qacc are not available for this type' + with self.assertRaisesRegex( + AttributeError, 'qpos, qvel, qacc are not available for this type' ): print(dx.bind(mx, s.geoms).qpos) - with self.assertRaises(KeyError, msg='invalid name: invalid_actuator_name'): - s.actuators[0].name = 'invalid_actuator_name' - print(dx.bind(mx, s.actuators).set('ctrl', [1, 2, 3])) - with self.assertRaises(KeyError, msg='invalid name: invalid_geom_name'): - s.geoms[0].name = 'invalid_geom_name' - print(mx.bind(s.geoms).pos) + + # test that modified names do not raise an error + s.actuators[0].name = 'modified_actuator_name' + np.testing.assert_array_equal(dx.bind(mx, s.actuators).ctrl, d.ctrl) + s.geoms[0].name = 'modified_geom_name' + np.testing.assert_array_equal(mx.bind(s.geoms[0]).pos, m.geom_pos[0, :]) # test batched data batch_size = 16 @@ -343,12 +342,15 @@ class SupportTest(parameterized.TestCase): vdx.bind(mx, s.bodies[i]).xpos, [d.xpos[i, :]] * batch_size ) - # test emtpy name + # test that adding a body requires recompilation s.worldbody.add_body() - m = s.compile() - mx = mjx.put_model(m) - with self.assertRaises(KeyError, msg='cannot bind spec with empty name'): + with self.assertRaises(ValueError) as e: mx.bind(s.bodies) + self.assertEqual( + str(e.exception), + 'mjSpec signature does not match mjx.Model signature:' + ' 5495345807332648606 != 270010677651259353', + ) _CONTACTS = """ diff --git a/mjx/mujoco/mjx/_src/types.py b/mjx/mujoco/mjx/_src/types.py index 228f3f3d..515d15f4 100644 --- a/mjx/mujoco/mjx/_src/types.py +++ b/mjx/mujoco/mjx/_src/types.py @@ -844,6 +844,7 @@ class Model(PyTreeNode): name_tupleadr: tuple name pointers (ntuple,) name_keyadr: keyframe name pointers (nkey,) names: names of all objects, 0-terminated (nnames,) + signature: compilation signature """ nq: int @@ -1187,6 +1188,7 @@ class Model(PyTreeNode): name_tupleadr: np.ndarray name_keyadr: np.ndarray names: bytes + signature: np.uint64 _sizes: jax.Array diff --git a/python/mujoco/codegen/generate_spec_bindings.py b/python/mujoco/codegen/generate_spec_bindings.py index e4617eab..bb52643d 100644 --- a/python/mujoco/codegen/generate_spec_bindings.py +++ b/python/mujoco/codegen/generate_spec_bindings.py @@ -627,6 +627,22 @@ def generate_signature() -> None: print(code) +def generate_id() -> None: + """Generate id functions.""" + for key, _, _, _, _ in SPECS: + if key == 'mjsPlugin': + continue + elem = key.removeprefix('mjs') + titlecase = 'Mjs' + elem + code = f"""\n + {key}.def_property_readonly("id", + [](raw::{titlecase}& self) -> int {{ + return mjs_getId(self.element); + }}); + """ + print(code) + + def main(argv: Sequence[str]) -> None: if len(argv) > 1: raise app.UsageError('Too many command-line arguments.') @@ -634,6 +650,7 @@ def main(argv: Sequence[str]) -> None: generate_add() generate_find() generate_signature() + generate_id() if __name__ == '__main__': From 86c970bc2315c788d594bc657e9b30fa7c5d29da Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Tue, 1 Apr 2025 03:33:54 -0700 Subject: [PATCH 023/191] Add error checking in mjs_setFrame. Also raise error if the frame is not found in the mjCBody copy constructor. Fixes #2543. PiperOrigin-RevId: 742624013 Change-Id: I4999b3165c97f8d079412214d027ca7a8dae8cb3 --- doc/APIreference/functions.rst | 2 +- doc/includes/references.h | 2 +- include/mujoco/mujoco.h | 4 +- python/mujoco/introspect/functions.py | 4 +- python/mujoco/specs.cc | 37 +++++++++++----- src/user/user_api.cc | 12 ++++-- src/user/user_api.h | 4 +- src/user/user_objects.cc | 17 +++++++- src/user/user_objects.h | 3 ++ src/xml/xml_native_reader.cc | 4 +- test/xml/xml_native_reader_test.cc | 61 +++++++++++++++++++++++++++ 11 files changed, 125 insertions(+), 25 deletions(-) diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index 7ca8f985..627b6871 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -4448,7 +4448,7 @@ Set element's default. .. mujoco-include:: mjs_setFrame -Set element's enclosing frame. +Set element's enclosing frame, return 0 on success. .. _mjs_resolveOrientation: diff --git a/doc/includes/references.h b/doc/includes/references.h index 355b5183..541dab8d 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -3683,7 +3683,7 @@ void mjs_setPluginAttributes(mjsPlugin* plugin, void* attributes); const char* mjs_getString(const mjString* source); const double* mjs_getDouble(const mjDoubleVec* source, int* size); void mjs_setDefault(mjsElement* element, const mjsDefault* def); -void mjs_setFrame(mjsElement* dest, mjsFrame* frame); +int mjs_setFrame(mjsElement* dest, mjsFrame* frame); const char* mjs_resolveOrientation(double quat[4], mjtByte degree, const char* sequence, const mjsOrientation* orientation); mjsFrame* mjs_bodyToFrame(mjsBody** body); diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index 5158e1f8..84496910 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -1640,8 +1640,8 @@ MJAPI const double* mjs_getDouble(const mjDoubleVec* source, int* size); // Set element's default. MJAPI void mjs_setDefault(mjsElement* element, const mjsDefault* def); -// Set element's enclosing frame. -MJAPI void mjs_setFrame(mjsElement* dest, mjsFrame* frame); +// Set element's enclosing frame, return 0 on success. +MJAPI int mjs_setFrame(mjsElement* dest, mjsFrame* frame); // Resolve alternative orientations to quat, return error if any. MJAPI const char* mjs_resolveOrientation(double quat[4], mjtByte degree, const char* sequence, diff --git a/python/mujoco/introspect/functions.py b/python/mujoco/introspect/functions.py index 737de191..d2955e80 100644 --- a/python/mujoco/introspect/functions.py +++ b/python/mujoco/introspect/functions.py @@ -10423,7 +10423,7 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ('mjs_setFrame', FunctionDecl( name='mjs_setFrame', - return_type=ValueType(name='void'), + return_type=ValueType(name='int'), parameters=( FunctionParameterDecl( name='dest', @@ -10438,7 +10438,7 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), ), ), - doc="Set element's enclosing frame.", + doc="Set element's enclosing frame, return 0 on success.", )), ('mjs_resolveOrientation', FunctionDecl( diff --git a/python/mujoco/specs.cc b/python/mujoco/specs.cc index bc9e5299..3a834ff3 100644 --- a/python/mujoco/specs.cc +++ b/python/mujoco/specs.cc @@ -564,7 +564,9 @@ PYBIND11_MODULE(_specs, m) { if (!attached_frame) { throw pybind11::value_error(mjs_getError(self.ptr)); } - mjs_setFrame(attached_frame->element, frame_ptr); + if (mjs_setFrame(attached_frame->element, frame_ptr) != 0) { + throw pybind11::value_error(mjs_getError(self.ptr)); + } } if (site.has_value()) { raw::MjsSite* site_ptr = nullptr; @@ -637,10 +639,11 @@ PYBIND11_MODULE(_specs, m) { return out; }, py::return_value_policy::reference_internal); - mjsBody.def("set_frame", - [](raw::MjsBody& self, raw::MjsFrame& frame) -> void { - mjs_setFrame(self.element, &frame); - }); + mjsBody.def("set_frame", [](raw::MjsBody& self, raw::MjsFrame& frame) { + if (mjs_setFrame(self.element, &frame) != 0) { + throw pybind11::value_error(mjs_getError(mjs_getSpec(self.element))); + } + }); mjsBody.def_property( "classname", [](raw::MjsBody& self) -> raw::MjsDefault* { @@ -850,7 +853,9 @@ PYBIND11_MODULE(_specs, m) { // ============================= MJSFRAME ==================================== mjsFrame.def("delete", [](raw::MjsFrame& self) { mjs_delete(self.element); }); mjsFrame.def("set_frame", [](raw::MjsFrame& self, raw::MjsFrame& frame) { - mjs_setFrame(self.element, &frame); + if (mjs_setFrame(self.element, &frame) != 0) { + throw pybind11::value_error(mjs_getError(mjs_getSpec(self.element))); + } }); mjsFrame.def_property_readonly( "parent", @@ -879,7 +884,9 @@ PYBIND11_MODULE(_specs, m) { // ============================= MJSGEOM ===================================== mjsGeom.def("delete", [](raw::MjsGeom& self) { mjs_delete(self.element); }); mjsGeom.def("set_frame", [](raw::MjsGeom& self, raw::MjsFrame& frame) { - mjs_setFrame(self.element, &frame); + if (mjs_setFrame(self.element, &frame) != 0) { + throw pybind11::value_error(mjs_getError(mjs_getSpec(self.element))); + } }); mjsGeom.def_property_readonly( "parent", @@ -899,7 +906,9 @@ PYBIND11_MODULE(_specs, m) { // ============================= MJSJOINT ==================================== mjsJoint.def("delete", [](raw::MjsJoint& self) { mjs_delete(self.element); }); mjsJoint.def("set_frame", [](raw::MjsJoint& self, raw::MjsFrame& frame) { - mjs_setFrame(self.element, &frame); + if (mjs_setFrame(self.element, &frame) != 0) { + throw pybind11::value_error(mjs_getError(mjs_getSpec(self.element))); + } }); mjsJoint.def_property_readonly( "parent", @@ -919,7 +928,9 @@ PYBIND11_MODULE(_specs, m) { // ============================= MJSSITE ===================================== mjsSite.def("delete", [](raw::MjsSite& self) { mjs_delete(self.element); }); mjsSite.def("set_frame", [](raw::MjsSite& self, raw::MjsFrame& frame) { - mjs_setFrame(self.element, &frame); + if (mjs_setFrame(self.element, &frame) != 0) { + throw pybind11::value_error(mjs_getError(mjs_getSpec(self.element))); + } }); mjsSite.def_property_readonly( "parent", @@ -957,7 +968,9 @@ PYBIND11_MODULE(_specs, m) { mjsCamera.def("delete", [](raw::MjsCamera& self) { mjs_delete(self.element); }); mjsCamera.def("set_frame", [](raw::MjsCamera& self, raw::MjsFrame& frame) { - mjs_setFrame(self.element, &frame); + if (mjs_setFrame(self.element, &frame) != 0) { + throw pybind11::value_error(mjs_getError(mjs_getSpec(self.element))); + } }); mjsCamera.def_property_readonly( "parent", @@ -977,7 +990,9 @@ PYBIND11_MODULE(_specs, m) { // ============================= MJSLIGHT ==================================== mjsLight.def("delete", [](raw::MjsLight& self) { mjs_delete(self.element); }); mjsLight.def("set_frame", [](raw::MjsLight& self, raw::MjsFrame& frame) { - mjs_setFrame(self.element, &frame); + if (mjs_setFrame(self.element, &frame) != 0) { + throw pybind11::value_error(mjs_getError(mjs_getSpec(self.element))); + } }); mjsLight.def_property_readonly( "parent", diff --git a/src/user/user_api.cc b/src/user/user_api.cc index 0e339780..646bb986 100644 --- a/src/user/user_api.cc +++ b/src/user/user_api.cc @@ -758,13 +758,19 @@ mjsFrame* mjs_findFrame(mjSpec* s, const char* name) { // set frame -void mjs_setFrame(mjsElement* dest, mjsFrame* frame) { +int mjs_setFrame(mjsElement* dest, mjsFrame* frame) { if (!frame) { - return; + return -1; } mjCFrame* frameC = static_cast(frame->element); mjCBase* baseC = static_cast(dest); - baseC->SetFrame(frameC); + try { + baseC->SetFrame(frameC); + return 0; + } catch (mjCError& e) { + baseC->model->SetError(e); + return -1; + } } diff --git a/src/user/user_api.h b/src/user/user_api.h index 3c6c4420..01aa9856 100644 --- a/src/user/user_api.h +++ b/src/user/user_api.h @@ -370,8 +370,8 @@ MJAPI const double* mjs_getDouble(const mjDoubleVec* source, int* size); // Set element's default. MJAPI void mjs_setDefault(mjsElement* element, const mjsDefault* def); -// Set element's enlcosing frame. -MJAPI void mjs_setFrame(mjsElement* dest, mjsFrame* frame); +// Set element's enclosing frame, return 0 on success. +MJAPI int mjs_setFrame(mjsElement* dest, mjsFrame* frame); // Resolve alternative orientations to quat, return error if any. MJAPI const char* mjs_resolveOrientation(double quat[4], mjtByte degree, const char* sequence, diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 1e9cf407..d65ac68b 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -773,6 +773,9 @@ void mjCBase::SetFrame(mjCFrame* _frame) { if (!_frame) { return; } + if (_frame->body && GetParent() != _frame->body) { + throw mjCError(this, "Frame and body '%s' have mismatched parents", name.c_str()); + } frame = _frame; } @@ -891,8 +894,18 @@ mjCBody& mjCBody::operator+=(const mjCBody& other) { for (int i=0; i < other.bodies.size(); i++) { bodies.push_back(new mjCBody(*other.bodies[i], model)); // triggers recursive call bodies.back()->parent = this; - bodies.back()->frame = - other.bodies[i]->frame ? frames[fmap[other.bodies[i]->frame]] : nullptr; + bodies.back()->frame = nullptr; + if (other.bodies[i]->frame) { + if (fmap.find(other.bodies[i]->frame) != fmap.end()) { + bodies.back()->frame = frames[fmap[other.bodies[i]->frame]]; + } else { + throw mjCError(this, "Frame '%s' not found in other body", + other.bodies[i]->frame->name.c_str()); + } + if (bodies.back()->frame && bodies.back()->frame->body != this) { + throw mjCError(this, "Frame and body '%s' have mismatched parents", name.c_str()); + } + } } return *this; diff --git a/src/user/user_objects.h b/src/user/user_objects.h index 6d9b146e..f04bf850 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -254,6 +254,9 @@ class mjCBase : public mjCBase_ { // Copy plugins instantiated in this object virtual void CopyPlugin() {} + // Returns parent of this object + virtual mjCBase* GetParent() const { return nullptr; } + // Copy assignment mjCBase& operator=(const mjCBase& other); diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 272b5a2c..7fda10cd 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -3647,7 +3647,9 @@ void mjXReader::Body(XMLElement* section, mjsBody* body, mjsFrame* frame, } } else { // only set frame to existing body - mjs_setFrame(child->element, pframe); + if (mjs_setFrame(child->element, pframe)) { + throw mjXError(elem, mjs_getError(spec)); + } } } diff --git a/test/xml/xml_native_reader_test.cc b/test/xml/xml_native_reader_test.cc index 52650330..7b74af4c 100644 --- a/test/xml/xml_native_reader_test.cc +++ b/test/xml/xml_native_reader_test.cc @@ -1449,6 +1449,67 @@ TEST_F(XMLReaderTest, ParseReplicateRepeatedName) { EXPECT_THAT(error.data(), HasSubstr("Element 'replicate'")); } +TEST_F(XMLReaderTest, RepeatedPrefix) { + static constexpr char parent[] = R"( + + + + + + + + + + + + + + )"; + + static constexpr char child_1[] = R"( + + + + + + + + + + + + + + )"; + + static constexpr char child_2[] = R"( + + + + + + )"; + + auto vfs = std::make_unique(); + 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 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("mismatched parents")); + mj_deleteSpec(spec); + mj_deleteVFS(vfs.get()); +} + TEST_F(XMLReaderTest, ParseReplicateExcludeTendon) { static constexpr char xml[] = R"( From 648a03c2cf136f67ceb6ab9f64e2d80bbf901d40 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Tue, 1 Apr 2025 05:20:59 -0700 Subject: [PATCH 024/191] Override inertia of body with visual geoms if discardvisual is true. Fixes #2546. PiperOrigin-RevId: 742650702 Change-Id: I753c1e1c89da732ece100676644bbecf9d6373bb --- src/user/user_model.cc | 31 ++++++++++++++++++++++++++----- src/user/user_objects.cc | 10 ---------- test/user/user_api_test.cc | 3 +-- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/user/user_model.cc b/src/user/user_model.cc index a63f13c9..11ef0755 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -558,7 +558,7 @@ void mjCModel::RemoveFromList(std::vector& list, const mjCModel& other) { template <> void mjCModel::DeleteAll(std::vector& elements) { for (mjCKey* element : elements) { - delete element; + element->Release(); } elements.clear(); } @@ -1553,7 +1553,7 @@ static void DeleteElements(std::vector& elements, int i = 0; for (int j=0; j < elements.size(); j++) { if (discard[j]) { - delete elements[j]; + elements[j]->Release(); } else { elements[i] = elements[j]; i++; @@ -1611,7 +1611,7 @@ void mjCModel::DeleteAll(std::vector& elements) { DeleteMaterial(sites_); DeleteMaterial(tendons_); for (mjCMaterial* element : elements) { - delete element; + element->Release(); } elements.clear(); } @@ -1621,7 +1621,7 @@ template <> void mjCModel::DeleteAll(std::vector& elements) { DeleteAllTextures(materials_); for (mjCTexture* element : elements) { - delete element; + element->Release(); } elements.clear(); } @@ -1781,7 +1781,6 @@ void mjCModel::IndexAssets(bool discard) { } } - // discard visual meshes and geoms if (discard) { std::vector discard_mesh(meshes_.size(), false); std::vector discard_geom(geoms_.size(), false); @@ -1795,6 +1794,28 @@ void mjCModel::IndexAssets(bool discard) { return geom->IsVisual(); }); + // update inertia in bodies + for (auto body : bodies_) { + if (body->spec.explicitinertial) { + continue; + } + for (auto geom : body->geoms) { + if (geom->IsVisual()) { + if (compiler.inertiafromgeom == mjINERTIAFROMGEOM_TRUE) { + compiler.inertiafromgeom = mjINERTIAFROMGEOM_AUTO; + } + body->explicitinertial = true; // for XML writer + body->spec.explicitinertial = true; + body->spec.mass = body->mass; + mjuu_copyvec(body->spec.ipos, body->ipos, 3); + mjuu_copyvec(body->spec.iquat, body->iquat, 4); + mjuu_copyvec(body->spec.inertia, body->inertia, 3); + break; + } + } + } + + // discard visual meshes and geoms Delete(meshes_, discard_mesh); Delete(geoms_, discard_geom); } diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index d65ac68b..d46c51df 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -1976,16 +1976,6 @@ void mjCBody::Compile(void) { } } - // if discarding visual geoms, use explicit inertias - if (compiler->discardvisual) { - for (int j=0; j < geoms.size(); j++) { - if (geoms[j]->IsVisual()) { - explicitinertial = true; - break; - } - } - } - // free joint alignment, phase 2 (transform sites, cameras and lights) if (align_free) { // frames have already been compiled and applied to children diff --git a/test/user/user_api_test.cc b/test/user/user_api_test.cc index 3837c423..d07f0388 100644 --- a/test/user/user_api_test.cc +++ b/test/user/user_api_test.cc @@ -520,8 +520,7 @@ TEST_F(PluginTest, RecompileCompare) { // if file is meant to fail, skip it if (absl::StrContains(p.path().string(), "malformed_") || absl::StrContains(p.path().string(), "touch_grid") || - absl::StrContains(p.path().string(), "cow") || - absl::StrContains(p.path().string(), "discardvisual")) { + absl::StrContains(p.path().string(), "cow")) { continue; } From 9ecec6f238e5a1932dc3c79ac4e4f65f5d53fd5f Mon Sep 17 00:00:00 2001 From: Kyle Bayes Date: Tue, 1 Apr 2025 06:10:30 -0700 Subject: [PATCH 025/191] Fix bad contact from passing through polytope2 in nativeccd. PiperOrigin-RevId: 742666700 Change-Id: I76034579b4e663f9abfa265ec3e2bc608f631f59 --- src/engine/engine_collision_gjk.c | 26 ++- src/engine/engine_collision_gjk.h | 2 +- test/engine/engine_collision_gjk_test.cc | 205 +++++++++++++++-------- 3 files changed, 155 insertions(+), 78 deletions(-) diff --git a/src/engine/engine_collision_gjk.c b/src/engine/engine_collision_gjk.c index 16859bf5..1fcc03d9 100644 --- a/src/engine/engine_collision_gjk.c +++ b/src/engine/engine_collision_gjk.c @@ -903,6 +903,26 @@ static void rotmat(mjtNum R[9], const mjtNum axis[3]) { +// return nonzero if the ray v1v2 intersects the triangle v3v4v5 +static inline int rayTriangle(const mjtNum v1[3], const mjtNum v2[3], const mjtNum v3[3], + const mjtNum v4[3], const mjtNum v5[3]) { + mjtNum diff12[3], diff13[3], diff14[3], diff15[3]; + sub3(diff12, v2, v1); + sub3(diff13, v3, v1); + sub3(diff14, v4, v1); + sub3(diff15, v5, v1); + + mjtNum vol1 = det3(diff13, diff14, diff12); + mjtNum vol2 = det3(diff14, diff15, diff12); + mjtNum vol3 = det3(diff15, diff13, diff12); + + if (vol1 >= 0 && vol2 >= 0 && vol3 >= 0) return 1; + if (vol1 <= 0 && vol2 <= 0 && vol3 <= 0) return -1; + return 0; +} + + + // create a polytope from a 1-simplex (returns 0 on success) static int polytope2(Polytope* pt, mjCCDStatus* status, mjCCDObj* obj1, mjCCDObj* obj2) { mjtNum *v1 = status->simplex[0].vert, *v2 = status->simplex[1].vert; @@ -970,9 +990,9 @@ static int polytope2(Polytope* pt, mjCCDStatus* status, mjCCDObj* obj1, mjCCDObj return polytope3(pt, status, obj1, obj2); } - // check that origin is in the hexahedron - if (status->dist > 10*mjMINVAL && !testTetra(v1, v3, v4, v5) && !testTetra(v2, v3, v4, v5)) { - return mjEPA_P2_MISSING_ORIGIN; + // check hexahedron is convex + if (!rayTriangle(v1, v2, v3, v4, v5)) { + return mjEPA_P2_NONCONVEX; } for (int i = 0; i < 6; i++) { diff --git a/src/engine/engine_collision_gjk.h b/src/engine/engine_collision_gjk.h index 027929e3..a8f2224b 100644 --- a/src/engine/engine_collision_gjk.h +++ b/src/engine/engine_collision_gjk.h @@ -44,7 +44,7 @@ typedef enum { mjEPA_NOCONTACT = -1, mjEPA_SUCCESS = 0, mjEPA_P2_INVALID_FACES, - mjEPA_P2_MISSING_ORIGIN, + mjEPA_P2_NONCONVEX, mjEPA_P2_ORIGIN_ON_FACE, mjEPA_P3_BAD_NORMAL, mjEPA_P3_INVALID_V4, diff --git a/test/engine/engine_collision_gjk_test.cc b/test/engine/engine_collision_gjk_test.cc index 6db923f2..dfaebaad 100644 --- a/test/engine/engine_collision_gjk_test.cc +++ b/test/engine/engine_collision_gjk_test.cc @@ -93,7 +93,7 @@ mjtNum GeomDist(mjModel* m, mjData* d, int g1, int g2, mjtNum x1[3], return dist; } -int Penetration(mjtNum& depth, std::vector& dir, +int Penetration(mjCCDStatus& status, mjtNum& depth, std::vector& dir, std::vector& pos, mjModel* model, mjData* data, int g1, int g2, mjtNum margin = 0, int max_contacts = 1) { mjCCDObj obj1, obj2; @@ -128,7 +128,6 @@ int Penetration(mjtNum& depth, std::vector& dir, #endif mjCCDConfig config; - mjCCDStatus status; // set config config.max_iterations = kMaxIterations; @@ -235,9 +234,11 @@ TEST_F(MjGjkTest, SphereSphereNoDist) { int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2); + int ncons = Penetration(status, dist, dir, pos, model, data, geom1, geom2); EXPECT_EQ(ncons, 0); mj_deleteData(data); @@ -262,9 +263,11 @@ TEST_F(MjGjkTest, SphereSphereIntersect) { int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2); + int ncons = Penetration(status, dist, dir, pos, model, data, geom1, geom2); EXPECT_EQ(ncons, 1); @@ -304,9 +307,10 @@ TEST_F(MjGjkTest, BoxBoxDepth) { int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2); + int ncons = Penetration(status, dist, dir, pos, model, data, geom1, geom2); EXPECT_EQ(ncons, 1); @@ -354,9 +358,11 @@ TEST_F(MjGjkTest, BoxBoxDepth2) { int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2); + int ncons = Penetration(status, dist, dir, pos, model, data, geom1, geom2); if (ncons == 1) { EXPECT_NEAR(dist, -0.033401579411886845, kTolerance); @@ -421,9 +427,11 @@ TEST_F(MjGjkTest, BoxBoxDepth3) { int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2); + int ncons = Penetration(status, dist, dir, pos, model, data, geom1, geom2); EXPECT_EQ(ncons, 1); EXPECT_NEAR(dist, -0.003066, kTolerance); @@ -453,11 +461,14 @@ TEST_F(MjGjkTest, BoxBoxTouching) { int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2); + int ncons = Penetration(status, dist, dir, pos, model, data, geom1, geom2); EXPECT_EQ(ncons, 0); + EXPECT_GT(status.epa_status, 0); mj_deleteData(data); mj_deleteModel(model); @@ -479,11 +490,13 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD) { mjData* data = mj_makeData(model); mj_forward(model, data); - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2, 0, 1000); + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); EXPECT_EQ(ncons, 4); EXPECT_NEAR(dist, -.1, kTolerance); @@ -517,11 +530,13 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD2) { mjData* data = mj_makeData(model); mj_forward(model, data); - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2, 0, 1000); + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); EXPECT_EQ(ncons, 4); EXPECT_NEAR(dist, -.1, kTolerance); @@ -573,11 +588,13 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD3) { xpos[2] = 1.095456702630382306296041861060; - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2, 0, 1000); + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); EXPECT_EQ(ncons, 4); mj_deleteData(data); @@ -634,11 +651,13 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD4) { xpos[1] = -0.023500601273213628239489025873; xpos[2] = -4.958782854594746325460619118530; - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2, 0, 1000); + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); EXPECT_EQ(ncons, 8); EXPECT_NEAR(dist, -0.00060425119242707459, kTolerance); @@ -701,11 +720,13 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD5) { xpos[2] = -4.659108354876987156956147373421; - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2, 0, 1000); + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); EXPECT_EQ(ncons, 8); EXPECT_NEAR(dist, -0.0001077858631973211, kTolerance); @@ -750,11 +771,13 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD6) { xpos[1] = 0.190777715293135141649827346555; xpos[2] = 0.100006658017411736993906856696; - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2, 0, 1000); + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); EXPECT_EQ(ncons, 5); EXPECT_NEAR(dist, -0.00009843, kTolerance); @@ -817,11 +840,13 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD7) { xpos[2] = -4.958375812037025376355359185254; - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2, 0, 1000); + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); EXPECT_EQ(ncons, 8); mj_deleteData(data); @@ -878,11 +903,13 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD8) { xpos[1] = -0.023505499999999998617106200527; xpos[2] = -4.958574289672835533338002278470; - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2, 0, 1000); + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); EXPECT_EQ(ncons, 4); mj_deleteData(data); @@ -940,11 +967,13 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD9) { xpos[2] = 0.2156259187793853615566774806211469694972; - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2, 0, 1000); + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); EXPECT_EQ(ncons, 4); mj_deleteData(data); @@ -979,11 +1008,13 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD10) { xpos[1] = -0.0765140000000000264357424839545274153352; xpos[2] = 0.1751399999999999623767621415026951581240; - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2, 0, 8); + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 8); EXPECT_EQ(ncons, 4); @@ -1030,9 +1061,11 @@ TEST_F(MjGjkTest, SmallBoxMesh) { int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2); + int ncons = Penetration(status, dist, dir, pos, model, data, geom1, geom2); EXPECT_EQ(ncons, 1); EXPECT_NEAR(dist, 0, kTolerance); @@ -1072,11 +1105,13 @@ TEST_F(MjGjkTest, BoxMesh) { mjData* data = mj_makeData(model); mj_forward(model, data); - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom2, geom1, 0, 1000); + int ncons = Penetration(status, dist, dir, pos, model, data, g2, g1, 0, 1000); EXPECT_EQ(ncons, 4); mj_deleteData(data); @@ -1105,11 +1140,13 @@ TEST_F(MjGjkTest, BoxMesh2) { mjData* data = mj_makeData(model); mj_forward(model, data); - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom2, geom1, 0, 1000); + int ncons = Penetration(status, dist, dir, pos, model, data, g2, g1, 0, 1000); EXPECT_EQ(ncons, 5); mj_deleteData(data); @@ -1138,11 +1175,13 @@ TEST_F(MjGjkTest, BoxMeshPrune) { mjData* data = mj_makeData(model); mj_forward(model, data); - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom2, geom1, 0, 4); + int ncons = Penetration(status, dist, dir, pos, model, data, g2, g1, 0, 4); EXPECT_EQ(ncons, 4); mj_deleteData(data); @@ -1173,11 +1212,13 @@ TEST_F(MjGjkTest, MeshMesh) { mjData* data = mj_makeData(model); mj_forward(model, data); - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2, 0, 1000); + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); EXPECT_EQ(ncons, 5); mj_deleteData(data); @@ -1208,11 +1249,13 @@ TEST_F(MjGjkTest, MeshMeshPrune) { mjData* data = mj_makeData(model); mj_forward(model, data); - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2, 0, 4); + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 4); EXPECT_EQ(ncons, 4); mj_deleteData(data); @@ -1246,11 +1289,13 @@ TEST_F(MjGjkTest, BoxEdge) { mjData* data = mj_makeData(model); mj_forward(model, data); - int geom1 = mj_name2id(model, mjOBJ_GEOM, "box2"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "box3"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "box2"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "box3"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2, 0, 4); + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 4); EXPECT_EQ(ncons, 2); mj_deleteData(data); @@ -1319,11 +1364,13 @@ TEST_F(MjGjkTest, BoxEdge2) { xpos[1] = 0.9828851949225971829093850828940048813820; xpos[2] = 3.0930077345364814789263618877157568931580; - int geom1 = mj_name2id(model, mjOBJ_GEOM, "box2"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "box3"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "box2"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "box3"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2, 0, 4); + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 4); EXPECT_EQ(ncons, 2); mj_deleteData(data); @@ -1391,11 +1438,13 @@ TEST_F(MjGjkTest, BoxEdgeEdge) { xpos[1] = -0.0000000000000000008679606505055748997840; xpos[2] = 2.8141526153588731773425024584867060184479; - int geom1 = mj_name2id(model, mjOBJ_GEOM, "box2"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "box3"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "box2"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "box3"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2, 0, 4); + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 4); EXPECT_EQ(ncons, 2); mj_deleteData(data); @@ -1439,11 +1488,13 @@ TEST_F(MjGjkTest, MeshEdge) { mjData* data = mj_makeData(model); mj_forward(model, data); - int geom1 = mj_name2id(model, mjOBJ_GEOM, "box2"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "box3"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "box2"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "box3"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2, 0, 4); + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 4); EXPECT_EQ(ncons, 2); mj_deleteData(data); @@ -1461,9 +1512,11 @@ TEST_F(MjGjkTest, EllipsoidEllipsoidPenetrating) { int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2); + int ncons = Penetration(status, dist, dir, pos, model, data, geom1, geom2); EXPECT_EQ(ncons, 1); EXPECT_NEAR(dist, -0.00022548856248122027, kTolerance); @@ -1542,11 +1595,13 @@ static constexpr char xml[] = R"( mjData* data = mj_makeData(model); mj_forward(model, data); - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2); + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2); EXPECT_EQ(ncons, 1); EXPECT_NEAR(dist, -0.01, kTolerance); @@ -1560,7 +1615,7 @@ static constexpr char xml[] = R"( EXPECT_NEAR(pos[2], -0.005, kTolerance); // multicontact - ncons = Penetration(dist, dir, pos, model, data, geom1, geom2, 0, 1000); + ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 1000); EXPECT_EQ(ncons, 4); mj_deleteData(data); @@ -1583,11 +1638,13 @@ TEST_F(MjGjkTest, EllipsoidEllipsoidIntersect) { mjData* data = mj_makeData(model); mj_forward(model, data); - int geom1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); - int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; std::vector dir, pos; mjtNum dist; - int ncons = Penetration(dist, dir, pos, model, data, geom1, geom2, 15); + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 15); EXPECT_EQ(ncons, 1); EXPECT_NEAR(dist, -14.245732934582151, kTolerance); From c0a7ea1edb65d94a17556d456880cbc18327d00b Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Tue, 1 Apr 2025 06:50:50 -0700 Subject: [PATCH 026/191] Added the element type mjOBJ_SPEC for mjSpec. PiperOrigin-RevId: 742678572 Change-Id: I95a382ac58b5bc88c2aa5c07210549a1d5e2f538 --- doc/includes/references.h | 3 ++- include/mujoco/mjmodel.h | 3 ++- python/mujoco/introspect/enums.py | 1 + src/engine/engine_io.c | 1 + src/user/user_model.cc | 2 +- unity/Runtime/Bindings/MjBindings.cs | 1 + 6 files changed, 8 insertions(+), 3 deletions(-) diff --git a/doc/includes/references.h b/doc/includes/references.h index 541dab8d..d3e7ba18 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -607,7 +607,8 @@ typedef enum mjtObj_ { // type of MujoCo object // meta elements, do not appear in mjModel mjOBJ_FRAME = 100, // frame - mjOBJ_DEFAULT // default + mjOBJ_DEFAULT, // default + mjOBJ_MODEL // entire model } mjtObj; typedef enum mjtConstraint_ { // type of constraint diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index 185d31de..632f6ddd 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -269,7 +269,8 @@ typedef enum mjtObj_ { // type of MujoCo object // meta elements, do not appear in mjModel mjOBJ_FRAME = 100, // frame - mjOBJ_DEFAULT // default + mjOBJ_DEFAULT, // default + mjOBJ_MODEL // entire model } mjtObj; diff --git a/python/mujoco/introspect/enums.py b/python/mujoco/introspect/enums.py index 66b937a2..9443566f 100644 --- a/python/mujoco/introspect/enums.py +++ b/python/mujoco/introspect/enums.py @@ -288,6 +288,7 @@ ENUMS: Mapping[str, EnumDecl] = dict([ ('mjNOBJECT', 26), ('mjOBJ_FRAME', 100), ('mjOBJ_DEFAULT', 101), + ('mjOBJ_MODEL', 102), ]), )), ('mjtConstraint', diff --git a/src/engine/engine_io.c b/src/engine/engine_io.c index ec1757f6..f982941f 100644 --- a/src/engine/engine_io.c +++ b/src/engine/engine_io.c @@ -2143,6 +2143,7 @@ static int numObjects(const mjModel* m, mjtObj objtype) { case mjOBJ_DEFAULT: case mjOBJ_FRAME: case mjOBJ_UNKNOWN: + case mjOBJ_MODEL: return -1; case mjOBJ_BODY: case mjOBJ_XBODY: diff --git a/src/user/user_model.cc b/src/user/user_model.cc index 11ef0755..402f738b 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -166,7 +166,7 @@ static void processlist(mjListKeyMap& ids, vector& list, // constructor mjCModel::mjCModel() { mjs_defaultSpec(&spec); - elemtype = mjOBJ_UNKNOWN; + elemtype = mjOBJ_MODEL; spec_comment_.clear(); spec_modelfiledir_.clear(); spec_meshdir_.clear(); diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index 7aecffe5..d7e79806 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -320,6 +320,7 @@ public enum mjtObj : int{ mjNOBJECT = 26, mjOBJ_FRAME = 100, mjOBJ_DEFAULT = 101, + mjOBJ_MODEL = 102, } public enum mjtConstraint : int{ mjCNSTR_EQUALITY = 0, From 305b68ecbfd9e32d13d0c92c8d7b55743dcacb56 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 1 Apr 2025 07:14:00 -0700 Subject: [PATCH 027/191] Add tests for `body.find_all()` of joints and geoms, fix error message. Fixes #2525 PiperOrigin-RevId: 742685596 Change-Id: I5f0969967f2490d32e5691983e8d0c363d48a6bd --- python/mujoco/specs.cc | 2 +- python/mujoco/specs_test.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/python/mujoco/specs.cc b/python/mujoco/specs.cc index 3a834ff3..64afef62 100644 --- a/python/mujoco/specs.cc +++ b/python/mujoco/specs.cc @@ -261,7 +261,7 @@ py::list FindAllImpl(raw::MjsBody& body, mjtObj objtype, bool recursive) { // this should never happen throw pybind11::value_error( "body.find_all supports the types: body, frame, geom, site, " - "light, camera."); + "joint, light, camera."); break; } el = mjs_nextChild(&body, el, recursive); diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py index f0c3bb6c..bfefd71f 100644 --- a/python/mujoco/specs_test.py +++ b/python/mujoco/specs_test.py @@ -636,6 +636,8 @@ class SpecsTest(absltest.TestCase): + + @@ -649,6 +651,8 @@ class SpecsTest(absltest.TestCase): self.assertLen(spec.sites, 5) self.assertLen(spec.worldbody.find_all('body'), 4) self.assertLen(spec.worldbody.find_all('site'), 5) + self.assertLen(spec.worldbody.find_all('joint'), 1) + self.assertLen(spec.worldbody.find_all('geom'), 1) self.assertEqual(spec.bodies[1].name, 'body1') self.assertEqual(spec.bodies[2].name, 'body2') self.assertEqual(spec.bodies[3].name, 'body3') From 08d22baea1a21e13518d195b59d1b940505089f9 Mon Sep 17 00:00:00 2001 From: Kyle Bayes Date: Tue, 1 Apr 2025 07:20:17 -0700 Subject: [PATCH 028/191] Fix bug in horizon code in nativeccd where parallel faces are included in horizon. PiperOrigin-RevId: 742687622 Change-Id: I4a180cb9c7451e0a2c22ffd77b7b6f6fd68836a4 --- src/engine/engine_collision_gjk.c | 2 +- test/engine/engine_collision_gjk_test.cc | 66 ++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/engine/engine_collision_gjk.c b/src/engine/engine_collision_gjk.c index 1fcc03d9..92d49a6a 100644 --- a/src/engine/engine_collision_gjk.c +++ b/src/engine/engine_collision_gjk.c @@ -1271,7 +1271,7 @@ static int horizonRec(Polytope* pt, Face* face, int e) { mjtNum dist2 = face->dist * face->dist; // v is visible from w so it is deleted and adjacent faces are checked - if (dot3(face->v, pt->horizon.w) >= dist2) { + if (dot3(face->v, pt->horizon.w) > dist2) { deleteFace(pt, face); // recursively search the adjacent faces on the next two edges diff --git a/test/engine/engine_collision_gjk_test.cc b/test/engine/engine_collision_gjk_test.cc index dfaebaad..325f6fd7 100644 --- a/test/engine/engine_collision_gjk_test.cc +++ b/test/engine/engine_collision_gjk_test.cc @@ -1022,6 +1022,72 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD10) { mj_deleteModel(model); } +TEST_F(MjGjkTest, BoxBoxMultiCCD11) { + static constexpr char xml[] = R"( + + + + + + )"; + + std::array error; + mjModel* model = LoadModelFromString(xml, error.data(), error.size()); + ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error.data(); + + mjData* data = mj_makeData(model); + mj_forward(model, data); + + mjtNum* xpos = data->geom_xpos; + mjtNum* xmat = data->geom_xmat; + + xmat[0] = 1.0000000000000000000000000000000000000000; + xmat[1] = 0.0000000000000000000000000000000000000000; + xmat[2] = 0.0000000000000000000000000000000000000000; + xmat[3] = 0.0000000000000000000000000000000000000000; + xmat[4] = 1.0000000000000000000000000000000000000000; + xmat[5] = -0.0000000000000000013928437397151766790940; + xmat[6] = 0.0000000000000000000000000000000000000000; + xmat[7] = 0.0000000000000000013928437397151766790940; + xmat[8] = 1.0000000000000000000000000000000000000000; + + xpos[0] = -0.1036549999999999971400654885655967518687; + xpos[1] = -0.1963450000000000195132798808117513544858; + xpos[2] = 0.1247685038468368534658736734854755923152; + + + xpos = data->geom_xpos + 3; + xmat = data->geom_xmat + 9; + + xmat[0] = 1.0000000000000000000000000000000000000000; + xmat[1] = 0.0000000000000000000000000000000000000000; + xmat[2] = 0.0000000000000000000000000000000000000000; + xmat[3] = 0.0000000000000000000000000000000000000000; + xmat[4] = 1.0000000000000000000000000000000000000000; + xmat[5] = -0.0000000000000000018885268354605779111974; + xmat[6] = 0.0000000000000000000000000000000000000000; + xmat[7] = 0.0000000000000000018885268354605779111974; + xmat[8] = 1.0000000000000000000000000000000000000000; + + xpos[0] = -0.1036549999999999971400654885655967518687; + xpos[1] = -0.1963450000000000195132798808117513544858; + xpos[2] = 0.1745248497897437800485676007156143896282; + + + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; + std::vector dir, pos; + mjtNum dist; + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 8); + + EXPECT_EQ(ncons, 4); + + mj_deleteData(data); + mj_deleteModel(model); +} + TEST_F(MjGjkTest, SmallBoxMesh) { static constexpr char xml[] = R"( From a02a27d4a4758b298c1480e9cb93117ba03cd98f Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Tue, 1 Apr 2025 08:04:58 -0700 Subject: [PATCH 029/191] Combine all mjs_attach functions into one. PiperOrigin-RevId: 742702092 Change-Id: I89e35c59ada017cd061e2031584eeea71776743d --- doc/APIreference/functions.rst | 37 +----- doc/XMLreference.rst | 5 +- doc/changelog.rst | 2 + doc/includes/references.h | 10 +- doc/programming/modeledit.rst | 29 ++--- include/mujoco/mujoco.h | 18 +-- python/mujoco/introspect/functions.py | 114 +----------------- python/mujoco/specs.cc | 24 ++-- src/user/user_api.cc | 159 +++++++++++++++----------- src/user/user_api.h | 18 +-- src/xml/xml_native_reader.cc | 4 +- test/user/user_api_test.cc | 99 +++++++++------- test/user/user_model_test.cc | 2 +- 13 files changed, 202 insertions(+), 319 deletions(-) diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index 627b6871..029fac76 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -3808,41 +3808,14 @@ Free all pointers with ``mju_free()``. Attachment ^^^^^^^^^^ -.. _mjs_attachBody: +.. _mjs_attach: -`mjs_attachBody <#mjs_attachBody>`__ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +`mjs_attach <#mjs_attach>`__ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. mujoco-include:: mjs_attachBody +.. mujoco-include:: mjs_attach -Attach child body to a parent frame, return the attached body if success or NULL otherwise. - -.. _mjs_attachFrame: - -`mjs_attachFrame <#mjs_attachFrame>`__ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. mujoco-include:: mjs_attachFrame - -Attach child frame to a parent body, return the attached frame if success or NULL otherwise. - -.. _mjs_attachToSite: - -`mjs_attachToSite <#mjs_attachToSite>`__ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. mujoco-include:: mjs_attachToSite - -Attach child body to a parent site, return the attached body if success or NULL otherwise. - -.. _mjs_attachFrameToSite: - -`mjs_attachFrameToSite <#mjs_attachFrameToSite>`__ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. mujoco-include:: mjs_attachFrameToSite - -Attach child frame to a parent site, return the attached frame if success or NULL otherwise. +Attach child to a parent, return the attached element if success or NULL otherwise. .. _mjs_detachBody: diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 1c1854bb..1f0c6b99 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -175,11 +175,10 @@ replicating 200 times, suffixes will be ``000, 001, ...`` etc). All referencing and namespaced appropriately. Detailed examples of models using replicate can be found in the `model/replicate/ `__ directory. -There is a caveat concerning :ref:`keyframes` when using replicate. Since :ref:`mjs_attachFrame` is used to +There is a caveat concerning :ref:`keyframes` when using replicate. Since :ref:`mjs_attach` is used to self-attach multiple times the enclosed kinematic tree, if this tree contains further :ref:`attach` elements, keyframes will not be replicated nor namespaced by :ref:`replicate`, but they will be attached and -namespaced once by the innermost call of :ref:`mjs_attachFrame` or :ref:`mjs_attachBody`. See the limitations discussed -in :ref:`attach`. +namespaced once by the innermost call of :ref:`mjs_attach`. See the limitations discussed in :ref:`attach`. .. _replicate-count: diff --git a/doc/changelog.rst b/doc/changelog.rst index ad3f10cc..4af1fae5 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -10,6 +10,8 @@ Upcoming version (not yet released) - The default value of the flag for toggling :ref:`internal flex contacts` was changed from "true" to "false". This feature has proven to be counterintuitive for users. + - All of the attach functions (``mjs_attachBody``, ``mjs_attachFrame``, ``mjs_attachToSite``, + ``mjs_attachFrameToSite``) have been removed and replaced by a single function :ref:`mjs_attach`. General ^^^^^^^ diff --git a/doc/includes/references.h b/doc/includes/references.h index d3e7ba18..8d1cf9c8 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -3613,14 +3613,8 @@ void mju_threadPoolEnqueue(mjThreadPool* thread_pool, mjTask* task); void mju_threadPoolDestroy(mjThreadPool* thread_pool); void mju_defaultTask(mjTask* task); void mju_taskJoin(mjTask* task); -mjsBody* mjs_attachBody(mjsFrame* parent, const mjsBody* child, - const char* prefix, const char* suffix); -mjsFrame* mjs_attachFrame(mjsBody* parent, const mjsFrame* child, - const char* prefix, const char* suffix); -mjsBody* mjs_attachToSite(mjsSite* parent, const mjsBody* child, - const char* prefix, const char* suffix); -mjsFrame* mjs_attachFrameToSite(mjsSite* parent, const mjsFrame* child, - const char* prefix, const char* suffix); +mjsElement* mjs_attach(mjsElement* parent, const mjsElement* 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); diff --git a/doc/programming/modeledit.rst b/doc/programming/modeledit.rst index cec8f03d..ac74efdf 100644 --- a/doc/programming/modeledit.rst +++ b/doc/programming/modeledit.rst @@ -111,9 +111,10 @@ This framework introduces a powerful new feature: attaching and detaching model to power the :ref:`attach` an :ref:`replicate` meta-elements in MJCF. Attachment allows the user to move or copy a subtree from one model into another, while also copying or moving related referenced assets and referencing elements from outside the kinematic tree (e.g., actuators and sensors). Similarly, detaching a subtree will -remove all associated elements from the model. The default behavior is to move during attach. The user can select to -instead copy by passing the corresponding flag to ``mjs_setDeepCopy``. This flag is temporary set to true while parsing -XMLs. It is possible to :ref:`attach a body to a frame`: +remove all associated elements from the model. The default behavior is to move the child into the parent while +attaching, so subsequent changes to the child will also change the parent. Alternatively, the user can choose to make an +entirely new copy during attach using :ref:`mjs_setDeepCopy`. This flag is temporarily set to true while parsing XMLs. +It is possible to :ref:`attach a body to a frame`: .. code-block:: C @@ -121,29 +122,29 @@ XMLs. It is possible to :ref:`attach a body to a frame`: mjSpec* child = mj_makeSpec(); parent->compiler.degree = 0; child->compiler.degree = 1; - mjsFrame* frame = mjs_addFrame(mjs_findBody(parent, "world"), NULL); - mjsBody* body = mjs_addBody(mjs_findBody(child, "world"), NULL); - mjsBody* attached_body_1 = mjs_attachBody(frame, body, "attached-", "-1"); + mjsElement* frame = mjs_addFrame(mjs_findBody(parent, "world"), NULL)->element; + mjsElement* body = mjs_addBody(mjs_findBody(child, "world"), NULL)->element; + mjsBody* attached_body_1 = mjs_asBody(mjs_attach(frame, body, "attached-", "-1")); -or :ref:`attach a body to a site`: +or :ref:`attach a body to a site`: .. code-block:: C mjSpec* parent = mj_makeSpec(); mjSpec* child = mj_makeSpec(); - mjsSite* site = mjs_addSite(mjs_findBody(parent, "world"), NULL); - mjsBody* body = mjs_addBody(mjs_findBody(child, "world"), NULL); - mjsBody* attached_body_2 = mjs_attachToSite(site, body, "attached-", "-2"); + mjsElement* site = mjs_addSite(mjs_findBody(parent, "world"), NULL)->element; + mjsElement* body = mjs_addBody(mjs_findBody(child, "world"), NULL)->element; + mjsBody* attached_body_2 = mjs_asBody(mjs_attach(site, body, "attached-", "-2")); -or :ref:`attach a frame to a body`: +or :ref:`attach a frame to a body`: .. code-block:: C mjSpec* parent = mj_makeSpec(); mjSpec* child = mj_makeSpec(); - mjsBody* body = mjs_addBody(mjs_findBody(parent, "world"), NULL); - mjsFrame* frame = mjs_addFrame(mjs_findBody(child, "world"), NULL); - mjsFrame* attached_frame = mjs_attachFrame(body, frame, "attached-", "-1"); + mjsElement* body = mjs_addBody(mjs_findBody(parent, "world"), NULL)->element; + mjsElement* frame = mjs_addFrame(mjs_findBody(child, "world"), NULL)->element; + mjsFrame* attached_frame = mjs_asFrame(mjs_attach(body, frame, "attached-", "-1")); Note that in the above examples, the parent and child models have different values for ``compiler.degree``, corresponding to the :ref:`compiler/angle` attribute, specifying the units in which angles are diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index 84496910..c17c7e47 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -1414,21 +1414,9 @@ MJAPI void mju_taskJoin(mjTask* task); //---------------------------------- Attachment ---------------------------------------------------- -// Attach child body to a parent frame, return the attached body if success or NULL otherwise. -MJAPI mjsBody* mjs_attachBody(mjsFrame* parent, const mjsBody* child, - const char* prefix, const char* suffix); - -// Attach child frame to a parent body, return the attached frame if success or NULL otherwise. -MJAPI mjsFrame* mjs_attachFrame(mjsBody* parent, const mjsFrame* child, - const char* prefix, const char* suffix); - -// Attach child body to a parent site, return the attached body if success or NULL otherwise. -MJAPI mjsBody* mjs_attachToSite(mjsSite* parent, const mjsBody* child, - const char* prefix, const char* suffix); - -// Attach child frame to a parent site, return the attached frame if success or NULL otherwise. -MJAPI mjsFrame* mjs_attachFrameToSite(mjsSite* parent, const mjsFrame* child, - const char* prefix, const char* suffix); +// Attach child to a parent, return the attached element if success or NULL otherwise. +MJAPI mjsElement* mjs_attach(mjsElement* parent, const mjsElement* child, + const char* prefix, const char* suffix); // Delete body and descendants from mjSpec, remove all references, return 0 on success. MJAPI int mjs_detachBody(mjSpec* s, mjsBody* b); diff --git a/python/mujoco/introspect/functions.py b/python/mujoco/introspect/functions.py index d2955e80..9e61b828 100644 --- a/python/mujoco/introspect/functions.py +++ b/python/mujoco/introspect/functions.py @@ -9000,23 +9000,23 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), doc='Wait for a task to complete.', )), - ('mjs_attachBody', + ('mjs_attach', FunctionDecl( - name='mjs_attachBody', + name='mjs_attach', return_type=PointerType( - inner_type=ValueType(name='mjsBody'), + inner_type=ValueType(name='mjsElement'), ), parameters=( FunctionParameterDecl( name='parent', type=PointerType( - inner_type=ValueType(name='mjsFrame'), + inner_type=ValueType(name='mjsElement'), ), ), FunctionParameterDecl( name='child', type=PointerType( - inner_type=ValueType(name='mjsBody', is_const=True), + inner_type=ValueType(name='mjsElement', is_const=True), ), ), FunctionParameterDecl( @@ -9032,109 +9032,7 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), ), ), - doc='Attach child body to a parent frame, return the attached body if success or NULL otherwise.', # pylint: disable=line-too-long - )), - ('mjs_attachFrame', - FunctionDecl( - name='mjs_attachFrame', - return_type=PointerType( - inner_type=ValueType(name='mjsFrame'), - ), - parameters=( - FunctionParameterDecl( - name='parent', - type=PointerType( - inner_type=ValueType(name='mjsBody'), - ), - ), - FunctionParameterDecl( - name='child', - type=PointerType( - inner_type=ValueType(name='mjsFrame', is_const=True), - ), - ), - FunctionParameterDecl( - name='prefix', - type=PointerType( - inner_type=ValueType(name='char', is_const=True), - ), - ), - FunctionParameterDecl( - name='suffix', - type=PointerType( - inner_type=ValueType(name='char', is_const=True), - ), - ), - ), - doc='Attach child frame to a parent body, return the attached frame if success or NULL otherwise.', # pylint: disable=line-too-long - )), - ('mjs_attachToSite', - FunctionDecl( - name='mjs_attachToSite', - return_type=PointerType( - inner_type=ValueType(name='mjsBody'), - ), - parameters=( - FunctionParameterDecl( - name='parent', - type=PointerType( - inner_type=ValueType(name='mjsSite'), - ), - ), - FunctionParameterDecl( - name='child', - type=PointerType( - inner_type=ValueType(name='mjsBody', is_const=True), - ), - ), - FunctionParameterDecl( - name='prefix', - type=PointerType( - inner_type=ValueType(name='char', is_const=True), - ), - ), - FunctionParameterDecl( - name='suffix', - type=PointerType( - inner_type=ValueType(name='char', is_const=True), - ), - ), - ), - doc='Attach child body to a parent site, return the attached body if success or NULL otherwise.', # pylint: disable=line-too-long - )), - ('mjs_attachFrameToSite', - FunctionDecl( - name='mjs_attachFrameToSite', - return_type=PointerType( - inner_type=ValueType(name='mjsFrame'), - ), - parameters=( - FunctionParameterDecl( - name='parent', - type=PointerType( - inner_type=ValueType(name='mjsSite'), - ), - ), - FunctionParameterDecl( - name='child', - type=PointerType( - inner_type=ValueType(name='mjsFrame', is_const=True), - ), - ), - FunctionParameterDecl( - name='prefix', - type=PointerType( - inner_type=ValueType(name='char', is_const=True), - ), - ), - FunctionParameterDecl( - name='suffix', - type=PointerType( - inner_type=ValueType(name='char', is_const=True), - ), - ), - ), - doc='Attach child frame to a parent site, return the attached frame if success or NULL otherwise.', # pylint: disable=line-too-long + doc='Attach child to a parent, return the attached element if success or NULL otherwise.', # pylint: disable=line-too-long )), ('mjs_detachBody', FunctionDecl( diff --git a/python/mujoco/specs.cc b/python/mujoco/specs.cc index 64afef62..62f4efe8 100644 --- a/python/mujoco/specs.cc +++ b/python/mujoco/specs.cc @@ -540,7 +540,7 @@ PYBIND11_MODULE(_specs, m) { SetFrame(worldbody, mjOBJ_CAMERA, worldframe); const char* p = prefix.has_value() ? prefix.value().c_str() : ""; const char* s = suffix.has_value() ? suffix.value().c_str() : ""; - raw::MjsFrame* attached_frame = nullptr; + raw::MjsElement* attached_frame = nullptr; if (frame.has_value()) { raw::MjsFrame* frame_ptr = nullptr; try { @@ -560,11 +560,12 @@ PYBIND11_MODULE(_specs, m) { if (!parent_body) { throw pybind11::value_error("Frame does not have a parent body."); } - attached_frame = mjs_attachFrame(parent_body, worldframe, p, s); + attached_frame = + mjs_attach(parent_body->element, worldframe->element, p, s); if (!attached_frame) { throw pybind11::value_error(mjs_getError(self.ptr)); } - if (mjs_setFrame(attached_frame->element, frame_ptr) != 0) { + if (mjs_setFrame(attached_frame, frame_ptr) != 0) { throw pybind11::value_error(mjs_getError(self.ptr)); } } @@ -583,7 +584,8 @@ PYBIND11_MODULE(_specs, m) { throw pybind11::value_error( "Site spec does not match parent spec."); } - attached_frame = mjs_attachFrameToSite(site_ptr, worldframe, p, s); + attached_frame = + mjs_attach(site_ptr->element, worldframe->element, p, s); if (!attached_frame) { throw pybind11::value_error(mjs_getError(self.ptr)); } @@ -597,7 +599,7 @@ PYBIND11_MODULE(_specs, m) { self.assets[asset.first] = asset.second; } child.parent = &self; - return attached_frame; + return mjs_asFrame(attached_frame); }, py::arg("child"), py::arg("prefix") = py::none(), py::arg("suffix") = py::none(), py::arg("site") = py::none(), @@ -829,11 +831,11 @@ PYBIND11_MODULE(_specs, m) { std::optional& suffix) -> raw::MjsFrame* { const char* p = prefix.has_value() ? prefix.value().c_str() : ""; const char* s = suffix.has_value() ? suffix.value().c_str() : ""; - auto new_frame = mjs_attachFrame(&self, &frame, p, s); + auto new_frame = mjs_attach(self.element, frame.element, p, s); if (!new_frame) { throw pybind11::value_error(mjs_getError(mjs_getSpec(self.element))); } - return new_frame; + return mjs_asFrame(new_frame); }, py::arg("frame"), py::arg("prefix") = py::none(), py::arg("suffix") = py::none(), @@ -870,12 +872,12 @@ PYBIND11_MODULE(_specs, m) { std::optional& suffix) -> raw::MjsBody* { const char* p = prefix.has_value() ? prefix.value().c_str() : ""; const char* s = suffix.has_value() ? suffix.value().c_str() : ""; - auto new_body = mjs_attachBody(&self, &body, p, s); + auto new_body = mjs_attach(self.element, body.element, p, s); if (!new_body) { throw pybind11::value_error( mjs_getError(mjs_getSpec(self.element))); } - return new_body; + return mjs_asBody(new_body); }, py::arg("body"), py::arg("prefix") = py::none(), py::arg("suffix") = py::none(), @@ -953,12 +955,12 @@ PYBIND11_MODULE(_specs, m) { std::optional& suffix) -> raw::MjsBody* { const char* p = prefix.has_value() ? prefix.value().c_str() : ""; const char* s = suffix.has_value() ? suffix.value().c_str() : ""; - auto new_body = mjs_attachToSite(&self, &body, p, s); + auto new_body = mjs_attach(self.element, body.element, p, s); if (!new_body) { throw pybind11::value_error( mjs_getError(mjs_getSpec(self.element))); } - return new_body; + return mjs_asBody(new_body); }, py::arg("body"), py::arg("prefix") = py::none(), py::arg("suffix") = py::none(), diff --git a/src/user/user_api.cc b/src/user/user_api.cc index 646bb986..a3d90e3a 100644 --- a/src/user/user_api.cc +++ b/src/user/user_api.cc @@ -73,7 +73,7 @@ mjSpec* mj_copySpec(const mjSpec* s) { try { modelC = new mjCModel(*static_cast(s->element)); } catch (mjCError& e) { - mju_error("Failed to copy spec: %s", e.message); + modelC->SetError(e); return nullptr; } return &modelC->spec; @@ -121,100 +121,125 @@ mjModel* mj_compile(mjSpec* s, const mjVFS* vfs) { // attach body to a frame of the parent -mjsBody* mjs_attachBody(mjsFrame* parent, const mjsBody* child, - const char* prefix, const char* suffix) { - if (!parent) { - mju_error("parent frame is null"); - return nullptr; - } - mjCFrame* frame_parent = static_cast(parent->element); - mjCBody* child_body = static_cast(child->element); +static mjsElement* attachBody(mjCFrame* parent, const mjCBody* child, + const char* prefix, const char* suffix) { try { - *frame_parent += std::string(prefix) + *child_body + std::string(suffix); + *parent += std::string(prefix) + *(mjCBody*)child + std::string(suffix); } catch (mjCError& e) { - frame_parent->model->SetError(e); + parent->model->SetError(e); return nullptr; } - mjsBody* attached_body = frame_parent->last_attached; - frame_parent->last_attached = nullptr; - return attached_body; + mjsBody* attached_body = parent->last_attached; + parent->last_attached = nullptr; + return attached_body->element; } // attach frame to a parent body -mjsFrame* mjs_attachFrame(mjsBody* parent, const mjsFrame* child, - const char* prefix, const char* suffix) { - if (!parent) { - mju_error("parent body is null"); - return nullptr; - } - mjCBody* body_parent = static_cast(parent->element); - mjCFrame* child_frame = static_cast(child->element); +static mjsElement* attachFrame(mjCBody* parent, const mjCFrame* child, + const char* prefix, const char* suffix) { try { - *body_parent += std::string(prefix) + *child_frame + std::string(suffix); + *parent += std::string(prefix) + *(mjCFrame*)child + std::string(suffix); } catch (mjCError& e) { - body_parent->model->SetError(e); + parent->model->SetError(e); return nullptr; } - mjsFrame* attached_frame = body_parent->last_attached; - body_parent->last_attached = nullptr; - return attached_frame; + mjsFrame* attached_frame = parent->last_attached; + parent->last_attached = nullptr; + return attached_frame->element; } // attach child body to a parent site -mjsBody* mjs_attachToSite(mjsSite* parent, const mjsBody* child, - const char* prefix, const char* suffix) { - if (!parent) { - mju_error("parent site is null"); - return nullptr; - } - mjSpec* spec = mjs_getSpec(parent->element); - mjCSite* site = static_cast(parent->element); - mjCBody* body = site->Body(); - mjCFrame* frame = body->AddFrame(site->frame); +static mjsElement* attachToSite(mjCSite* parent, const mjCBody* child, + const char* prefix, const char* suffix) { + mjSpec* spec = mjs_getSpec(parent->spec.element); + mjCBody* body = parent->Body(); + mjCFrame* frame = body->AddFrame(parent->frame); frame->SetParent(body); - frame->spec.pos[0] = site->spec.pos[0]; - frame->spec.pos[1] = site->spec.pos[1]; - frame->spec.pos[2] = site->spec.pos[2]; - frame->spec.quat[0] = site->spec.quat[0]; - frame->spec.quat[1] = site->spec.quat[1]; - frame->spec.quat[2] = site->spec.quat[2]; - frame->spec.quat[3] = site->spec.quat[3]; + frame->spec.pos[0] = parent->spec.pos[0]; + frame->spec.pos[1] = parent->spec.pos[1]; + frame->spec.pos[2] = parent->spec.pos[2]; + frame->spec.quat[0] = parent->spec.quat[0]; + frame->spec.quat[1] = parent->spec.quat[1]; + frame->spec.quat[2] = parent->spec.quat[2]; + frame->spec.quat[3] = parent->spec.quat[3]; mjs_resolveOrientation(frame->spec.quat, spec->compiler.degree, - spec->compiler.eulerseq, &site->spec.alt); - return mjs_attachBody(&frame->spec, child, prefix, suffix); + spec->compiler.eulerseq, &parent->spec.alt); + return attachBody(frame, child, prefix, suffix); } // attach child frame to a parent site -mjsFrame* mjs_attachFrameToSite(mjsSite* parent, const mjsFrame* child, - const char* prefix, const char* suffix) { +static mjsElement* attachFrameToSite(mjCSite* parent, const mjCFrame* child, + const char* prefix, const char* suffix) { + mjSpec* spec = mjs_getSpec(parent->spec.element); + mjCBody* body = parent->Body(); + mjCFrame* frame = body->AddFrame(parent->frame); + frame->SetParent(body); + frame->spec.pos[0] = parent->spec.pos[0]; + frame->spec.pos[1] = parent->spec.pos[1]; + frame->spec.pos[2] = parent->spec.pos[2]; + frame->spec.quat[0] = parent->spec.quat[0]; + frame->spec.quat[1] = parent->spec.quat[1]; + frame->spec.quat[2] = parent->spec.quat[2]; + frame->spec.quat[3] = parent->spec.quat[3]; + mjs_resolveOrientation(frame->spec.quat, spec->compiler.degree, + spec->compiler.eulerseq, &parent->spec.alt); + + mjsElement* attached_frame = attachFrame(body, child, prefix, suffix); + mjs_setFrame(attached_frame, &frame->spec); + return attached_frame; +} + + +mjsElement* mjs_attach(mjsElement* parent, const mjsElement* child, + const char* prefix, const char* suffix) { if (!parent) { - mju_error("parent site is null"); + mju_error("parent element is null"); return nullptr; } - mjSpec* spec = mjs_getSpec(parent->element); - mjCSite* site = static_cast(parent->element); - mjCBody* body = site->Body(); - mjCFrame* frame = body->AddFrame(site->frame); - frame->SetParent(body); - frame->spec.pos[0] = site->spec.pos[0]; - frame->spec.pos[1] = site->spec.pos[1]; - frame->spec.pos[2] = site->spec.pos[2]; - frame->spec.quat[0] = site->spec.quat[0]; - frame->spec.quat[1] = site->spec.quat[1]; - frame->spec.quat[2] = site->spec.quat[2]; - frame->spec.quat[3] = site->spec.quat[3]; - mjs_resolveOrientation(frame->spec.quat, spec->compiler.degree, - spec->compiler.eulerseq, &site->spec.alt); - - mjsFrame* attached_frame = mjs_attachFrame(&body->spec, child, prefix, suffix); - mjs_setFrame(attached_frame->element, &frame->spec); - return attached_frame; + if (!child) { + mju_error("child element is null"); + return nullptr; + } + mjCModel* model = static_cast(mjs_getSpec(parent)->element); + switch (parent->elemtype) { + case mjOBJ_FRAME: + if (child->elemtype == mjOBJ_BODY) { + return attachBody(static_cast(parent), + static_cast(child), prefix, suffix); + } else { + model->SetError(mjCError(0, "child element is not a body")); + return nullptr; + } + case mjOBJ_BODY: + if (child->elemtype == mjOBJ_FRAME) { + return attachFrame(static_cast(parent), + static_cast(child), prefix, suffix); + } else { + model->SetError(mjCError(0, "child element is not a frame")); + return nullptr; + } + case mjOBJ_SITE: + if (child->elemtype == mjOBJ_BODY) { + return attachToSite(static_cast(parent), + static_cast(child), prefix, suffix); + } else if (child->elemtype == mjOBJ_FRAME) { + return attachFrameToSite(static_cast(parent), + static_cast(child), prefix, suffix); + } else { + model->SetError(mjCError(0, "child element is not a body or frame")); + return nullptr; + } + default: + model->SetError(mjCError(0, "parent element is not a frame, body or site")); + return nullptr; + } + return nullptr; } diff --git a/src/user/user_api.h b/src/user/user_api.h index 01aa9856..d01b5172 100644 --- a/src/user/user_api.h +++ b/src/user/user_api.h @@ -69,21 +69,9 @@ MJAPI int mjs_setDeepCopy(mjSpec* s, int deepcopy); //---------------------------------- Attachment ---------------------------------------------------- -// Attach child body to a parent frame, return the attached body if success or NULL otherwise. -MJAPI mjsBody* mjs_attachBody(mjsFrame* parent, const mjsBody* child, - const char* prefix, const char* suffix); - -// Attach child frame to a parent body, return the attached frame if success or NULL otherwise. -MJAPI mjsFrame* mjs_attachFrame(mjsBody* parent, const mjsFrame* child, - const char* prefix, const char* suffix); - -// Attach child body to a parent site, return the attached body if success or NULL otherwise. -MJAPI mjsBody* mjs_attachToSite(mjsSite* parent, const mjsBody* child, - const char* prefix, const char* suffix); - -// Attach child frame to a parent site, return the attached frame if success or NULL otherwise. -MJAPI mjsFrame* mjs_attachFrameToSite(mjsSite* parent, const mjsFrame* child, - const char* prefix, const char* suffix); +// Attach child to a parent, return the attached element if success or NULL otherwise. +MJAPI mjsElement* mjs_attach(mjsElement* parent, const mjsElement* child, + const char* prefix, const char* suffix); // Detach body from mjSpec, remove all references and delete the body, return 0 on success. MJAPI int mjs_detachBody(mjSpec* s, mjsBody* b); diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 7fda10cd..94c61755 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -3566,7 +3566,7 @@ void mjXReader::Body(XMLElement* section, mjsBody* body, mjsFrame* frame, UpdateString(suffix, count, i); // attach to parent - if (!mjs_attachFrame(body, pframe, /*prefix=*/"", suffix.c_str())) { + if (!mjs_attach(body->element, pframe->element, /*prefix=*/"", suffix.c_str())) { throw mjXError(elem, mjs_getError(spec)); } } @@ -3642,7 +3642,7 @@ void mjXReader::Body(XMLElement* section, mjsBody* body, mjsFrame* frame, if (!child) { throw mjXError(elem, "could not find body '%s''%s'", body_name.c_str()); } - if (!mjs_attachBody(pframe, child, prefix.c_str(), "")) { + if (!mjs_attach(pframe->element, child->element, prefix.c_str(), "")) { throw mjXError(elem, mjs_getError(spec)); } } else { diff --git a/test/user/user_api_test.cc b/test/user/user_api_test.cc index d07f0388..c9bc6bd1 100644 --- a/test/user/user_api_test.cc +++ b/test/user/user_api_test.cc @@ -279,20 +279,23 @@ TEST_F(PluginTest, AttachPlugin) { EXPECT_THAT(body_1, NotNull()); mjsFrame* attachment_frame = mjs_addFrame(body_1, 0); EXPECT_THAT(attachment_frame, NotNull()); - mjs_attachBody(attachment_frame, mjs_findBody(spec_1, "body"), "child-", ""); + mjs_attach(attachment_frame->element, mjs_findBody(spec_1, "body")->element, + "child-", ""); mjModel* model_1 = mj_compile(parent, nullptr); EXPECT_THAT(model_1, NotNull()); EXPECT_THAT(model_1->nbody, 3); // attach it a second time to test namespacing and compile ASSERT_THAT(spec_2, NotNull()) << err.data(); - mjs_attachBody(attachment_frame, mjs_findBody(spec_2, "body"), "copy-", ""); + mjs_attach(attachment_frame->element, mjs_findBody(spec_2, "body")->element, + "copy-", ""); mjModel* model_2 = mj_compile(parent, nullptr); EXPECT_THAT(model_2, NotNull()); EXPECT_THAT(model_2->nbody, 4); // attach a body not referencing the plugin and compile - mjs_attachBody(attachment_frame, mjs_findBody(spec_3, "empty"), "empty-", ""); + mjs_attach(attachment_frame->element, mjs_findBody(spec_3, "empty")->element, + "empty-", ""); mjModel* model_3 = mj_compile(parent, nullptr); EXPECT_THAT(model_3, NotNull()); EXPECT_THAT(model_3->nbody, 5); @@ -314,9 +317,9 @@ TEST_F(PluginTest, DetachPlugin) { ASSERT_THAT(child, NotNull()) << err.data(); // attach a body referencing the plugin to the frame - mjsFrame* frame = mjs_addFrame(mjs_findBody(parent, "world"), 0); - mjsBody* body = mjs_findBody(child, "body"); - EXPECT_THAT(mjs_attachBody(frame, body, "child-", ""), NotNull()); + mjsElement* frame = mjs_addFrame(mjs_findBody(parent, "world"), 0)->element; + mjsElement* body = mjs_findBody(child, "body")->element; + EXPECT_THAT(mjs_attach(frame, body, "child-", ""), NotNull()); // detach the body and compile mjsBody* body_to_detach = mjs_findBody(parent, "child-body"); @@ -372,7 +375,8 @@ TEST_F(PluginTest, AttachExplicitPlugin) { mjsFrame* attachment_frame = mjs_addFrame(body_parent, 0); EXPECT_THAT(attachment_frame, NotNull()); - mjs_attachBody(attachment_frame, mjs_findBody(child, "body"), "child-", ""); + mjs_attach(attachment_frame->element, mjs_findBody(child, "body")->element, + "child-", ""); mjModel* model = mj_compile(parent, nullptr); EXPECT_THAT(model, NotNull()); EXPECT_THAT(model->nplugin, 1); @@ -937,7 +941,8 @@ TEST_F(MujocoTest, AttachSame) { EXPECT_THAT(body, NotNull()); // attach child to parent frame - mjsBody* attached = mjs_attachBody(frame, body, "attached-", "-1"); + mjsBody* attached = + mjs_asBody(mjs_attach(frame->element, body->element, "attached-", "-1")); EXPECT_THAT(attached, mjs_findBody(parent, "attached-body-1")); // check that the spec was not copied @@ -1073,7 +1078,8 @@ TEST_F(MujocoTest, AttachDifferent) { EXPECT_THAT(body, NotNull()); // attach child to parent frame - mjsBody* attached = mjs_attachBody(frame, body, "attached-", "-1"); + mjsBody* attached = + mjs_asBody(mjs_attach(frame->element, body->element, "attached-", "-1")); EXPECT_THAT(attached, mjs_findBody(parent, "attached-body-1")); // check that the spec was copied @@ -1212,7 +1218,8 @@ TEST_F(MujocoTest, AttachFrame) { EXPECT_THAT(frame, NotNull()); // attach child frame to parent body - mjsFrame* attached = mjs_attachFrame(body, frame, "attached-", "-1"); + mjsFrame* attached = + mjs_asFrame(mjs_attach(body->element, frame->element, "attached-", "-1")); EXPECT_THAT(attached, mjs_findFrame(parent, "attached-pframe-1")); // check that the spec was copied @@ -1286,7 +1293,7 @@ TEST_F(MujocoTest, AttachCompiled) { // attach child body to the frame mjsBody* to_attach = mjs_findBody(child, "base"); EXPECT_THAT(to_attach, NotNull()) << mjs_getError(child); - mjs_attachBody(frame, to_attach, "", ""); + mjs_attach(frame->element, to_attach->element, "", ""); // check that attached model can be compiled mjModel* m_attached = mj_compile(parent, 0); @@ -1433,7 +1440,8 @@ TEST_F(MujocoTest, AttachToSite) { EXPECT_THAT(site, NotNull()); mjsBody* body = mjs_findBody(child, "sphere"); EXPECT_THAT(body, NotNull()); - mjsBody* attached = mjs_attachToSite(site, body, "attached-", "-1"); + mjsBody* attached = + mjs_asBody(mjs_attach(site->element, body->element, "attached-", "-1")); EXPECT_THAT(attached, NotNull()); mjModel* model = mj_compile(parent, 0); @@ -1498,7 +1506,8 @@ TEST_F(MujocoTest, AttachFrameToSite) { EXPECT_THAT(site, NotNull()); mjsFrame* frame = mjs_findFrame(child, "frame"); EXPECT_THAT(frame, NotNull()); - mjsFrame* attached = mjs_attachFrameToSite(site, frame, "attached-", "-1"); + mjsFrame* attached = + mjs_asFrame(mjs_attach(site->element, frame->element, "attached-", "-1")); EXPECT_THAT(attached, NotNull()); mjModel* model = mj_compile(parent, 0); @@ -1573,7 +1582,8 @@ TEST_F(MujocoTest, BodyToFrame) { EXPECT_THAT(frame, NotNull()); mjsBody* body = mjs_findBody(child1, "sphere"); EXPECT_THAT(body, NotNull()); - mjsBody* attached = mjs_attachBody(frame, body, "attached-", "-1"); + mjsBody* attached = + mjs_asBody(mjs_attach(frame->element, body->element, "attached-", "-1")); EXPECT_THAT(attached, NotNull()); mjModel* model1 = mj_compile(parent, 0); EXPECT_THAT(model1, NotNull()); @@ -1581,7 +1591,8 @@ TEST_F(MujocoTest, BodyToFrame) { // attach the world to the same frame and convert it to a frame mjsBody* world = mjs_findBody(child2, "world"); EXPECT_THAT(world, NotNull()); - mjsBody* child_world = mjs_attachBody(frame, world, "attached-", "-2"); + mjsBody* child_world = + mjs_asBody(mjs_attach(frame->element, world->element, "attached-", "-2")); EXPECT_THAT(child_world, NotNull()); mjsFrame* frame_world = mjs_bodyToFrame(&child_world); EXPECT_THAT(frame_world, NotNull()); @@ -1662,7 +1673,8 @@ TEST_F(MujocoTest, AttachSpecToSite) { mjs_setFrame(mjs_firstChild(world, mjOBJ_CAMERA, 0), frame); // attach the entire spec to the site - mjsFrame* worldframe = mjs_attachFrameToSite(site, frame, "attached-", "-1"); + mjsFrame* worldframe = + mjs_asFrame(mjs_attach(site->element, frame->element, "attached-", "-1")); EXPECT_THAT(worldframe, NotNull()); // compile and compare @@ -1739,7 +1751,8 @@ TEST_F(MujocoTest, AttachSpecToBody) { mjs_setFrame(mjs_firstChild(world, mjOBJ_CAMERA, 0), frame); // attach the entire spec to the site - mjsFrame* worldframe = mjs_attachFrame(body, frame, "attached-", "-1"); + mjsFrame* worldframe = + mjs_asFrame(mjs_attach(body->element, frame->element, "attached-", "-1")); EXPECT_THAT(worldframe, NotNull()); worldframe->pos[0] = 1; worldframe->pos[1] = 2; @@ -1937,8 +1950,8 @@ TEST_F(MujocoTest, RecompileAttach) { mjSpec* child2 = mj_parseXMLString(xml, 0, er.data(), er.size()); EXPECT_THAT(child2, NotNull()); - mjsFrame* frame1 = mjs_addFrame(mjs_findBody(parent, "world"), 0); - mjs_attachBody(frame1, mjs_findBody(child1, "body"), "child-", "-1"); + mjsElement* frame1 = mjs_addFrame(mjs_findBody(parent, "world"), 0)->element; + mjs_attach(frame1, mjs_findBody(child1, "body")->element, "child-", "-1"); mjModel* model = mj_compile(parent, 0); EXPECT_THAT(model, NotNull()); @@ -1950,8 +1963,8 @@ TEST_F(MujocoTest, RecompileAttach) { mj_step(model, data); } - mjsFrame* frame2 = mjs_addFrame(mjs_findBody(parent, "world"), 0); - mjs_attachBody(frame2, mjs_findBody(child2, "body"), "child-", "-2"); + mjsElement* frame2 = mjs_addFrame(mjs_findBody(parent, "world"), 0)->element; + mjs_attach(frame2, mjs_findBody(child2, "body")->element, "child-", "-2"); EXPECT_EQ(mj_recompile(parent, 0, model, data), 0); EXPECT_THAT(model, NotNull()); @@ -2003,8 +2016,8 @@ TEST_F(MujocoTest, AttachMocap) { mjsBody* world = mjs_findBody(spec, "world"); EXPECT_THAT(world, NotNull()); - mjsFrame* frame = mjs_addFrame(world, NULL); - mjs_attachBody(frame, body, "attached-", "-1"); + mjsElement* frame = mjs_addFrame(world, NULL)->element; + mjs_attach(frame, body->element, "attached-", "-1"); mjsBody* attached_body = mjs_findBody(spec, "attached-mocap-1"); EXPECT_THAT(attached_body, NotNull()); @@ -2083,7 +2096,7 @@ TEST_F(MujocoTest, AttachUnnamedAssets) { geom->type = mjGEOM_MESH; mjSpec* spec = mj_makeSpec(); - mjs_attachFrame(mjs_findBody(spec, "world"), frame, "_", ""); + mjs_attach(mjs_findBody(spec, "world")->element, frame->element, "_", ""); mjModel* model = mj_compile(spec, vfs.get()); EXPECT_THAT(model, NotNull()); @@ -2229,8 +2242,8 @@ void AttachNestedKeyframe(bool compile) { mjs_setDeepCopy(child, true); // attach gchild to child - mjs_attachBody(mjs_findFrame(child, "frame"), - mjs_findBody(gchild, "body"), "gchild-", ""); + mjs_attach(mjs_findFrame(child, "frame")->element, + mjs_findBody(gchild, "body")->element, "gchild-", ""); // compile required before further attachment mjModel* m_child = compile ? mj_compile(child, 0) : nullptr; @@ -2243,8 +2256,8 @@ void AttachNestedKeyframe(bool compile) { }; // attach child to parent - mjs_attachBody(mjs_findFrame(parent, "frame"), - mjs_findBody(child, "body"), "child-", ""); + mjs_attach(mjs_findFrame(parent, "frame")->element, + mjs_findBody(child, "body")->element, "child-", ""); EXPECT_THAT(warning, HasSubstr(compile ? "" : "model has pending keyframes")); @@ -2305,11 +2318,11 @@ TEST_F(MujocoTest, RepeatedAttachKeyframe) { EXPECT_THAT(child, NotNull()) << er.data(); mjsBody* body_1 = mjs_findBody(parent, "body"); - mjsFrame* attachment_frame = mjs_addFrame(body_1, 0); - mjs_attachBody(attachment_frame, mjs_findBody(child, "b1"), "b1-", ""); + mjsElement* attachment_frame = mjs_addFrame(body_1, 0)->element; + mjs_attach(attachment_frame, mjs_findBody(child, "b1")->element, "b1-", ""); mjModel* model_1 = mj_compile(parent, 0); EXPECT_THAT(model_1, NotNull()); - mjs_attachBody(attachment_frame, mjs_findBody(child, "b2"), "b2-", ""); + mjs_attach(attachment_frame, mjs_findBody(child, "b2")->element, "b2-", ""); mjModel* model_2 = mj_compile(parent, 0); EXPECT_THAT(model_2, NotNull()); @@ -2374,8 +2387,8 @@ TEST_F(MujocoTest, ResizeParentKeyframe) { mjSpec* child = mj_parseXMLString(xml_child, 0, er.data(), er.size()); EXPECT_THAT(child, NotNull()) << er.data(); - mjs_attachBody(mjs_findFrame(parent, "frame"), mjs_findBody(child, "body"), - "child-", ""); + mjs_attach(mjs_findFrame(parent, "frame")->element, + mjs_findBody(child, "body")->element, "child-", ""); mjModel* model = mj_compile(parent, 0); EXPECT_THAT(model, NotNull()); @@ -2466,12 +2479,10 @@ TEST_F(MujocoTest, DifferentUnitsAllowed) { mjSpec* child = mj_parseXMLString(child_xml, 0, error.data(), error.size()); mjSpec* spec = mj_parseXMLString(parent_xml, 0, error.data(), error.size()); ASSERT_THAT(spec, NotNull()) << error.data(); - mjs_attachBody(mjs_findFrame(child, "frame"), - mjs_findBody(gchild, "gchild"), - "gchild_", ""); - mjs_attachBody(mjs_findFrame(spec, "frame"), - mjs_findBody(child, "child"), - "child_", ""); + mjs_attach(mjs_findFrame(child, "frame")->element, + mjs_findBody(gchild, "gchild")->element, "gchild_", ""); + mjs_attach(mjs_findFrame(spec, "frame")->element, + mjs_findBody(child, "child")->element, "child_", ""); mjModel* model = mj_compile(spec, 0); EXPECT_THAT(model, NotNull()); @@ -2544,7 +2555,8 @@ TEST_F(MujocoTest, DifferentOptionsInAttachedFrame) { EXPECT_THAT(world, NotNull()); mjsFrame* child_frame = mjs_findFrame(child, "child"); EXPECT_THAT(child_frame, NotNull()); - mjsFrame* attached_frame = mjs_attachFrame(world, child_frame, "child-", ""); + mjsElement* attached_frame = + mjs_attach(world->element, child_frame->element, "child-", ""); EXPECT_THAT(attached_frame, NotNull()); // wrap the child frame in the parent frame and compile @@ -2659,8 +2671,9 @@ TEST_F(MujocoTest, ApplyNameSpaceToDefaults) { mjSpec* parent = mj_parseXMLString(xml_p, 0, err.data(), err.size()); EXPECT_THAT(parent, NotNull()) << err.data(); - mjsBody* attached = mjs_attachBody(mjs_findFrame(parent, "parent"), - mjs_findBody(child, "body"), "child-", ""); + mjsElement* attached = + mjs_attach(mjs_findFrame(parent, "parent")->element, + mjs_findBody(child, "body")->element, "child-", ""); EXPECT_THAT(attached, NotNull()); mjModel* model = mj_compile(parent, vfs.get()); @@ -2774,7 +2787,7 @@ TEST_F(MujocoTest, ErrorWhenCompilingOrphanedSpec) { EXPECT_THAT(body, NotNull()); mjsFrame* frame = mjs_addFrame(mjs_findBody(parent, "world"), nullptr); EXPECT_THAT(frame, NotNull()); - mjs_attachBody(frame, body, "child-", ""); + mjs_attach(frame->element, body->element, "child-", ""); mj_deleteSpec(parent); mjModel* model = mj_compile(child, 0); EXPECT_THAT(model, IsNull()); diff --git a/test/user/user_model_test.cc b/test/user/user_model_test.cc index 9f85669e..8b00753d 100644 --- a/test/user/user_model_test.cc +++ b/test/user/user_model_test.cc @@ -623,7 +623,7 @@ TEST_F(MujocoTest, Modeldir) { mjSpec* spec = mj_makeSpec(); mjs_setDeepCopy(spec, true); mjs_setString(spec->meshdir, "asset"); - mjs_attachFrame(mjs_findBody(spec, "world"), frame, "_", ""); + mjs_attach(mjs_findBody(spec, "world")->element, frame->element, "_", ""); mjModel* model = mj_compile(spec, vfs.get()); EXPECT_THAT(model, NotNull()); From e5912c3ce499d28fd9e26bebbb51f6233a040163 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 1 Apr 2025 08:11:08 -0700 Subject: [PATCH 030/191] Use tolerance to compare quaternion components in `UserObjectsTest.Inertial` Fixes #2515 PiperOrigin-RevId: 742704324 Change-Id: Ib26891e43128e66d2ac1840ed574285781fb12a5 --- test/user/user_objects_test.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/user/user_objects_test.cc b/test/user/user_objects_test.cc index efd51a99..b9f13cd4 100644 --- a/test/user/user_objects_test.cc +++ b/test/user/user_objects_test.cc @@ -2508,7 +2508,8 @@ TEST_F(UserObjectsTest, Inertial) { mjtNum quat[4]; const mjtNum euler[3] = {3, 4, 5}; mju_euler2Quat(quat, euler, "xyz"); - EXPECT_EQ(AsVector(m->body_iquat+4, 4), AsVector(quat, 4)); + EXPECT_THAT(AsVector(m->body_iquat+4, 4), + Pointwise(DoubleNear(1e-8), AsVector(quat, 4))); EXPECT_EQ(m->body_mass[2], 2); EXPECT_THAT(AsVector(m->body_ipos+6, 3), ElementsAre(1, 2, 3)); From b839fe79e19f797589474db04735d38d3298bbb5 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Tue, 1 Apr 2025 09:50:42 -0700 Subject: [PATCH 031/191] Allow to attach an entire mjSpec in mjs_attach. PiperOrigin-RevId: 742737713 Change-Id: Ie172568b8c35232e1f13d75aebee20346e76e95b --- doc/XMLreference.rst | 5 ++-- python/mujoco/specs.cc | 33 ++------------------------ src/user/user_api.cc | 45 +++++++++++++++++++++++++++++++++++- src/xml/xml_native_reader.cc | 22 +++++++++++------- 4 files changed, 63 insertions(+), 42 deletions(-) diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 1f0c6b99..4e5b4393 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -3716,8 +3716,9 @@ all attachments will appear in the saved XML file. .. _body-attach-body: -:at:`body`: :at-val:`string, required` - Name of the body in the sub-model to attach here. The body and its subtree will be attached. +:at:`body`: :at-val:`string, optional` + Name of the body in the sub-model to attach here. The body and its subtree will be attached. If this attribute is not + specified, the contents of the world body will be attached in a new :ref:`frame`. .. _body-attach-prefix: diff --git a/python/mujoco/specs.cc b/python/mujoco/specs.cc index 62f4efe8..a87fcf4e 100644 --- a/python/mujoco/specs.cc +++ b/python/mujoco/specs.cc @@ -269,16 +269,6 @@ py::list FindAllImpl(raw::MjsBody& body, mjtObj objtype, bool recursive) { return list; // list of pointers, so they can be copied } -void SetFrame(raw::MjsBody* body, mjtObj objtype, raw::MjsFrame* frame) { - mjsElement* el = mjs_firstChild(body, objtype, 0); - while (el) { - if (frame->element != el && mjs_getFrame(el) == nullptr) { - mjs_setFrame(el, frame); - } - el = mjs_nextChild(body, el, 0); - } -} - PYBIND11_MODULE(_specs, m) { auto structs_m = py::module::import("mujoco._structs"); py::function mjmodel_from_raw_ptr = @@ -526,18 +516,6 @@ PYBIND11_MODULE(_specs, m) { throw pybind11::value_error( "Only one of frame or site can be specified."); } - auto worldbody = mjs_findBody(child.ptr, "world"); - if (!worldbody) { - throw pybind11::value_error("Child does not have a world body."); - } - auto worldframe = mjs_addFrame(worldbody, nullptr); - SetFrame(worldbody, mjOBJ_BODY, worldframe); - SetFrame(worldbody, mjOBJ_SITE, worldframe); - SetFrame(worldbody, mjOBJ_FRAME, worldframe); - SetFrame(worldbody, mjOBJ_JOINT, worldframe); - SetFrame(worldbody, mjOBJ_GEOM, worldframe); - SetFrame(worldbody, mjOBJ_LIGHT, worldframe); - SetFrame(worldbody, mjOBJ_CAMERA, worldframe); const char* p = prefix.has_value() ? prefix.value().c_str() : ""; const char* s = suffix.has_value() ? suffix.value().c_str() : ""; raw::MjsElement* attached_frame = nullptr; @@ -556,18 +534,11 @@ PYBIND11_MODULE(_specs, m) { throw pybind11::value_error( "Frame spec does not match parent spec."); } - raw::MjsBody* parent_body = mjs_getParent(frame_ptr->element); - if (!parent_body) { - throw pybind11::value_error("Frame does not have a parent body."); - } attached_frame = - mjs_attach(parent_body->element, worldframe->element, p, s); + mjs_attach(frame_ptr->element, child.ptr->element, p, s); if (!attached_frame) { throw pybind11::value_error(mjs_getError(self.ptr)); } - if (mjs_setFrame(attached_frame, frame_ptr) != 0) { - throw pybind11::value_error(mjs_getError(self.ptr)); - } } if (site.has_value()) { raw::MjsSite* site_ptr = nullptr; @@ -585,7 +556,7 @@ PYBIND11_MODULE(_specs, m) { "Site spec does not match parent spec."); } attached_frame = - mjs_attach(site_ptr->element, worldframe->element, p, s); + mjs_attach(site_ptr->element, child.ptr->element, p, s); if (!attached_frame) { throw pybind11::value_error(mjs_getError(self.ptr)); } diff --git a/src/user/user_api.cc b/src/user/user_api.cc index a3d90e3a..6962493f 100644 --- a/src/user/user_api.cc +++ b/src/user/user_api.cc @@ -119,6 +119,18 @@ mjModel* mj_compile(mjSpec* s, const mjVFS* vfs) { } +// set frame for all elements of a body +static void SetFrame(mjsBody* body, mjtObj objtype, mjsFrame* frame) { + mjsElement* el = mjs_firstChild(body, objtype, 0); + while (el) { + if (frame->element != el && mjs_getFrame(el) == nullptr) { + mjs_setFrame(el, frame); + } + el = mjs_nextChild(body, el, 0); + } +} + + // attach body to a frame of the parent static mjsElement* attachBody(mjCFrame* parent, const mjCBody* child, @@ -207,13 +219,44 @@ mjsElement* mjs_attach(mjsElement* parent, const mjsElement* child, return nullptr; } mjCModel* model = static_cast(mjs_getSpec(parent)->element); + if (child->elemtype == mjOBJ_MODEL) { + mjCModel* child_model = static_cast((mjsElement*)child); + mjsBody* worldbody = mjs_findBody(&child_model->spec, "world"); + if (!worldbody) { + model->SetError(mjCError(0, "Child does not have a world body.")); + return nullptr; + } + mjsFrame* worldframe = mjs_addFrame(worldbody, nullptr); + SetFrame(worldbody, mjOBJ_BODY, worldframe); + SetFrame(worldbody, mjOBJ_SITE, worldframe); + SetFrame(worldbody, mjOBJ_FRAME, worldframe); + SetFrame(worldbody, mjOBJ_JOINT, worldframe); + SetFrame(worldbody, mjOBJ_GEOM, worldframe); + SetFrame(worldbody, mjOBJ_LIGHT, worldframe); + SetFrame(worldbody, mjOBJ_CAMERA, worldframe); + child = worldframe->element; + } switch (parent->elemtype) { case mjOBJ_FRAME: if (child->elemtype == mjOBJ_BODY) { return attachBody(static_cast(parent), static_cast(child), prefix, suffix); + } else if (child->elemtype == mjOBJ_FRAME) { + mjsBody* parent_body = mjs_getParent(parent); + if (!parent_body) { + model->SetError(mjCError(0, "Frame does not have a parent body.")); + return nullptr; + } + mjCFrame* frame = static_cast(parent); + mjsElement* attached_frame = + attachFrame(static_cast(parent_body->element), + static_cast(child), prefix, suffix); + if (mjs_setFrame(attached_frame, &frame->spec)) { + return nullptr; + } + return attached_frame; } else { - model->SetError(mjCError(0, "child element is not a body")); + model->SetError(mjCError(0, "child element is not a body or frame")); return nullptr; } case mjOBJ_BODY: diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 94c61755..35b1837c 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -3627,27 +3627,33 @@ void mjXReader::Body(XMLElement* section, mjsBody* body, mjsFrame* frame, else if (name == "attach") { string model_name, body_name, prefix; ReadAttrTxt(elem, "model", model_name, /*required=*/true); - ReadAttrTxt(elem, "body", body_name, /*required=*/true); + ReadAttrTxt(elem, "body", body_name, /*required=*/false); ReadAttrTxt(elem, "prefix", prefix, /*required=*/true); - mjsBody* child = mjs_findBody(spec, (prefix+body_name).c_str()); + mjsBody* child_body = mjs_findBody(spec, (prefix+body_name).c_str()); mjsFrame* pframe = frame ? frame : mjs_addFrame(body, nullptr); - if (!child) { + if (!child_body) { mjSpec* asset = mjs_findSpec(spec, model_name.c_str()); if (!asset) { throw mjXError(elem, "could not find model '%s'", model_name.c_str()); } - child = mjs_findBody(asset, body_name.c_str()); - if (!child) { - throw mjXError(elem, "could not find body '%s''%s'", body_name.c_str()); + mjsElement* child; + if (body_name.empty()) { + child = asset->element; + } else { + child_body = mjs_findBody(asset, body_name.c_str()); + if (!child_body) { + throw mjXError(elem, "could not find body '%s''%s'", body_name.c_str()); + } + child = child_body->element; } - if (!mjs_attach(pframe->element, child->element, prefix.c_str(), "")) { + if (!mjs_attach(pframe->element, child, prefix.c_str(), "")) { throw mjXError(elem, mjs_getError(spec)); } } else { // only set frame to existing body - if (mjs_setFrame(child->element, pframe)) { + if (mjs_setFrame(child_body->element, pframe)) { throw mjXError(elem, mjs_getError(spec)); } } From 40393f460a4a378ac26dbf0416217e90063d84b0 Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Wed, 2 Apr 2025 06:33:26 -0700 Subject: [PATCH 032/191] Fix shadow flickering artefacts on platforms that do not support ARB_clip_control. PiperOrigin-RevId: 743112447 Change-Id: Ibb727685605c0a257547162703f61e5232ff544c --- doc/changelog.rst | 2 ++ src/render/render_context.c | 10 +++++++--- src/render/render_gl3.c | 21 ++++++++++++++++++--- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index 4af1fae5..155dff63 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -24,6 +24,8 @@ Bug fixes which the Jacobian is computed, now fixed. - Fixed a bug that caused the parent frame of elements in the child worldbody to be incorrectly set when attaching an mjSpec to a frame or a site. +- Fixed a bug that caused shadow rendering to flicker on platforms (e.g., MacOS) that do not support ARB_clip_control. + Fixed in collaboration with :github:user:`aftersomemath`. Python bindings ^^^^^^^^^^^^^^^ diff --git a/src/render/render_context.c b/src/render/render_context.c index 6adb4f11..b305f2dd 100644 --- a/src/render/render_context.c +++ b/src/render/render_context.c @@ -1050,13 +1050,17 @@ static void makeShadow(const mjModel* m, mjrContext* con) { } glBindFramebuffer(GL_FRAMEBUFFER, con->shadowFBO); - // create shadow depth texture: in TEXTURE1 + // Create a shadow depth texture in TEXTURE1 and explicitly select an int24 + // depth buffer. A depth stencil format is used because that appears to be + // more widely supported (MacOS does not support GL_DEPTH_COMPONENT24). Using + // a fixed format makes it easier to choose glPolygonOffset parameters that + // result in reasonably consistent and artifact free shadows across platforms. glGenTextures(1, &con->shadowTex); glActiveTexture(GL_TEXTURE1); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, con->shadowTex); - glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, - con->shadowSize, con->shadowSize, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL); + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, + con->shadowSize, con->shadowSize, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); diff --git a/src/render/render_gl3.c b/src/render/render_gl3.c index dffa48d7..9d168585 100644 --- a/src/render/render_gl3.c +++ b/src/render/render_gl3.c @@ -1211,9 +1211,24 @@ void mjr_render(mjrRect viewport, mjvScene* scn, const mjrContext* con) { int cull_face = glIsEnabled(GL_CULL_FACE); glDisable(GL_CULL_FACE); // all faces cast shadows glEnable(GL_POLYGON_OFFSET_FILL); - float kOffsetFactor = -1.5f; - float kOffsetUnits = -4.0f; - glPolygonOffset(kOffsetFactor, kOffsetUnits); // prevents "shadow acne" + + // The limited resolution of the shadow maps means multiple fragments + // sample the same texel. When light and camera directions differ on + // surfaces that should be lit this causes "shadow acne" because some + // fragments will be lit while adjacent fragments are not. To mitigate + // this artifact, an offset is applied to the depth values in the + // shadow map. The offset must be large enough to ensure consistent + // depth comparison occurs within the limited precision of the depth + // buffer. The offset is computed by glPolygonOffset using parameters + // that are chosen empirically. We need different values when clip + // control is on/off because this setting changes the depth precision. + float kOffsetFactor = -16.0f; + float kOffsetUnits = -512.0f; + if (mjGLAD_GL_ARB_clip_control) { + kOffsetFactor = -1.5f; + kOffsetUnits = -4.0f; + } + glPolygonOffset(kOffsetFactor, kOffsetUnits); // render all geoms to depth texture for (int j=0; j < ngeom; j++) { From 54f132f3f09419ea93eace2b020fa08418cff4c8 Mon Sep 17 00:00:00 2001 From: Baruch Tabanpour Date: Wed, 2 Apr 2025 10:15:12 -0700 Subject: [PATCH 033/191] Resolve discrepancy in RK4 between MJX and MJ. PiperOrigin-RevId: 743185866 Change-Id: I478faca1d8733999cc9a0682e931b19ee05e3843 --- mjx/mujoco/mjx/_src/forward.py | 13 +++++++------ mjx/mujoco/mjx/_src/forward_test.py | 1 + 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/mjx/mujoco/mjx/_src/forward.py b/mjx/mujoco/mjx/_src/forward.py index 07f33a90..71a6e3f5 100644 --- a/mjx/mujoco/mjx/_src/forward.py +++ b/mjx/mujoco/mjx/_src/forward.py @@ -317,7 +317,7 @@ def euler(m: Model, d: Data) -> Data: @named_scope def rungekutta4(m: Model, d: Data) -> Data: """Runge-Kutta explicit order 4 integrator.""" - d_t0 = d + d0 = d # pylint: disable=invalid-name A, B = _RK4_A, _RK4_B C = jp.tril(A).sum(axis=0) # C(i) = sum_j A(i,j) @@ -338,9 +338,9 @@ def rungekutta4(m: Model, d: Data) -> Data: lambda k: a * k, (kqvel, d.qacc, d.act_dot) ) # get intermediate RK solutions - kqpos = scan.flat(m, integrate_fn, 'jqv', 'q', m.jnt_type, d_t0.qpos, dqvel) - kact = d_t0.act + dact_dot * m.opt.timestep - kqvel = d_t0.qvel + dqacc * m.opt.timestep + kqpos = scan.flat(m, integrate_fn, 'jqv', 'q', m.jnt_type, d0.qpos, dqvel) + kact = d0.act + dact_dot * m.opt.timestep + kqvel = d0.qvel + dqacc * m.opt.timestep d = d.replace(qpos=kqpos, qvel=kqvel, act=kact, time=t) d = forward(m, d) @@ -352,9 +352,10 @@ def rungekutta4(m: Model, d: Data) -> Data: abt = jp.vstack([jp.diag(A), B[1:4], T]).T out, _ = jax.lax.scan(f, (qvel, qacc, act_dot, kqvel, d), abt, unroll=3) - qvel, qacc, act_dot, *_ = out + qvel, qacc, act_dot, _, d1 = out - d = _advance(m, d_t0, act_dot, qacc, qvel) + d = d1.replace(qpos=d0.qpos, qvel=d0.qvel, act=d0.act, time=d0.time) + d = _advance(m, d, act_dot, qacc, qvel) return d diff --git a/mjx/mujoco/mjx/_src/forward_test.py b/mjx/mujoco/mjx/_src/forward_test.py index ebe94ebe..c6466449 100644 --- a/mjx/mujoco/mjx/_src/forward_test.py +++ b/mjx/mujoco/mjx/_src/forward_test.py @@ -130,6 +130,7 @@ class ForwardTest(absltest.TestCase): _assert_attr_eq(d, dx, 'qpos') _assert_attr_eq(d, dx, 'act') _assert_attr_eq(d, dx, 'time') + _assert_attr_eq(d, dx, 'xpos') def test_eulerdamp(self): m = test_util.load_test_file('pendula.xml') From 8941f56e86c6977425791159fce096990d817c7e Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Thu, 3 Apr 2025 06:28:05 -0700 Subject: [PATCH 034/191] Copy UIDs during mj_copySpec. This will produce the same mjSpec signature after a deep copy. PiperOrigin-RevId: 743538018 Change-Id: Ic0f5b06a00e8883cb92dc98159db3bea02cb7a36 --- src/user/user_model.cc | 2 +- src/user/user_objects.cc | 6 +++--- test/user/user_api_test.cc | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/user/user_model.cc b/src/user/user_model.cc index 402f738b..e5650c17 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -297,7 +297,7 @@ void mjCModel::CopyList(std::vector& dest, // copy the element from the other model to this model if (deepcopy_) { source[i]->ForgetKeyframes(); - candidate->uid = GetUid(); + candidate->uid = source[i]->uid; } else { candidate->AddRef(); } diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index d46c51df..631bb2f8 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -840,7 +840,7 @@ mjCBody::mjCBody(mjCModel* _model) { mjCBody::mjCBody(const mjCBody& other, mjCModel* _model) { model = _model; - uid = model->GetUid(); + uid = other.uid; mjSpec* origin = model->FindSpec(other.compiler); compiler = origin ? &origin->compiler : &model->spec.compiler; *this = other; @@ -944,7 +944,7 @@ mjCBody& mjCBody::operator+=(const mjCFrame& other) { frames.back()->frame = other.frame; if (model->deepcopy_) { frames.back()->NameSpace(other_model); - frames.back()->uid = model->GetUid(); + frames.back()->uid = other.uid; } else { frames.back()->AddRef(); } @@ -1038,7 +1038,7 @@ void mjCBody::CopyList(std::vector& dst, const std::vector& src, if (!model->deepcopy_) { dst.back()->AddRef(); } else { - dst.back()->uid = model->GetUid(); + dst.back()->uid = src[i]->uid; } // set namespace diff --git a/test/user/user_api_test.cc b/test/user/user_api_test.cc index c9bc6bd1..ddde4679 100644 --- a/test/user/user_api_test.cc +++ b/test/user/user_api_test.cc @@ -538,6 +538,9 @@ TEST_F(PluginTest, RecompileCompare) { // copy spec mjSpec* s_copy = mj_copySpec(s); + // compare signature + EXPECT_EQ(s->element->signature, s_copy->element->signature) << xml; + // compile twice and compare mjModel* m_old = mj_compile(s, nullptr); From f96f3e1c22ac1e231165b2a1fda8b68ddc143c6a Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Thu, 3 Apr 2025 10:11:35 -0700 Subject: [PATCH 035/191] Add XML `compiler/saveinertial` flag. Fixes #2405 PiperOrigin-RevId: 743607654 Change-Id: I1f2e61be89b79c1995e079f36aa285096f9fb441 --- doc/XMLreference.rst | 4 ++++ doc/XMLschema.rst | 2 +- doc/changelog.rst | 4 +++- doc/includes/references.h | 1 + include/mujoco/mjspec.h | 1 + python/mujoco/introspect/structs.py | 5 +++++ src/user/user_init.c | 1 + src/xml/xml_native_reader.cc | 9 ++++++--- src/xml/xml_native_writer.cc | 3 ++- test/xml/xml_native_writer_test.cc | 26 ++++++++++++++++++++++---- unity/Runtime/Bindings/MjBindings.cs | 1 + 11 files changed, 47 insertions(+), 10 deletions(-) diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 4e5b4393..16ab04d7 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -832,6 +832,10 @@ has any effect. The settings here are global and apply to the entire model. necessary to adjust this attribute and the geom-specific groups so as to exclude world geoms from the inertial computation. +.. _compiler-saveinertial: + +:at:`saveinertial`: :at-val:`[false, true], "false"` + If set to "true", the compiler will save explicit :ref:`inertial ` clauses for all bodies. .. _compiler-lengthrange: diff --git a/doc/XMLschema.rst b/doc/XMLschema.rst index 6cbc0121..5af5fca5 100644 --- a/doc/XMLschema.rst +++ b/doc/XMLschema.rst @@ -54,7 +54,7 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | | | | | :ref:`discardvisual` | :ref:`usethread` | :ref:`fusestatic` | :ref:`inertiafromgeom` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`inertiagrouprange` | :ref:`assetdir` | :ref:`alignfree` | | | +| | | | :ref:`inertiagrouprange` | :ref:`saveinertial` | :ref:`assetdir` | :ref:`alignfree` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| compiler |br| |_| |L| | | .. table:: | diff --git a/doc/changelog.rst b/doc/changelog.rst index 155dff63..3f727ebb 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -15,7 +15,9 @@ Upcoming version (not yet released) General ^^^^^^^ -- Add :ref:`orientation` parameter to :ref:`composite`. Moreover, allow the +- Added the :ref:`compiler/saveinertial` flag, writing explicit inertial clauses for all + bodies when saving to XML. +- Added :ref:`orientation` attribute to :ref:`composite`. Moreover, allow the composite to be the direct child of a frame. Bug fixes diff --git a/doc/includes/references.h b/doc/includes/references.h index 8d1cf9c8..069b895a 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -1736,6 +1736,7 @@ typedef struct mjsCompiler_ { // compiler options mjtByte fusestatic; // fuse static bodies with parent int inertiafromgeom; // use geom inertias (mjtInertiaFromGeom) int inertiagrouprange[2]; // range of geom groups used to compute inertia + mjtByte saveinertial; // save explicit inertial clause for all bodies to XML int alignfree; // align free joints with inertial frame mjLROpt LRopt; // options for lengthrange computation } mjsCompiler; diff --git a/include/mujoco/mjspec.h b/include/mujoco/mjspec.h index 3764257b..65eb55a4 100644 --- a/include/mujoco/mjspec.h +++ b/include/mujoco/mjspec.h @@ -138,6 +138,7 @@ typedef struct mjsCompiler_ { // compiler options mjtByte fusestatic; // fuse static bodies with parent int inertiafromgeom; // use geom inertias (mjtInertiaFromGeom) int inertiagrouprange[2]; // range of geom groups used to compute inertia + mjtByte saveinertial; // save explicit inertial clause for all bodies to XML int alignfree; // align free joints with inertial frame mjLROpt LRopt; // options for lengthrange computation } mjsCompiler; diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index 2cd2ff27..73007b79 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -9137,6 +9137,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), doc='range of geom groups used to compute inertia', ), + StructFieldDecl( + name='saveinertial', + type=ValueType(name='mjtByte'), + doc='save explicit inertial clause for all bodies to XML', + ), StructFieldDecl( name='alignfree', type=ValueType(name='int'), diff --git a/src/user/user_init.c b/src/user/user_init.c index 76ea52ad..183034ce 100644 --- a/src/user/user_init.c +++ b/src/user/user_init.c @@ -42,6 +42,7 @@ void mjs_defaultSpec(mjSpec* spec) { spec->compiler.usethread = 1; spec->compiler.inertiafromgeom = mjINERTIAFROMGEOM_AUTO; spec->compiler.inertiagrouprange[1] = mjNGROUP-1; + spec->compiler.saveinertial = 0; mj_defaultLROpt(&spec->compiler.LRopt); // engine data diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 35b1837c..695ceeac 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -97,10 +97,10 @@ static void UpdateString(string& psuffix, int count, int i) { const char* MJCF[nMJCF][mjXATTRNUM] = { {"mujoco", "!", "1", "model"}, {"<"}, - {"compiler", "*", "19", "autolimits", "boundmass", "boundinertia", "settotalmass", + {"compiler", "*", "20", "autolimits", "boundmass", "boundinertia", "settotalmass", "balanceinertia", "strippath", "coordinate", "angle", "fitaabb", "eulerseq", - "meshdir", "texturedir", "discardvisual", "usethread", - "fusestatic", "inertiafromgeom", "inertiagrouprange", "assetdir", "alignfree"}, + "meshdir", "texturedir", "discardvisual", "usethread", "fusestatic", "inertiafromgeom", + "inertiagrouprange", "saveinertial", "assetdir", "alignfree"}, {"<"}, {"lengthrange", "?", "10", "mode", "useexisting", "uselimit", "accel", "maxforce", "timeconst", "timestep", @@ -1015,6 +1015,9 @@ void mjXReader::Compiler(XMLElement* section, mjSpec* spec) { if (MapValue(section, "alignfree", &n, bool_map, 2)) { spec->compiler.alignfree = (n == 1); } + if (MapValue(section, "saveinertial", &n, bool_map, 2)) { + spec->compiler.saveinertial = (n == 1); + } // lengthrange subelement XMLElement* elem = FindSubElem(section, "lengthrange"); diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index 77a7dc6a..639ae081 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -1632,7 +1632,8 @@ void mjXWriter::Body(XMLElement* elem, mjCBody* body, mjCFrame* frame, string_vi WriteVector(elem, "user", body->get_userdata()); // write inertial - if (body->explicitinertial && model->compiler.inertiafromgeom != mjINERTIAFROMGEOM_TRUE) { + if (model->compiler.saveinertial || + (body->explicitinertial && model->compiler.inertiafromgeom != mjINERTIAFROMGEOM_TRUE)) { XMLElement* inertial = InsertEnd(elem, "inertial"); WriteAttr(inertial, "pos", 3, body->ipos); WriteAttr(inertial, "quat", 4, body->iquat, unitq); diff --git a/test/xml/xml_native_writer_test.cc b/test/xml/xml_native_writer_test.cc index 4de0a7fd..9bb31819 100644 --- a/test/xml/xml_native_writer_test.cc +++ b/test/xml/xml_native_writer_test.cc @@ -22,7 +22,7 @@ #include #include #include -#include +#include // NOLINT(build/c++17) #include #include @@ -32,7 +32,6 @@ #include #include #include -#include "src/cc/array_safety.h" #include "src/xml/xml_numeric_format.h" #include "test/fixture.h" @@ -135,6 +134,23 @@ TEST_F(XMLWriterTest, SavesDisableSensor) { mj_deleteModel(model); } +TEST_F(XMLWriterTest, SavesInertial) { + static constexpr char xml[] = R"( + + + + + + + + + )"; + mjModel* model = LoadModelFromString(xml); + std::string saved_xml = SaveAndReadXml(model); + EXPECT_THAT(saved_xml, HasSubstr("mass=\"1\"")); + mj_deleteModel(model); +} + TEST_F(XMLWriterTest, EmptyUserSensor) { static constexpr char xml[] = R"( @@ -957,8 +973,10 @@ TEST_F(XMLWriterTest, WritesSkin) { ASSERT_THAT(model, NotNull()); EXPECT_THAT(model->nskin, 1); - mjModel* mtemp = LoadModelFromString(SaveAndReadXml(model)); - ASSERT_THAT(mtemp, NotNull()); + char error[1024]; + mjModel* mtemp = LoadModelFromString(SaveAndReadXml(model), + error, sizeof(error)); + ASSERT_THAT(mtemp, NotNull()) << error; EXPECT_THAT(mtemp->nskin, 1); mj_deleteModel(model); diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index d7e79806..3fb3f812 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -5765,6 +5765,7 @@ public unsafe struct mjsCompiler_ { public byte fusestatic; public int inertiafromgeom; public fixed int inertiagrouprange[2]; + public byte saveinertial; public int alignfree; public mjLROpt_ LRopt; } From e1f5ceb65a6cae711ce7aa544e40d0ba324d8e9c Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Thu, 3 Apr 2025 12:05:30 -0700 Subject: [PATCH 036/191] Add `mjModel.tendon_armature` (in preparation, not yet implemented) PiperOrigin-RevId: 743649968 Change-Id: I1d893e3e06afc48b652575bc7837750ab3573670 --- doc/XMLreference.rst | 11 +++- doc/XMLschema.rst | 4 +- doc/includes/references.h | 4 +- include/mujoco/mjmodel.h | 1 + include/mujoco/mjspec.h | 3 +- include/mujoco/mjxmacro.h | 1 + mjx/mujoco/mjx/_src/types.py | 2 + python/mujoco/introspect/structs.py | 13 +++++ src/user/user_model.cc | 2 + src/user/user_objects.cc | 12 ++++ src/xml/xml_native_reader.cc | 11 ++-- src/xml/xml_native_writer.cc | 1 + test/xml/xml_native_reader_test.cc | 85 +++++++++++++++++++++++++++- unity/Runtime/Bindings/MjBindings.cs | 1 + 14 files changed, 140 insertions(+), 11 deletions(-) diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 16ab04d7..8ad6c587 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -835,7 +835,7 @@ has any effect. The settings here are global and apply to the entire model. .. _compiler-saveinertial: :at:`saveinertial`: :at-val:`[false, true], "false"` - If set to "true", the compiler will save explicit :ref:`inertial ` clauses for all bodies. + If set to "true", the compiler will save explicit :ref:`inertial ` clauses for all bodies. .. _compiler-lengthrange: @@ -4706,6 +4706,13 @@ length X, as in the clip on the right of `this example model joint damping which is integrated implicitly by the Euler method, tendon damping is not integrated implicitly, thus joint damping should be used if possible. +.. TODO(tassa): Update here once the feature is implemented. + +.. _tendon-spatial-armature: + +:at:`armature`: :at-val:`real, "0"` + Inertia associated with tendon. This feature is not yet implemented. + .. _tendon-spatial-user: :at:`user`: :at-val:`real(nuser_tendon), "0 0 ..."` @@ -4815,6 +4822,8 @@ as above. .. _tendon-fixed-damping: +.. _tendon-fixed-armature: + .. _tendon-fixed-user: .. |tendon/fixed attrib list| replace:: diff --git a/doc/XMLschema.rst b/doc/XMLschema.rst index 5af5fca5..56119cd7 100644 --- a/doc/XMLschema.rst +++ b/doc/XMLschema.rst @@ -627,7 +627,7 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | | | | | :ref:`material` | :ref:`margin` | :ref:`stiffness` | :ref:`damping` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`rgba` | :ref:`user` | | | | +| | | | :ref:`armature` | :ref:`rgba` | :ref:`user` | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_2| spatial |br| |_2| |L| | | .. table:: | @@ -661,7 +661,7 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | | | | | :ref:`solimpfriction` | :ref:`frictionloss` | :ref:`springlength` | :ref:`margin` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`stiffness` | :ref:`damping` | :ref:`user` | | | +| | | | :ref:`stiffness` | :ref:`damping` | :ref:`armature` | :ref:`user` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_2| fixed |br| |_2| |L| | | .. table:: | diff --git a/doc/includes/references.h b/doc/includes/references.h index 069b895a..846f0c75 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -1339,6 +1339,7 @@ struct mjModel_ { mjtNum* tendon_margin; // min distance for limit detection (ntendon x 1) mjtNum* tendon_stiffness; // stiffness coefficient (ntendon x 1) mjtNum* tendon_damping; // damping coefficient (ntendon x 1) + mjtNum* tendon_armature; // inertia associated with tendon velocity (ntendon x 1) mjtNum* tendon_frictionloss; // loss due to friction (ntendon x 1) mjtNum* tendon_lengthspring; // spring resting length range (ntendon x 2) mjtNum* tendon_length0; // tendon length in qpos0 (ntendon x 1) @@ -2174,13 +2175,14 @@ typedef struct mjsTendon_ { // tendon specification mjsElement* element; // element type mjString* name; // name - // stiffness, damping, friction + // stiffness, damping, friction, armature double stiffness; // stiffness coefficient double springlength[2]; // spring resting length; {-1, -1}: use qpos_spring double damping; // damping coefficient double frictionloss; // friction loss mjtNum solref_friction[mjNREF]; // solver reference: tendon friction mjtNum solimp_friction[mjNIMP]; // solver impedance: tendon friction + double armature; // inertia associated with tendon velocity // length range int limited; // does tendon have limits (mjtLimited) diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index 632f6ddd..1699ca0c 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -1040,6 +1040,7 @@ struct mjModel_ { mjtNum* tendon_margin; // min distance for limit detection (ntendon x 1) mjtNum* tendon_stiffness; // stiffness coefficient (ntendon x 1) mjtNum* tendon_damping; // damping coefficient (ntendon x 1) + mjtNum* tendon_armature; // inertia associated with tendon velocity (ntendon x 1) mjtNum* tendon_frictionloss; // loss due to friction (ntendon x 1) mjtNum* tendon_lengthspring; // spring resting length range (ntendon x 2) mjtNum* tendon_length0; // tendon length in qpos0 (ntendon x 1) diff --git a/include/mujoco/mjspec.h b/include/mujoco/mjspec.h index 65eb55a4..42b8c03f 100644 --- a/include/mujoco/mjspec.h +++ b/include/mujoco/mjspec.h @@ -617,13 +617,14 @@ typedef struct mjsTendon_ { // tendon specification mjsElement* element; // element type mjString* name; // name - // stiffness, damping, friction + // stiffness, damping, friction, armature double stiffness; // stiffness coefficient double springlength[2]; // spring resting length; {-1, -1}: use qpos_spring double damping; // damping coefficient double frictionloss; // friction loss mjtNum solref_friction[mjNREF]; // solver reference: tendon friction mjtNum solimp_friction[mjNIMP]; // solver impedance: tendon friction + double armature; // inertia associated with tendon velocity // length range int limited; // does tendon have limits (mjtLimited) diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index 34a6dee4..0aaf1af6 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -484,6 +484,7 @@ X ( mjtNum, tendon_margin, ntendon, 1 ) \ XMJV( mjtNum, tendon_stiffness, ntendon, 1 ) \ XMJV( mjtNum, tendon_damping, ntendon, 1 ) \ + X ( mjtNum, tendon_armature, ntendon, 1 ) \ XMJV( mjtNum, tendon_frictionloss, ntendon, 1 ) \ XMJV( mjtNum, tendon_lengthspring, ntendon, 2 ) \ X ( mjtNum, tendon_length0, ntendon, 1 ) \ diff --git a/mjx/mujoco/mjx/_src/types.py b/mjx/mujoco/mjx/_src/types.py index 515d15f4..9052043c 100644 --- a/mjx/mujoco/mjx/_src/types.py +++ b/mjx/mujoco/mjx/_src/types.py @@ -771,6 +771,7 @@ class Model(PyTreeNode): tendon_margin: min distance for limit detection (ntendon,) tendon_stiffness: stiffness coefficient (ntendon,) tendon_damping: damping coefficient (ntendon,) + tendon_armature: inertia associated with tendon velocity (ntendon,) tendon_frictionloss: loss due to friction (ntendon,) tendon_lengthspring: spring resting length range (ntendon, 2) tendon_length0: tendon length in qpos0 (ntendon,) @@ -1113,6 +1114,7 @@ class Model(PyTreeNode): tendon_margin: jax.Array tendon_stiffness: jax.Array tendon_damping: jax.Array + tendon_armature: jax.Array tendon_frictionloss: jax.Array tendon_lengthspring: jax.Array tendon_length0: jax.Array diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index 73007b79..59b21fb8 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -3687,6 +3687,14 @@ STRUCTS: Mapping[str, StructDecl] = dict([ doc='damping coefficient', array_extent=('ntendon',), ), + StructFieldDecl( + name='tendon_armature', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='inertia associated with tendon velocity', + array_extent=('ntendon',), + ), StructFieldDecl( name='tendon_frictionloss', type=PointerType( @@ -11324,6 +11332,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), doc='solver impedance: tendon friction', ), + StructFieldDecl( + name='armature', + type=ValueType(name='double'), + doc='inertia associated with tendon velocity', + ), StructFieldDecl( name='limited', type=ValueType(name='int'), diff --git a/src/user/user_model.cc b/src/user/user_model.cc index e5650c17..e49c6112 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -3390,6 +3390,7 @@ void mjCModel::CopyObjects(mjModel* m) { m->tendon_margin[i] = (mjtNum)pte->margin; m->tendon_stiffness[i] = (mjtNum)pte->stiffness; m->tendon_damping[i] = (mjtNum)pte->damping; + m->tendon_armature[i] = (mjtNum)pte->armature; m->tendon_frictionloss[i] = (mjtNum)pte->frictionloss; m->tendon_lengthspring[2*i] = (mjtNum)pte->springlength[0]; m->tendon_lengthspring[2*i+1] = (mjtNum)pte->springlength[1]; @@ -4931,6 +4932,7 @@ bool mjCModel::CopyBack(const mjModel* m) { tendons_[i]->margin = (double)m->tendon_margin[i]; tendons_[i]->stiffness = (double)m->tendon_stiffness[i]; tendons_[i]->damping = (double)m->tendon_damping[i]; + tendons_[i]->armature = (double)m->tendon_armature[i]; tendons_[i]->frictionloss = (double)m->tendon_frictionloss[i]; if (nuser_tendon) { diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 631bb2f8..f52f8cf6 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -5616,6 +5616,12 @@ void mjCTendon::Compile(void) { // spatial path else { + if (armature < 0) { + throw mjCError(this, + "tendon '%s' (id = %d): tendon armature cannot be negative", + name.c_str(), id); + } + switch (path[i]->type) { case mjWRAP_PULLEY: // pulley should not follow other pulley @@ -5657,6 +5663,12 @@ void mjCTendon::Compile(void) { name.c_str(), id, i); } + if (armature > 0) { + throw mjCError(this, + "tendon '%s' (id = %d): geom wrapping not supported by tendon armature", + name.c_str(), id); + } + // mark geoms as non visual model->Geoms()[path[i]->obj->id]->SetNotVisual(); break; diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 695ceeac..f3afd4a2 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -363,18 +363,18 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { {"tendon", "*", "0"}, {"<"}, - {"spatial", "*", "18", "name", "class", "group", "limited", "range", + {"spatial", "*", "19", "name", "class", "group", "limited", "range", "solreflimit", "solimplimit", "solreffriction", "solimpfriction", "frictionloss", "springlength", "width", "material", - "margin", "stiffness", "damping", "rgba", "user"}, + "margin", "stiffness", "damping", "armature", "rgba", "user"}, {"<"}, {"site", "*", "1", "site"}, {"geom", "*", "2", "geom", "sidesite"}, {"pulley", "*", "1", "divisor"}, {">"}, - {"fixed", "*", "15", "name", "class", "group", "limited", "range", + {"fixed", "*", "16", "name", "class", "group", "limited", "range", "solreflimit", "solimplimit", "solreffriction", "solimpfriction", - "frictionloss", "springlength", "margin", "stiffness", "damping", "user"}, + "frictionloss", "springlength", "margin", "stiffness", "damping", "armature", "user"}, {"<"}, {"joint", "*", "2", "joint", "coef"}, {">"}, @@ -2061,6 +2061,7 @@ void mjXReader::OneTendon(XMLElement* elem, mjsTendon* tendon) { ReadAttr(elem, "margin", 1, &tendon->margin, text); ReadAttr(elem, "stiffness", 1, &tendon->stiffness, text); ReadAttr(elem, "damping", 1, &tendon->damping, text); + ReadAttr(elem, "armature", 1, &tendon->armature, text); ReadAttr(elem, "frictionloss", 1, &tendon->frictionloss, text); // read springlength, either one or two values; if one, copy to second value if (ReadAttr(elem, "springlength", 2, tendon->springlength, text, false, false) == 1) { @@ -3800,7 +3801,7 @@ void mjXReader::Tendon(XMLElement* section) { def = mjs_getSpecDefault(spec); } - // create equality constraint and parse + // create tendon and parse mjsTendon* tendon = mjs_addTendon(spec, def); OneTendon(elem, tendon); diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index 639ae081..5f2d44cf 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -729,6 +729,7 @@ void mjXWriter::OneTendon(XMLElement* elem, const mjCTendon* tendon, mjCDef* def WriteAttr(elem, "margin", 1, &tendon->margin, &def->Tendon().margin); WriteAttr(elem, "stiffness", 1, &tendon->stiffness, &def->Tendon().stiffness); WriteAttr(elem, "damping", 1, &tendon->damping, &def->Tendon().damping); + WriteAttr(elem, "armature", 1, &tendon->armature, &def->Tendon().armature); WriteAttr(elem, "frictionloss", 1, &tendon->frictionloss, &def->Tendon().frictionloss); if (tendon->springlength[0] != tendon->springlength[1] || def->Tendon().springlength[0] != def->Tendon().springlength[1]) { diff --git a/test/xml/xml_native_reader_test.cc b/test/xml/xml_native_reader_test.cc index 7b74af4c..05a6c61c 100644 --- a/test/xml/xml_native_reader_test.cc +++ b/test/xml/xml_native_reader_test.cc @@ -15,6 +15,7 @@ // Tests for xml/xml_native_reader.cc. #include +#include #include #include #include @@ -1148,6 +1149,88 @@ TEST_F(XMLReaderTest, ParsePolycoef) { mj_deleteModel(m); } +TEST_F(XMLReaderTest, TendonArmature) { + static constexpr char xml[] = R"( + + + + + + + + + + + + + + + + + + + + + + + + )"; + std::array error; + mjModel* m = LoadModelFromString(xml, error.data(), error.size()); + EXPECT_THAT(m, NotNull()) << error.data(); + EXPECT_EQ(m->ntendon, 3); + EXPECT_FLOAT_EQ(m->tendon_armature[0], 1.5); + EXPECT_FLOAT_EQ(m->tendon_armature[1], 2.5); + EXPECT_FLOAT_EQ(m->tendon_armature[2], 0); + mj_deleteModel(m); +} + +TEST_F(XMLReaderTest, TendonArmatureNegative) { + static constexpr char xml[] = R"( + + + + + + + + + + + + + + )"; + std::array error; + mjModel* m = LoadModelFromString(xml, error.data(), error.size()); + EXPECT_THAT(m, IsNull()); + EXPECT_THAT(error.data(), HasSubstr("tendon armature cannot be negative")); +} + +TEST_F(XMLReaderTest, TendonArmatureGeomWrap) { + static constexpr char xml[] = R"( + + + + + + + + + + + + + + + + )"; + std::array error; + mjModel* m = LoadModelFromString(xml, error.data(), error.size()); + EXPECT_THAT(m, IsNull()); + EXPECT_THAT(error.data(), HasSubstr("geom wrapping not supported")); +} + // ------------------------ test frame parsing --------------------------------- TEST_F(XMLReaderTest, ParseFrame) { static constexpr char xml[] = R"( @@ -1862,7 +1945,7 @@ TEST_F(XMLReaderTest, CameraInvalidFovyAndSensorsize) { EXPECT_THAT(error.data(), HasSubstr("line 6")); } -TEST_F(XMLReaderTest, CameraPricipalRequiresSensorsize) { +TEST_F(XMLReaderTest, CameraPrincipalRequiresSensorsize) { static constexpr char xml[] = R"( diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index 3fb3f812..f7d6ac29 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -5578,6 +5578,7 @@ public unsafe struct mjModel_ { public double* tendon_margin; public double* tendon_stiffness; public double* tendon_damping; + public double* tendon_armature; public double* tendon_frictionloss; public double* tendon_lengthspring; public double* tendon_length0; From d05251af2a7fb677f31f5896fbe689d9073e25d2 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Fri, 4 Apr 2025 07:42:40 -0700 Subject: [PATCH 037/191] Add tendon armature PiperOrigin-RevId: 743939992 Change-Id: I587214f5d6fabbc0cc273c33d82decbe9ad8f919 --- doc/XMLreference.rst | 22 ++- doc/changelog.rst | 1 + doc/images/XMLreference/tendon_armature.gif | Bin 0 -> 125761 bytes .../XMLreference/tendon_armature_dark.gif | Bin 0 -> 135942 bytes src/engine/engine_core_smooth.c | 124 +++++++++++- src/engine/engine_core_smooth.h | 9 + src/engine/engine_forward.c | 15 +- src/engine/engine_inverse.c | 5 +- src/engine/engine_setconst.c | 3 +- src/user/user_model.cc | 76 +++++--- src/user/user_model.h | 1 + test/engine/engine_core_smooth_test.cc | 178 ++++++++++++++++++ .../testdata/core_smooth/ten_armature_0.xml | 28 +++ .../core_smooth/ten_armature_0_compare.xml | 17 ++ .../core_smooth/ten_armature_0_equiv.xml | 20 ++ .../testdata/core_smooth/ten_armature_1.xml | 28 +++ .../core_smooth/ten_armature_1_compare.xml | 26 +++ .../core_smooth/ten_armature_1_equiv.xml | 34 ++++ .../testdata/core_smooth/ten_armature_2.xml | 29 +++ .../testdata/core_smooth/ten_armature_3.xml | 39 ++++ .../testdata/core_smooth/ten_armature_4.xml | 47 +++++ .../core_smooth/ten_armature_offtree.xml | 77 ++++++++ 22 files changed, 735 insertions(+), 44 deletions(-) create mode 100644 doc/images/XMLreference/tendon_armature.gif create mode 100644 doc/images/XMLreference/tendon_armature_dark.gif create mode 100644 test/engine/testdata/core_smooth/ten_armature_0.xml create mode 100644 test/engine/testdata/core_smooth/ten_armature_0_compare.xml create mode 100644 test/engine/testdata/core_smooth/ten_armature_0_equiv.xml create mode 100644 test/engine/testdata/core_smooth/ten_armature_1.xml create mode 100644 test/engine/testdata/core_smooth/ten_armature_1_compare.xml create mode 100644 test/engine/testdata/core_smooth/ten_armature_1_equiv.xml create mode 100644 test/engine/testdata/core_smooth/ten_armature_2.xml create mode 100644 test/engine/testdata/core_smooth/ten_armature_3.xml create mode 100644 test/engine/testdata/core_smooth/ten_armature_4.xml create mode 100644 test/engine/testdata/core_smooth/ten_armature_offtree.xml diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 8ad6c587..6166691b 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -4706,12 +4706,30 @@ length X, as in the clip on the right of `this example model joint damping which is integrated implicitly by the Euler method, tendon damping is not integrated implicitly, thus joint damping should be used if possible. -.. TODO(tassa): Update here once the feature is implemented. +.. image:: images/XMLreference/tendon_armature.gif + :width: 30% + :align: right + :class: only-light + :target: https://github.com/google-deepmind/mujoco/blob/main/test/engine/testdata/core_smooth/ten_armature_1_compare.xml +.. image:: images/XMLreference/tendon_armature_dark.gif + :width: 30% + :align: right + :class: only-dark + :target: https://github.com/google-deepmind/mujoco/blob/main/test/engine/testdata/core_smooth/ten_armature_1_compare.xml .. _tendon-spatial-armature: :at:`armature`: :at-val:`real, "0"` - Inertia associated with tendon. This feature is not yet implemented. + Inertia associated with changes in tendon length. Setting this attribute to a positive value :math:`m` adds a kinetic + energy term :math:`\frac{1}{2}mv^2`, where :math:`v` is the tendon velocity. Tendon inertia is most valuable + when modeling the :ref:`armature` inertia in a linear actuator which contains a spinning element + or the inertial motion of a fluid in a linear hydraulic actuator. In the illustration, we compare (*left*) a 3-dof + system with a "tendon" implemented with a rotational joint and a slider joint with + :ref:`armature`, attached to the world with a :ref:`connect` constraint and + (*right*) an equivalent 1-dof model with an armature-bearing tendon. Like joint :ref:`armature`, + this added inertia is only associated with changes in tendon length, and would not affect the dynamics of a moving + fixed-length tendon. Because the tendon Jacobian :math:`J` is position-dependent, tendon armature leads to an + additional bias-force term :math:`c = m J \dot{J}^T \dot{q}`. .. _tendon-spatial-user: diff --git a/doc/changelog.rst b/doc/changelog.rst index 3f727ebb..b5d51bc0 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -15,6 +15,7 @@ Upcoming version (not yet released) General ^^^^^^^ +- Added :ref:`tendon armature`: inertia associated with changes in tendon length. - Added the :ref:`compiler/saveinertial` flag, writing explicit inertial clauses for all bodies when saving to XML. - Added :ref:`orientation` attribute to :ref:`composite`. Moreover, allow the diff --git a/doc/images/XMLreference/tendon_armature.gif b/doc/images/XMLreference/tendon_armature.gif new file mode 100644 index 0000000000000000000000000000000000000000..5a1c6d98c8e0713257a0b2c1c752c032c9c33142 GIT binary patch literal 125761 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%0DD z`q!w+2^Z6aR{5H;y5{L8s!{UmsMQ=&?8~+#yz#Pn`+kg}fxMjg#d5vLzTiuA%1F1X z#@)k8dDmJA(8#{5^QG99-TnG&&rtIef0MWznOui^ee#FpkaDr5A!L<|tZ2^Fo3hN2 zy(79V-rqk~&y|qn=y?-V{#DkEpjatORc zGg2G1V`*lyuZml`F#_l8IGb5|GBlJJ-))$SnBK)@`l@6YY^KBRYWEqUS!wxGb=?S` z_9BL^?&$dv2Q%9)zE<80Zd-2WwzEE(1^N99;@gpd@-49OngLxOPf;(=Br5Y^ntc+L&vrjs(N8Wi3){IXg}im7#O}3=Gx4-2h%Y;;U@%+ZxlKKbTxJhN z$t_M?HL0tsLBGZb@22`u4yF#Lv{o@vf|8%~0reZgSv?8JG4Zovrc79r$so+B; zz#F-+ZxlIg1U_gqYiGI-Zv8gkdIAfTf-%&^- zIaW409G2Y3xEN}1et%RXmvxaCfUS+CWpr1(cToQ%B=xoomhG(rH|vWu5_ZDw_9s6m z)9o2}S)zW|r#!k}&9i((^I+ND510jSl?xIExr{x<&^f0@zgCBMQLlvA@Cn)_ug7F^ zkr5A%Uw=N@*V3o!^3QM=^0?!jD5K>7or32KN_Q*c_|(u&xJb|IVtG+oGu4MfWpsqjiUz(ImD0itof5`{ zPa}M+(Hl8TJ5+OJD3*qPARk=xXy=8U%^cMks;xkTs7~NJ=kMT=JuIgsUXs{XT=fAS%%CcGia1*Gp}4eW>Q{TldTYNK z-Eg*(h8hXI*^|19P~oL>`iBxlIEBOv(A2oIY*SGvqGFLj`k2Lm*Fzk+NqD&ewPN4W z8C}@{ebmo#$m$su3VIRVHZ)tucvmN9A)peF^N5hRPyuglkH;q*$NmZH#CqUz64?-Q zHnl*0zS6s#<+0)01=$?F^F#XGj+53vY%YYYR*-VkEo!l9=sj*F8W&d8OCXCBXCmHr zCDUb`3L%=D_jsTRRi7;?jHHBWPp;+vWF0Di&D=F^fMj$FSkyn(&z-lr)f|tZLvZa- z6u8u@E}Vj+(KLfZ5T$Q7$u5x7{%UW>gla?1+*u1Jmx@NMM*O!dc;-0B29Flk)+nrg zHLg3E=vDr}?cB_7lSK-b%c_*s+cNtIFOu*=;}8eDZ->Dh*jYTyYYJ?V6Y+@*hx+MD zE>q#dW{*7{Gng*LrTk|;`lefRB86x@G)Z$=CW&cIo>y9@;0e@#4df1#EI1V)hT;zb-gbc+{Q+hfASYYbhj^X&@U*RUu8Y;TOz%_14%rBdvrPpDG*m4uy&F zDmeI+;H}0#BB?UP(hQh96`n-BuO?fx*2R|WvFm2PoVI{-jNyd(ilgbOv><$DYTQ&u zvo*sl*jR%a2*Kib{oLy;<)k;AZX;LVh*oZN5lb-_9BtqVT4gsuP;fAXV9$n&B4wjd z8So|x&yu4{yL&<|!nL@~h|Tsi`Ir=0WzAR~s-XqSt>G&m<6!G8P6h>Z1uX`%(XLfg zJl=Ho^Wg*~3Q;82d1@j!2a-cYw??~)b8GM|2r6TC#bSQcziqk5iSi)>8ONp?&<_(o zpq*q)DH+GQYYS5@)yS5zGqmfk7H&j4p+mre)ctk)(!>zF&k0Mp6br6JlASy|{Q`5} z&}J;p37isZA=U;N{W)urD_%A47A zC6*U^$Ja^)G#EuTDnp_3*^q++&LkrXj>Al-VraW4^2dx9dZtV1{y6|OFAr=)%hT_C zfL{O2<`bnHr9!Is#(BuyE6f>XQv&DmH5~*EF(pak!xf_GTs16vaz6t>5DEF~>12FMR%*{B2iFyg9<46X^*gnVE`{O}lji!q#c}rkQ-jTj-SB8N?hs8%8QjF`(Gom( zvq_*R?^C!qoY&pfc+CET;qx9mp$JXWwjGp1_h=bO`ShSL3_o!ZF~>U8%4f-^?ER%# zE!}?En2b)I%?? zIAf!QiLFc?)D*k%o?c7V~+-Fi10?&}bnA~`a-WM@~k)5`v8$>HmGdwxUPqrNlV z@u}z67G0U?b-ViSPKL#%L;0QJ@z+nqVSf2w0pQz@uPMh;^UF)fyr#xPaE3L%?64mw zxn$|`0=ni}H9eoVas-D1)~22YEFtdgE{yR?Z0wvEc?e1__s<{x^zBDeW=P(CdhyXc zqmyIx89_50e6Tu44Ug^o`5a8n1?MJ!ExdD=o9arAHB~0n)|UK!P&M7gJ`iXR2#4k{ zF;sIHY?=2}?&~fM0W)?_I;!kTMAq>F)6~DNqwPeLNyhoHa|tgR|E|%2Ve&gA5$|v3 zTr1fA{eIF&f z52^q>bpBluSX^HG2KLKAN=({a0uo~&CHB6f1SG~LJ5s@`nxM-E&v$~EN=$vYz2Y!d zcQ~e=;?USuP|Y$==YglTLt}nIP&h%+);gsV-~=E9U>F?g7r-kYXboULz-u1#T>9M0A~0*HGob9H~~PSAeRwS*`^jnG3SN@|4>^yPB9ycjj2{KT+Sj68=g=T==7 zGGf@wnGlJQnWv3Eh0-_%_~E~Y(*NHp=^_6G5)N}u|AGYi|13!SEx8<2C$eufe5&PY zp0^1S8m@WKu|jYs%|3lse~hXIp;AW<>~PJuVpe$uSXNO=9xk3F?{VYZ&8+duf%GX9 zsS2G7&yq>^Ft&Dn>kB+ku5uPZmj_%oPvZ!N^xV0u;3B%3RTf#g`S^7`O=-9`yr+f7Svg#zrxKLE{zcjmND}qfyk{FLbW>IHsgi*ggW2 zH(<0+1^yw5#Cj!Sr|i5n_{To!;Y*y}|@hSC+%hAye(8la755Ttc>79`Yt}Z@%m&9=`lzpy*Jl5odz`&+*R9q4U zx{P*b2M?;csLrnVJ||UGruaL?Nin_nv)7-Sjn3GFQ(=YzS@t!)6a?9-ok?*WUR4RYoPF%BEQ)Dt1Te?R|5vxu95kIxEHh=T&Ln zHd5?>9_}9&w%6sGK99U9-Iq7Ot1&rPy9z6tRg~0{`q6N`Me;K-?BA+z)m+bD$-o2T zVH6Q7Is)!qa3h|=k}!|F;D+b-7nwE0 zjXd;_3cF*sue?xpQN5$ad7Px&$s>(McqUCajF9%rx$$Coj?mxH34Un7Mzl6A$zuCR z+0!v4Oq2?-*8pxt%u*xgg(N*!shMg~)XlGf+|PSqe8Z7S?PC}nF%w|0l%f11vp1}; z9GG4+Yvah3uXJDNoy}bX1`3`;8D03o`ij`dhc60od(hIZUwZkZi9v(S|2{O3I$uY> zSN4=JP=4~nigIk5PlzLl0GE646s|^rq~2b9?lO?kdGgmQ#XPBwzM3UO)wT=u?Sn{n zK|KR16dlvvM|2m5!@lBTkr<0f$Dh6%u+QuDmIpXH-dkY|8rVoRu>@f~a9fA%%{%tB zUt`4{M3*RR?B8kc5R0;4&I39b-fbXdXL}NB(jD4E6^&Whn_@p_`zHIK9fWY3CX(FN zW;TAcWRpc@gI*bvjfod=U@F3N@kB5TSUzP^8zKaGIDls$)|n zLi;s4BiB9SV3UYQ$^hHqPpLsVouh=l= z1VTa>4vU~kBDi)e+uuBVPyh>E^In97!Le+}x!60me!V>&XZyf7b_2vRmQA)|I4S#y zGK}pdP}xo;xME+Hf9aqe$pR{e5W&7@e$M2qg+crt!Bw!rDjNp(Mlk>W=J{80+t>2Y z;qDuhn}+K29=kg(#6ph-K&+P~RQY^XrX{m$|MlV-PZq`aUn+R_hp|3o!S9w!+;ASE z`!yZ`AdfnhRUp@jy8EKvHWp4iEv1m5w(_+~ z7R`)^b@%XJh5ehKb~b@~jQDHk^7OV_@bIm}wfvR*C)Wry19Gfp%+VcAZ8r{FVlZOS z4C*v8hC6WJLkB*0Y|8dnP?z68+oIYqSo_d`kQgZK;Zh z`Ff2<`&pwzuKlF{G#B%)JPf%vcrMkqbrNw$NV~e{Wc;>`%4IP;tS0j7xM zjBndxOk-6n?Dmmt>y9UAZ>p2R5wT2~{%Gum&zBF&2yLOb68F&c)u#JfbpmoOZa=YE zyH;jZRMALeYU9Wzu7b$@6I&}p8K+I49n^}?K6r9%?w3ow^qs351Eo67k*Go3YFS4E zHrkO&QZO?p%G*c7rK7v^`7B83)uuYjb=@mUxma%Tvn71Gb|9wo=cQ6G-75#@DsbAN zcVg6DpAtYKjyGQ@fBa6O0E$S*+MV{?zvoYRU9X4x$y6h-%slFzK00s8&5k%~6qLBw zx#`1|lyy@W50Nm4cN{O%Jk0kJajYy7bmFkhMiU|`QdH`M_*DP?u&V?qVD%{2Na3u_ zY=oBDE}4QJ8LU4^)>i;gw_$Gov5*0!qY)$2Q&Gmwi}4t<$yN>jZKLDQvEO1KDT zJ&6EiMh`U)jfZsmm}k=#oVFhu?;s;I50x>6vYrh`^TXd-2 zn2amiNbV1N_Tu)j2Mse#r{|zse0W2nW&%*#*f4w;H2Q6o!Y+wEgp}EHl>5 z$aAyHoT4@32+M7#v-8p1QvosIx&^znwkU7xvA*vfoJ**#1AVWT)u!5!HOH@;Mr1^@ z<67VMJsG@Jn-;W%X7W5Hf9`h9tBKK$=32YNqI9E@8&%1DH|jU8?;jrfxHUQXa!JyM z_isd=(I;;{18|b~a_OB|@c8KnZx7weJJrj#>DK&g@0obFmY3%>LaJ%0S09c<1kq}) z{u9(S$BWrgSksVh#QXMXCjWr9e{RY3<%!nZUGMLROZ%GqI%XpLcn^x3=EeDSFU7?+ zB~g)Wb}7*XF(vO7e}xUs$2?iGa?3fZg>*aVs}}v^L%&A|WNwzq4p>UB=(!)%U)VwuyluVf3R*S@Q#>&qs#< zlK@2dK6L7M+QHfBvCh(ci}z1~!U|9sZ7236MudQ4$iKhOHCFQ`E~kQayQVST>Uij2qVwzTetsn%0Q5WnT7dUGK{p?~aUqS8Vk?9R>UGq#0O{Jy?o1vxqqaC0 zSH&SvVUY6e4kXX{Y@)=yLE`-a7nUu*(xtvb$!ejmG)v2z0@FFFf-m8ZM$FyUhg^3Zv)nhsT_o7>4^eiO`k)FM1pRs2 zyf^qc*bPJPs%)N&)Yxp)EZIs9&D{`z%$$qcS~TMRmakJhp>e)S>+aJt2b11JxEJrD zqqU>hU|UgRSF_~pnZ?)o-Q>q-TO#X>iYl^?Wu8kc)jMVeGdf$E?L_y(HXfLZ4pvK+y~(F0pW}+lJZl=R1jmQbP6uZCH??TIW1KNyTOQp; zqlY6WgZf&^2JR}{dUwg@-6(dv(uLct9;?< zbG~;E&ev9+bf}1xy6jsv$Uh^(xA?oBwjvT@cWw`#I(rUp%xKw(&$k>PqciqvB=?%g zTAWZWG<&g4qisK*aV=`Yj>G8erYl2mj0@WU5y8J)n$<=y?8rQKs*!1o=;9o;eQQtH ztooe9M|rMQNa!}V|I$y$`PpP~vOX~WeeJUL>aRhCJHNOXAP=8Dxbqj5NW-9pR!U$P zuaJ@NL2!GEpM770r+f4hc}$pYw%*6K&A-N*0U&BKp@blwpOcKCB-TY4>MuAksQ>nr zlYxJ892Dy{u7ekC3s~lO+3a!XLZ~cwLkZ;z@II7k$ker=&>(OfcBo&-e#t+vF#fSx zQqT*f4>kY(ehdEZiTMpa=I0#MEFRk96%#DkVF&bLZ2W)y%*Zr;{7SJ)c9s1UU2#LW zW6$TF7tmO5X_mT5&DO7j{gA9?`!^eiG9iE>TrFTwc{XBsfHgyTbp7-g;>aHn3=tu6 zG`5ExTNTUwOkr}Z_#DV~h&(=w%7xYwA!NA!k_XG-jR_0lArvB3WUyTrO%T=I_phoM z7V_rf42n4CMxlsqV}{tg&aXSP=3;s4{K-nYrCgXA6|O5Ul-W?Cycxzu*-+Rr2~tl7 zh7du>XYujY5X}K1gs?-K9C@@aT5$;~qXmJ& z2~;?VMP<=yy0w=eIUcE2k-MjOj+dFv8{ZVksREj?Eh#jrDstIxo8cJToB>G%cY?xm zA=^gb@<}2Dq@fGO6d*JSBq3V&O(@Co&5{W6wRaD{YvS@$r}62-sDOXZ>%L1GvLSDu3r?f9eP$V^o*Z<;76>SM zCgIIi&OCx`ETW^v^(y_ecbeT*LE}og0xOK)qti;gD6WlfGQR6pd@fe2TT~h*Z`U(c zY8WIb<e?i2JvlcSficOCkpI&3@!XTm zMX8ACEybdX`88#BvPBn_$b!;>Tdw}D%@TCJ#mC}`D!VlP8}xcP%c3J(jy~4wh;Kw_D?0mnLwmB@7GF_%A@QteDCzBbH8pX40LC=r3B9!COKzn^yp}Z);av% z*0bFbVIVtY<(#*Hv%Of8A|~$nHFnv(IY`I|RD{-gaA7~2 z_~0T2p$&sC#(u9^@x-nAP$O$T%ux(sLonrUo*nW!ZP+A4Ikt;r9EPE=wvGeA?|T#i zhH1$60*-uSmrTrnz%O~|(H{l`>M(rdpFgX^cRm_bSX6`=&QehxB_y~5l^ulC)kPnb zNEXd|#3n^K#Bj;>b1R=pP^nCm$Inpt-GVN{EbEJ@sFFB}^fWNZ0OTA{}OfRvl32CQJ`!uUV?eOH1PE zO{eSKT8n_%JN_o1Te3D$xgU3)=b`9lWjvzOac0aeqU~^Y7s^TQ(O7M68*YJlK?C&1UiAuLuHtBQ|O-)L= zlnQ5iuFOB%nd648m8Wx;L9F)heXV0rY-2Iu0c6co0#!Ckl47_hT5-$g;59_+-X958 zU>;2upm?HVoW1G2hT6Xb$S~ny4|SBcF*=NR7+xL-B8{t$utR>Ji?OguNklRY5=O;v zrSaTYr16b7jakBCGv{zD(qtAzVxbWduJS0%jwlmGL~5{PT7Gc3kD*!(m@py6uh~G> zDw^J){)>fK6zujvhfZYuKbj;FkTZWT2f4?kX~vWsH%#L{8f%^Hv$5=M)nwU88HYc;_R~|kJ8<2KfKGWZR zHqiISnypcfhdNGV9LnC~G8x5t(jg8D<9+NYv@C3|@@T&KW46OJsI5+5d*pWo8{M!uU)9~1?JXoA9Vq?NjK$X zwdUW!+XLltPB;+ckr4IxS6Ncv$6vp{fd+9pPna4o^86|I4Et@UB7Xd*z4dI?xz60g zlFly!4z5*OZoj{hwY})bwy%99Y0*J3QFD3GbeAg=#pkNb9qgN$q_vvSK(?Hny8EDw zH#%D1k{@>Q6nF5h_{Q- z_w|{^)~3I``|383&U$&mEB*&YF75OrM%~NO1jJUJLEQj z#>kiMd{c1Wr#1%UwO;%!*C$?{Yy-E1-j6mFr$;vyL{+DU1IPgq2FMm5UOwH)UmmTW zzMOpLWJJ%|?B%67@F+i*8nirG1seLJjR0LL!I7ZF!IR)}5P%t@qm`hH56UDHjeNi= zAg^WNIp*5%*Kz=iacEcoKmvRVfCm6Jz`evYCzw}Kge6s!bY5aeI50ER2@#+maKtBC zcPK(C0;mHZ2fzv72f!z#v10&c0A>JC0q6o;O{r`u2v&g{7a$F?KnOxypoJ1sYY;Ic z1V9BK1bCGg#_*>$0Wc~6uJE2!_6-SiSz7=n!4uR1N&_XO6U4m$1<|7^rJm7v&vAey zfSQvhSWh-O?(->z5vu-|bT>e0jdO8(f6`7UsKK3@BSpB08@KlohyXQ?REDQj^5 z_#b|%NBkS{&apotf!ef2iuCcC>N6`dFBnX`bB!2YMsqDHcxCWwG+7KLxqj~U2@3Pg z=ImY~euJZrora=I9a#n(H+XfL+8Ir5b4^ythmR4ABPOBEFXWdsW&<2!#%s_Tgo3w) zie}@X!5JbGVh;ImIJ4FrZRCr}P{}So#GST-R((a@Ae(@k1WA4QGPB zJrZdgj>rvhbh9rO8{bJ=k*&nqQT#%Vw5EvFas1*geq(r?G*YLfz@x$Sc-rW@Q?W6R z3vpzXeP$O_)_vy^$%NuPSGOlOzc~6xZBQM>3@*DEvsmCoCbKJcBXBq+4x$fpgX&cfzwO{q?n*EfvyMmg9#ZmIZN<$*M;(8ai$~&fh zNuGm3%kHj2A9Xw%#B$P5_GI^dvTo6Zq9q7eLJcCA;%Oo(%RzoQ%lV-dV5Ani5oQoB z^LFDIV~P-Ah?vSyM^DKJ*25s^|RSgdr7;TsxbiK%2^AVnfmH?GT(N z8DqwR8jcQd4Gf$RWZ*OqR2Y1LrMSV?!^mJ*Mz0j)uYWqB!=(s4ix!A0$b#eyFvw&a z3{VS=FL0tISSozFH#09mLFB6>h&4$)TAK4*jUg(kLCnEir^28FHj-$OOH;ED!7j7W zEjQ1+T1MV6xf}{pq;loMhqH{Se#IEyu3tkppenN@>_Y?65>mPAUeG0|{5VHpI}=fw ztY9qL)h%l~d;xa&(?R%#`|EeFg?2%R@&XDL7f3|PE%?YBrFOv{AXm=JVkJ;777bH? zwT{h2Ds4gg*qteTkmbr-Q-&3*I5=gGC-uQH-pN%hhGw84HF~&`vp1;ouw!N=HH0ZS zBikbKq2-Tg5?g1DMz>Fbrr~b)B;G-@+|iQ79=zWP?t>tnP36w)^KPVVZQd4R+`xLh z<3aa%-p?O3+JC@et_44^Dc16(gdw$sja%wQm8b{06^&lM4l*p(R(Z$9g*~3y5)DDG zeQk01LsOum0~F(ghF)>|=;3J1K{3o^uO!7IipB)G)iIAvQ|BS%a~2s7#ok*J#ubQ7 zuf`kwUMnO*di4oR!M%8c4e=7CjLMF{^RX)OC$Qoj8-G`JuF!aEqVWOyHl133$%|IQ zzII8n%xSh;ZFV^nT5>epb^A>lo??~lhuomNYYpKde6B6MW$gXL!%gq8A??TaU28S5 zAzZ)y>UASl_62(0L9LZ0irRB+aWZd4s94g>%TK(w{hlR_p4ja4v;DXB6sRSYGcM@_ z-urji=B(c4{gp|kKgK@fb9U`J8^gNVTGw6antK1zC(forIcq(;usWm7{Ic#cS^MdW zLkBMR-g#320$+H$a&!Kp%_sY+sJk5xVKa?Kh00E{Zpb)k#)=cdV&Ska?ef8EpNIi8 z_YDUNY{dr)4_SwkJaMH0|Kj9{SJyb*itTODW&gOM=&OIOtlytC=V;5^`mv*I=rIjz z@L;&Ya#e+iPL??P&qQ$jH~rcslV1KkZlopzp#Z@u=b&?U6zwJSw1f!A@g^aiM_ntSt2p&N=C$}9ib`&fPmqT*z3adl1WQ~ecVabx0YBUT=2dhMl#)cUstqEj^kb*)S_#CpnfuQstTgpX`I=>0 zwknUs%l$_%wIQM1pEi{Z_h-wg+^|*mK7H`E4O`B2h2KpGoTTZ;b}3YyYI{v!X2>$_ zWm&9bm3&rPl4FUYLVFLvtO(qh(7DC8`gfpS`|i^VWR281bkA1vw=ZeHeoQtRHOOVr2ahG zo=?Rk*^2cOAV|;9)0b$33~gQC9H(fQ$_X?OEkv00AA)f)QA%9e$mrVF7hySo^H)N9M<&Y(*AHz=_%R$zSXxd-V6sSz>90{wrFbq;N;zS}e`PCVRp zrR$kjUcRVrCJDVz2g(Vkf9= zW~3Xa+ZUUfEkCI*b!gh>Z*QA%_VL0eGY9*a>gH8j_jTeKp5CoDtJ9ii`(M8t$$2z( znMQwnw*%|wyVJ_L>5Rbb>Wr05JwHC!w`2Ch>t~P0f1UHF&#&zKHurB>L|d4bwX4fQ zOm6e%p8V_OM~YjkJQ?LXcP`mb2j2CT)MbRu_nl0M^!c{<9kk;w6~}?IrB8;c`s_l4 z1*s2Pvv;~0eE9U!J2Cp<^D%J5_5Qi!e4+5u@5R2pFXc%AuWx15o+<>QD)7$Cs@TFe zy1N_J0c3^B?W_=*7Wu@Y1(a zj0U^{SOo9^U=a|^vJnUXpa8ESZx#&@#J(X(dxC9VZsr|jW}rL93>N@m04M_pgKXvx zhy-8+U?Tt-0FnT0074=Q>H$fB#tDEV*H0oqOMpmXga!a93c@1+xB#pPnV=HW%K@w? z<~9N1QHcb9{BJr9aFUv0bhuGHm^ZUenTdZRF-zySW&KT<0mK2U1L9(?gH_?^)TmJ* zxSeCcjRt%MFsp4*_yz!0xbB3C2ce465HcYFQ)g;s0zg`>qZ6eu0zh^D5gG&c zX+Znr{}wd=0XJx6)BlDWhnfDrp|Ssea8s>%=Kr;feAQK9v)Zfi)<{yd*Z;nZ9P)H} z^~D?SYu5!>2^vboxH}H) z)S}q6iY=pVcuW&K;_S?HUG><^3qLzP7CY-T&CcFd^{-OmlBJ*Wp7iRu6$Y}uKmRNs zd09KGUGw7M!y2%9d+Yo z{%epG3kyE!JrB=G_lo}&5Iu0(=*aV_I#mkKV=1it~OZO|FJQ z*?7?a=Va7lhXK&%Uj&vYQ)v5c`Tw#!qegaXkhx`kP(~b;FmS5%tTurV{xIzJd$h4u z-bJE93bAahzn06zbHsf!(Pti6D!jxxD$ozeh_`I*Eo>bK=#0d#xBc+VcjU{3obJv8 zuj5FC#HsIl<;qvsnJ>0a#kt~gwT~~fFR`Apk?z)$wrT~&zTN6Z5c9y2WKW{$eXse9 zOw1ZjWS3@M$sGn|mvHBzI!yc4jT1%%Bs$b@OhLu*HxcThd?ZMc;(ulP@_6RT&sPt< zmVUkxX`WqLrvXRP*Q71r4XivU*q^VO=KHqP-~K*nED%BE44NTN*UfKXYn$?FxbS1B zH7g(bmFl@(OZm-=ufMMpaUfP`A>;!Sg&_>5XbO9j1EmQ45-1vrpuvr7yF9@+TMhvV zHD_hXG(gj==lfOg!w#wq667{UsG6D}Q>%SA(~lo2e?oLy^SR)tOfv{#v+a?B2MOc% zsHl}JhM1%&<%j%)s+h;|^layIK2|(TRcXmoV}Y40a8Bl@1Xd$;h8rd5$*EZ&;3XrF zNY6UW3Spma`~2}uRJisC6@doJR|6Nh@4jbu$PWmSZ25Zif=Cfs3`K8daEJ(8wrgLP ztYD01aA_E=WIF}3C5SOLv5+-8yV5?L%77!O9)53xu!aQ=Eb#82PW$Ld8kJ+_E6UOs zk4I7(^mU8gW^AffgyG947Zl6+rb`cYEfF)VPUd7sn06txIwA5{OOuGFfDDXy)#YDwysfMcv;3y`C;*O@lDw!+1Eu3L9>!NP; zC)ivQYK{CvUCq4aTW4kV=QC$UoYad0+WsVbJ!AS6rKTywI4?~d=``VN49#xr$UI?m zCO1PL;WT;z`Fbr2yx>jx2;vc1If*PZXlF0&~e z+w!ASHt_V(HFd2;F0sl3J$Dt?Uv|{)Qd#?IBB`V#`w3AsxY#s9YM-g+x+H1}yf26< z4?oxb1npQ@uCTrrmK6Y<_6Z3XzGOay@s0Ip2pi#?mbL}9vK6q^%`lViDJQR{4at(O z?msS-1h`NdTR5#nBmzgbzGF=P{C$v~!sYy;kah`A58?}i=&BWr!S&Vd2V56hu9}Q^ zT=o3*Old4-yW|}G2H_WvK;XzCRG+qcBnLDVOG8>}5jWLy{Iuf@fi7os5biTb;xc=n+pt0FJ+=WX1NBiE> zEkCCMMh73Z>|IOK{Q^Ne`rdz=5-_SZrrG&Iwp1^;T#Wnq)o$|zC!C`YZgqd7xh4UP z?x_6iZed?!%`ypnt$&;tl4o^uPm0XO&07< z@_Vb~$S85=gp~LM_511sHkW@)%qqXTwJ+jT_Kh#B?bfSiS1iw zJFC{kZ&u~bEWAwgsp~y>@>EDvh4dA+dKiJX`#5tEUyf4{uG@2VIve9jliv#sxtNAO z`-`A4lGC?q|1Fp-r)AYs^y9+Dy?BX8rt9|&75KQ^oQ3<)2+Lij0m)TP++*l@){-d%kh#DHD$ z@Uv>N!E!UlSsR8uX6p>G6~PPpZ<+i_x~WE<&vJxxzda^&zHd^kqQwt=o6=4!*x|IyX%U_gZ zdHShd=uMp@$eCRZB^GzSZXVKAF8CSvRDW8U-R!z-ESG!xUES%&uGY))Y0MYD%DRaXBn68)&SoEPPRedZ zguhA|x`9eyCKnE)^M&keDk>O~tTevCk_r#XPSR(f%Oofdi)~-uy7*90S?3R@K+p%W zxh4(VwB|$ER7@EalTFN6-5WEj4)>s<(j}+_79x$h8rMp+7Rq@4+1W8qSyWx>%bu^5Hl0V|5er5_%pfxfBf2wZDyMp zPFiS$BeWzDIgHIskrpFKOqsjp<{*UXnhitcCM0*cS)!bhC}sN2O*oR~CTGW8BONN| zd{w9%@x9LX_jvpskMCb`UC+9qDb!YZGNihr>WY1=ZZzY|e%D zF+KWt@536}t+6xZ_xD{D4^GWirFb!;FWf6g*pvO>gIG8y7AM697Ntj%*%xZkPU#0E z4X?g?QjwAvK(A_S4LNGr_H{CF;Io*-P_cB}@9xvybaQ@8@_t^vF;??-a^#rvo=r+&hlIo}(4e&=o9^5Sblo3c~If(My?aBJ42Z1577-YjmW=8K;!_SstBGl|M> z$ngLCtgt%o{NwEFQ1sH0!x@qG?N2ON87P4k5?05JIE~l5){DfyeNWtWY7@C!l8Nk5jg&k z2mN%@Y@7RQ%XR(RMvAN2D;8^aISQ1ZADrqfb?Dc)ltv{ z23piZH8hAbppz~Te26OJ%n34EHxZevW?L;tN)$2}|D-9A6(*6sLMR2vYLZ3zOI>Ul z$D5cw<1`ntjd|TD)JKEJ!^wS-#4?12^djvPsQeW_AfJB1h48|bT;Mw??@y0*uqlWn zg9ML%aKXk5QerxzsEn`-z<0FC`L~)e;#b`hp^& z@d`IcGvwxPD6fcvK#El@hoB1LArG<)>crVH21;=2GDTdPxB9ZohH)D|i)bmyhNQWP{$3DH>4Cy9HvoMfwJ6EnwRG`b5DA&-E@I2c*6 zAZ<-_W8iFy!7~dm$}^kFY9O* z)Tzvt>~nBfUvO#C`&HM(#Ps;%eEzSQ#(M;qj*mBAl;T6ukAJJQKU{`Iz&c2rt|OH$ zytU5>I_?G9t~mz#>O%T-kRG?f|6It)=TqY=gQtw5ow55oi}Fw2`MDlpv6E8AIkDt$ zjmqd5o@$E@E3~kgb9(#tOX~DvfvWLxvSFidyXS6MUNmo*{j6Atsy`bnOz#z_HqQJU zNSzztUh@|Q8DK-7R%g%_dOw8Kul@5iXvor>zgxN^Dsqu08s z942K5TrQkHVrK*%?Y;21ebh_y_dtE(MTJQ9LOr@hK9F{1g`Sb>M>V5+YwjW-(V>`z~-8x4*Zfi*nhq?d30 zp=q^OYBh>Z(kzW{H9T^`iqG5p4K9$uWx2mq{sFwH^GC5awFS1rAf+hSLs0|nM1DV| z7;@rq;nDe0JJgaE5f}4qo_;ip5~`_7a)Rpy11iX>?4pGu@X%sYOzpvMpW!#je(ytS ze{ejTiZvMJb=Q0MZ|W@Q0$|pA$D@V0Y^_?Ok|ExMthXP2QFL5^Yk40g?mUTh8aBGr zJ@!a#Yp#!%af8JQKYi<2z5Co|J;zrs)k72yaBpw0IKoG=)aSXa$0j*{`MbjQFt$mb zY@_C0bfNgP{oNmZP5t5f=L>dF@x;YX;mur*QQqq z)eoJ<(syQT^>rAwKyIni4dk5I~#HM}TF z4MRf0UPmGfamzWn3t#(h$V1@hS8?u-P2_MVZI?`5W9N^NrmFZQ=JFQL+Tp*nZ4ilk zr2*$Or0QkiQuM>N^CUc za*X?a5aaC9Nh($2Kcc@@~<3o2aE}^5>WJ^!8JPZzkPyayW?!3 zE_qL?3yZ$3j@iKjX;@V$8+AnvSAN0StKV^HqB z;340bWCv*ng7nS&g~^?~Y==~!m5tmXry!nrj@SuIGxr&uA}Ln_=-}%LaM^It5gDIw z>e``<)=U1pVu#YzhlS2m5FqDD2xh;*50i6Tb6xT9NvKz#;#tUp^f^V>`%yyh>N8*O zOB-rJd;ttMW==i^-Md9L!*Dcq*~{?LkJ7&)V0l27t|W8wFiEV6nF#ayY3og%1E@n< z#}zVMmb5V3!{rEd7Yd2Z6s&q2CY|fBa)lIV<5!Wf06)e$s{nQmT|TQ*@2uWrq@Zt9 zvdZwWi>Kca+-2$P1{u1eMB75&cVTTi=_x5mo-Zi&D4S4z7T5R2t@CvRC!O%4=viem zN(wqFz{2-i`n(zYa0@cuHxHjx^auAc`XyqVos6H23rnW z^1|JN@DmdHP&FYk(p_SQ*%KRJ&T;AnE54)WW{ z$?gsuhX!(9+{wKldW7F9x?4C)@%W~#tn)-a?|gze{5h(1ci$m)vWincs<1_gWqK^! zcve~Ke#afvyFPY$bcIhvTYjYF0_wGFIcmFTJA+LxUOsVyxV^sHV9YJ6yqT>O#x~mm z{(Pz4mw&+IhsO~^U=~OI7}sWOba26qGyfd&G|k{Xnl#1F2gWkPek1dGeO*Cm(VmI-b(e&#t7N1`jg_RFim6sxbd1q;k+eR*3&5f9$ zEe0)m5f7kGBI2hd3I3;7v1RK+d;>09!B|E^Gr@(;q*a>X0!`JlJZx87$+P>YZJxzI6z4iuaCqp{&~P^)|5^?AcC&4oq;PU-IfuUsCsUyj<(kP_3F zJY3V1AB%5{fo_a+5|&Lf%rrXR*qI=;G$p|)n_b39hR7Y`&}|YCh>a{p5UC)z2)FVC zq+bXYgbag^AU55Qt{KmaDNRlzaM(!obZB!T!8yYaUrftTR zy+i^Vo%%ziR`X7vig=!lT+*jza)0xk+K@UdSXQJs4NU*L+w!1YgCxpa?bPf6%G$xj z3gaIOKj;vqx2!3I+rGWYlE}g^7l$5fhD{xi$YvgOc0}b+FBVW>0Nbc%ug-y0^8x_O xxpf+>4OM>-;pybH9&!rv<-b#x4Rv}9Coql^L^*Yi!s46`^^6FM3k84!{{h*rqxJv* literal 0 HcmV?d00001 diff --git a/doc/images/XMLreference/tendon_armature_dark.gif b/doc/images/XMLreference/tendon_armature_dark.gif new file mode 100644 index 0000000000000000000000000000000000000000..9860860dc1b7a26181c5b9a47606122e7d4555c3 GIT binary patch literal 135942 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%0DAd4<989(kM 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%0DOq=hO?lAV_3Oo;FDfxbMm`hXt~ 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 literal 0 HcmV?d00001 diff --git a/src/engine/engine_core_smooth.c b/src/engine/engine_core_smooth.c index 22870d7b..0bb97dc7 100644 --- a/src/engine/engine_core_smooth.c +++ b/src/engine/engine_core_smooth.c @@ -865,15 +865,7 @@ void mj_tendon(const mjModel* m, mjData* d) { void mj_tendonDot(const mjModel* m, mjData* d, int id, mjtNum* Jdot) { int nv = m->nv; - // allocate stack arrays - mjtNum *jac1, *jac2, *jacdif, *tmp; - mj_markStack(d); - jac1 = mjSTACKALLOC(d, 3*nv, mjtNum); - jac2 = mjSTACKALLOC(d, 3*nv, mjtNum); - jacdif = mjSTACKALLOC(d, 3*nv, mjtNum); - tmp = mjSTACKALLOC(d, nv, mjtNum); - - // return if tendon id is invalid + // tendon id is invalid: return if (id < 0 || id >= m->ntendon) { return; } @@ -887,6 +879,13 @@ void mj_tendonDot(const mjModel* m, mjData* d, int id, mjtNum* Jdot) { return; } + // allocate stack arrays + mj_markStack(d); + mjtNum* jac1 = mjSTACKALLOC(d, 3*nv, mjtNum); + mjtNum* jac2 = mjSTACKALLOC(d, 3*nv, mjtNum); + mjtNum* jacdif = mjSTACKALLOC(d, 3*nv, mjtNum); + mjtNum* tmp = mjSTACKALLOC(d, nv, mjtNum); + // process spatial tendon mjtNum divisor = 1; int wraptype, j = 0; @@ -1470,6 +1469,63 @@ void mj_transmission(const mjModel* m, mjData* d) { //-------------------------- inertia --------------------------------------------------------------- +// add tendon armature to qM +void mj_tendonArmature(const mjModel* m, mjData* d) { + TM_START; + int nv = m->nv, ntendon = m->ntendon, issparse = mj_isSparse(m); + + for (int k=0; k < ntendon; k++) { + mjtNum armature = m->tendon_armature[k]; + + if (!armature) { + continue; + } + + // dense + if (!issparse) { + mjtNum* ten_J = d->ten_J + nv*k; + for (int i=0; i < m->nv; i++) { + int Madr = m->dof_Madr[i]; + for (int j = i; j >= 0; j = m->dof_parentid[j]) { + d->qM[Madr++] += armature * ten_J[j] * ten_J[i]; + } + } + } + + // sparse + else { + // get sparse info for tendon k + int rowadr = d->ten_J_rowadr[k]; + int rownnz = d->ten_J_rownnz[k]; + const int* colind = d->ten_J_colind + rowadr; + mjtNum* ten_J = d->ten_J + rowadr; + + // iterate forward on nonzero rows i + for (int adr_i=0; adr_i < rownnz; adr_i++) { + int i = colind[adr_i]; + int Madr = m->dof_Madr[i]; + int adr_j = rownnz - 1; + + // iterate backward on ancestors of i, find matching column j + for (int j = i; j >= 0; j = m->dof_parentid[j]) { + // reduce adr_j until column index is no bigger than j + while (colind[adr_j] > j && adr_j >= 0) { + adr_j--; + } + + // found match, update qM + if (colind[adr_j] == j) { + d->qM[Madr++] += armature * ten_J[adr_j] * ten_J[adr_i]; + } + } + } + } + } + TM_END(mjTIMER_POS_INERTIA); +} + + + // composite rigid body inertia algorithm void mj_crb(const mjModel* m, mjData* d) { TM_START; @@ -2321,3 +2377,53 @@ void mj_rnePostConstraint(const mjModel* m, mjData* d) { mju_addTo(d->cfrc_int+6*m->body_parentid[j], d->cfrc_int+6*j, 6); } } + + + +// add bias force due to tendon armature +void mj_tendonBias(const mjModel* m, mjData* d, mjtNum* qfrc) { + int ntendon = m->ntendon, nv = m->nv, issparse = mj_isSparse(m); + mjtNum* ten_Jdot = NULL; + mj_markStack(d); + + // add bias term due to tendon armature + for (int i=0; i < ntendon; i++) { + mjtNum armature = m->tendon_armature[i]; + + // no armature: skip + if (!armature) { + continue; + } + + // allocate if required + if (!ten_Jdot) { + ten_Jdot = mjSTACKALLOC(d, nv, mjtNum); + } + + // get dense d/dt(tendon Jacobian) for tendon i + mj_tendonDot(m, d, i, ten_Jdot); + + // add bias term: qfrc += ten_J * armature * dot(ten_Jdot, qvel) + mjtNum coef = armature * mju_dot(ten_Jdot, d->qvel, nv); + + if (coef) { + // dense + if (!issparse) { + mju_addToScl(qfrc, d->ten_J + nv*i, coef, nv); + } + + // sparse + else { + int nnz = d->ten_J_rownnz[i]; + int adr = d->ten_J_rowadr[i]; + const int* colind = d->ten_J_colind + adr; + const mjtNum* ten_J = d->ten_J + adr; + for (int j=0; j < nnz; j++) { + qfrc[colind[j]] += coef * ten_J[j]; + } + } + } + } + + mj_freeStack(d); +} diff --git a/src/engine/engine_core_smooth.h b/src/engine/engine_core_smooth.h index 41d06775..4bc124b0 100644 --- a/src/engine/engine_core_smooth.h +++ b/src/engine/engine_core_smooth.h @@ -51,6 +51,9 @@ MJAPI void mj_transmission(const mjModel* m, mjData* d); // composite rigid body inertia algorithm MJAPI void mj_crb(const mjModel* m, mjData* d); +// add tendon armature to qM +MJAPI void mj_tendonArmature(const mjModel* m, mjData* d); + // sparse L'*D*L factorizaton of inertia-like matrix M, assumed spd (legacy implementation) MJAPI void mj_factorI_legacy(const mjModel* m, mjData* d, const mjtNum* M, mjtNum* qLD, mjtNum* qLDiagInv); @@ -99,6 +102,12 @@ MJAPI void mj_rne(const mjModel* m, mjData* d, int flg_acc, mjtNum* result); // RNE with complete data: compute cacc, cfrc_ext, cfrc_int MJAPI void mj_rnePostConstraint(const mjModel* m, mjData* d); + +//-------------------------- tendon bias ----------------------------------------------------------- + +// add bias force due to tendon armature +MJAPI void mj_tendonBias(const mjModel* m, mjData* d, mjtNum* qfrc); + #ifdef __cplusplus } #endif diff --git a/src/engine/engine_forward.c b/src/engine/engine_forward.c index a61d48a0..d55f857b 100644 --- a/src/engine/engine_forward.c +++ b/src/engine/engine_forward.c @@ -114,8 +114,9 @@ typedef struct mjFwdPositionArgs_ mjFwdPositionArgs; // wrapper for mj_crb and mj_factorM void* mj_inertialThreaded(void* args) { mjFwdPositionArgs* forward_args = (mjFwdPositionArgs*) args; - mj_crb(forward_args->m, forward_args->d); // timed internally (POS_INERTIA) - mj_factorM(forward_args->m, forward_args->d); // timed internally (POS_INERTIA) + mj_crb(forward_args->m, forward_args->d); // timed internally (POS_INERTIA) + mj_tendonArmature(forward_args->m, forward_args->d); // timed internally (POS_INERTIA) + mj_factorM(forward_args->m, forward_args->d); // timed internally (POS_INERTIA) return NULL; } @@ -142,9 +143,10 @@ void mj_fwdPosition(const mjModel* m, mjData* d) { // no threadpool: inertia and collision on main thread if (!d->threadpool) { - mj_crb(m, d); // timed internally (POS_INERTIA) - mj_factorM(m, d); // timed internally (POS_INERTIA) - mj_collision(m, d); // timed internally (POS_COLLISION) + mj_crb(m, d); // timed internally (POS_INERTIA) + mj_tendonArmature(m, d); // timed internally (POS_INERTIA) + mj_factorM(m, d); // timed internally (POS_INERTIA) + mj_collision(m, d); // timed internally (POS_COLLISION) } // have threadpool: inertia and collision on separate threads @@ -222,6 +224,9 @@ void mj_fwdVelocity(const mjModel* m, mjData* d) { // compute qfrc_bias with abbreviated RNE (without acceleration) mj_rne(m, d, 0, d->qfrc_bias); + // add bias force due to tendon armature + mj_tendonBias(m, d, d->qfrc_bias); + TM_END(mjTIMER_VELOCITY); } diff --git a/src/engine/engine_inverse.c b/src/engine/engine_inverse.c index f372947a..c4042401 100644 --- a/src/engine/engine_inverse.c +++ b/src/engine/engine_inverse.c @@ -45,8 +45,9 @@ void mj_invPosition(const mjModel* m, mjData* d) { mj_tendon(m, d); TM_END(mjTIMER_POS_KINEMATICS); - mj_crb(m, d); // timed internally (POS_INERTIA) - mj_factorM(m, d); // timed internally (POS_INERTIA) + mj_crb(m, d); // timed internally (POS_INERTIA) + mj_tendonArmature(m, d); // timed internally (POS_INERTIA) + mj_factorM(m, d); // timed internally (POS_INERTIA) mj_collision(m, d); // timed internally (POS_COLLISION) diff --git a/src/engine/engine_setconst.c b/src/engine/engine_setconst.c index 73f8919e..eece506c 100644 --- a/src/engine/engine_setconst.c +++ b/src/engine/engine_setconst.c @@ -102,10 +102,11 @@ static void set0(mjModel* m, mjData* d) { memset(m->flex_rigid, 0, m->nflex); // run remaining computations + mj_tendon(m, d); mj_crb(m, d); + mj_tendonArmature(m, d); mj_factorM(m, d); mj_flex(m, d); - mj_tendon(m, d); mj_transmission(m, d); // restore flex rigidity diff --git a/src/user/user_model.cc b/src/user/user_model.cc index e49c6112..2b5dc102 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -2769,31 +2769,6 @@ void mjCModel::CopyTree(mjModel* m) { } } m->nB = nB; - - // set dof_simplenum - int count = 0; - for (int i=nv-1; i >= 0; i--) { - if (m->body_simple[m->dof_bodyid[i]]) { - count++; // increment counter - } else { - count = 0; // reset - } - m->dof_simplenum[i] = count; - } - - // compute nC - int nOD = 0; // number of off-diagonal (non-simple) parent dofs - for (int i=0; i < nv; i++) { - // count ancestor (off-diagonal) dofs - if (!m->dof_simplenum[i]) { - int j = i; - while (j >= 0) { - if (j != i) nOD++; - j = m->dof_parentid[j]; - } - } - } - m->nC = nC = nOD + nv; } // copy plugin data @@ -3564,6 +3539,54 @@ void mjCModel::CopyObjects(mjModel* m) { +// finalize simple bodies/dofs including tendon information +void mjCModel::FinalizeSimple(mjModel* m) { + // demote bodies affected by inertia-bearing tendon to non-simple + for (int i=0; i < ntendon; i++) { + if (m->tendon_armature[i] == 0) { + continue; + } + int adr = m->tendon_adr[i]; + int num = m->tendon_num[i]; + for (int j=adr; j < adr+num; j++) { + int objid = m->wrap_objid[j]; + if (m->wrap_type[j] == mjWRAP_SITE) { + m->body_simple[m->site_bodyid[objid]] = 0; + } + if (m->wrap_type[j] == mjWRAP_CYLINDER || m->wrap_type[j] == mjWRAP_SPHERE) { + m->body_simple[m->geom_bodyid[objid]] = 0; + } + } + } + + // set dof_simplenum + int count = 0; + for (int i=nv-1; i >= 0; i--) { + if (m->body_simple[m->dof_bodyid[i]]) { + count++; // increment counter + } else { + count = 0; // reset + } + m->dof_simplenum[i] = count; + } + + // compute nC + int nOD = 0; // number of off-diagonal (non-simple) parent dofs + for (int i=0; i < nv; i++) { + // count ancestor (off-diagonal) dofs + if (!m->dof_simplenum[i]) { + int j = i; + while (j >= 0) { + if (j != i) nOD++; + j = m->dof_parentid[j]; + } + } + } + m->nC = nC = nOD + nv; +} + + + // save the current state template void mjCModel::SaveState(const std::string& state_name, const T* qpos, const T* qvel, const T* act, @@ -4509,6 +4532,9 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) { // copy objects outsite kinematic tree (including keyframes) CopyObjects(m); + // finalize simple bodies/dofs including tendon information + FinalizeSimple(m); + // compute non-zeros in actuator_moment m->nJmom = nJmom = CountNJmom(m); diff --git a/src/user/user_model.h b/src/user/user_model.h index c55384bf..e8e3cc8c 100644 --- a/src/user/user_model.h +++ b/src/user/user_model.h @@ -349,6 +349,7 @@ class mjCModel : public mjCModel_, private mjSpec { void CopyPaths(mjModel*); // copy paths, compute path addresses void CopyObjects(mjModel*); // copy objects outside kinematic tree void CopyTree(mjModel*); // copy objects inside kinematic tree + void FinalizeSimple(mjModel* m); // finalize simple bodies/dofs including tendon information void CopyPlugins(mjModel*); // copy plugin data int CountNJmom(const mjModel* m); // compute number of non-zeros in actuator_moment matrix diff --git a/test/engine/engine_core_smooth_test.cc b/test/engine/engine_core_smooth_test.cc index 8f24a99a..1d383922 100644 --- a/test/engine/engine_core_smooth_test.cc +++ b/test/engine/engine_core_smooth_test.cc @@ -17,6 +17,7 @@ #include "src/engine/engine_core_smooth.h" #include "src/engine/engine_util_sparse.h" +#include #include #include #include @@ -213,6 +214,183 @@ TEST_F(CoreSmoothTest, TendonJdot) { } } +static const char* const kTen_offtree = + "engine/testdata/core_smooth/ten_armature_offtree.xml"; + +TEST_F(CoreSmoothTest, TendonArmature) { + const std::string xml_path = GetTestDataFilePath(kTen_offtree); + char error[1024]; + mjModel* m = mj_loadXML(xml_path.c_str(), nullptr, error, sizeof(error)); + ASSERT_THAT(m, NotNull()) << "Failed to load model: " << error; + int nv = m->nv; + mjData* d = mj_makeData(m); + + for (mjtJacobian sparsity : {mjJAC_DENSE, mjJAC_SPARSE}) { + m->opt.jacobian = sparsity; + + mj_forward(m, d); + + // get full M, includes both CRB and tendon inertia + vector M(nv*nv); + mj_fullM(m, M.data(), d->qM); + + // put only CRB inertia in M2 + mj_crb(m, d); + vector M2(nv*nv); + mj_fullM(m, M2.data(), d->qM); + + vector ten_J(nv); // tendon Jacobian + vector ten_M(nv*nv); // tendon inertia + + // add tendon inertias to M2 using outer product + for (int j=0; j < m->ntendon; j++) { + // get tendon Jacobian + if (mj_isSparse(m)) { + int rowadr = d->ten_J_rowadr[j]; + int* rownnz = d->ten_J_rownnz + j; + int zero = 0; + mju_sparse2dense(ten_J.data(), d->ten_J + rowadr, 1, nv, + rownnz, &zero, d->ten_J_colind + rowadr); + } else { + mju_copy(ten_J.data(), d->ten_J + j*nv, nv); + } + + // get tendon inertia only, using outer product + mju_mulMatMat(ten_M.data(), ten_J.data(), ten_J.data(), nv, 1, nv); + mju_scl(ten_M.data(), ten_M.data(), m->tendon_armature[j], nv * nv); + + // manually add values, at nonzeros only + for (int i=0; i < nv*nv; i++) { + if (M[i]) M2[i] += ten_M[i]; + } + } + + // expect matrices to match + EXPECT_THAT(M2, Pointwise(DoubleNear(1e-9), M)); + } + + mj_deleteData(d); + mj_deleteModel(m); +} + +static const char* const kTen_i0 = + "engine/testdata/core_smooth/ten_armature_0.xml"; +static const char* const kTen_i1 = + "engine/testdata/core_smooth/ten_armature_1.xml"; +static const char* const kTen_i2 = + "engine/testdata/core_smooth/ten_armature_2.xml"; +static const char* const kTen_i3 = + "engine/testdata/core_smooth/ten_armature_3.xml"; +static const char* const kTen_i4 = + "engine/testdata/core_smooth/ten_armature_4.xml"; + +TEST_F(CoreSmoothTest, TendonArmatureConservesEnergy) { + for (const char* local_path : {kTen_i0, kTen_i1, kTen_i2, kTen_i3, kTen_i4}) { + const std::string xml_path = GetTestDataFilePath(local_path); + char error[1024]; + mjModel* m = mj_loadXML(xml_path.c_str(), nullptr, error, sizeof(error)); + ASSERT_THAT(m, NotNull()) << "Failed to load model: " << error; + mjData* d = mj_makeData(m); + + for (mjtJacobian sparsity : {mjJAC_DENSE, mjJAC_SPARSE}) { + m->opt.jacobian = sparsity; + + mj_resetDataKeyframe(m, d, 0); + mj_forward(m, d); + + double energy_0 = d->energy[0] + d->energy[1]; + + double eps = std::max(energy_0, 1.0) * 1e-5; + while (d->time < 1) { + mj_step(m, d); + double energy_t = d->energy[0] + d->energy[1]; + EXPECT_THAT(energy_t, DoubleNear(energy_0, eps)); + } + } + mj_deleteData(d); + mj_deleteModel(m); + } +} + +TEST_F(CoreSmoothTest, TendonArmatureConservesMomentum) { + const std::string xml_path = GetTestDataFilePath(kTen_i4); + char error[1024]; + mjModel* m = mj_loadXML(xml_path.c_str(), nullptr, error, sizeof(error)); + ASSERT_THAT(m, NotNull()) << "Failed to load model: " << error; + mjData* d = mj_makeData(m); + + for (mjtJacobian sparsity : {mjJAC_DENSE, mjJAC_SPARSE}) { + m->opt.jacobian = sparsity; + + mj_resetData(m, d); + mj_forward(m, d); + + // this model contains subtreelinvel and subtreeangmom sensors + vector sdata_0 = AsVector(d->sensordata, m->nsensordata); + EXPECT_THAT(sdata_0, Each(Eq(0))); + + double eps = 1e-5; + while (d->time < 1) { + mj_step(m, d); + vector sdata_t = AsVector(d->sensordata, m->nsensordata); + EXPECT_THAT(sdata_t, Pointwise(DoubleNear(eps), sdata_0)); + } + + // momentum is conserved nontrivially (velocities are non-zero) + EXPECT_GT(d->energy[1], 0); + } + + mj_deleteData(d); + mj_deleteModel(m); +} + +static const char* const kTen_i0_equiv = + "engine/testdata/core_smooth/ten_armature_0_equiv.xml"; +static const char* const kTen_i1_equiv = + "engine/testdata/core_smooth/ten_armature_1_equiv.xml"; + +TEST_F(CoreSmoothTest, TendonInertiaEquivalent) { + for (const char* lpath : {kTen_i0, kTen_i1}) { + // load tendon model + const std::string path = GetTestDataFilePath(lpath); + char error[1024]; + mjModel* m = mj_loadXML(path.c_str(), nullptr, error, sizeof(error)); + ASSERT_THAT(m, NotNull()) << "Failed to load model: " << error; + int gid = mj_name2id(m, mjOBJ_GEOM, "query"); + mjData* d = mj_makeData(m); + + if (m->nkey) mj_resetDataKeyframe(m, d, 0); + + // load equivalent model + const char* lpath_e = lpath == kTen_i0 ? kTen_i0_equiv : kTen_i1_equiv; + const std::string path_e = GetTestDataFilePath(lpath_e); + mjModel* m_e = mj_loadXML(path_e.c_str(), nullptr, error, sizeof(error)); + ASSERT_THAT(m, NotNull()) << "Failed to load model: " << error; + int gid_e = mj_name2id(m_e, mjOBJ_GEOM, "query"); + mjData* d_e = mj_makeData(m_e); + + if (m_e->nkey) mj_resetDataKeyframe(m_e, d_e, 0); + + // the equality constraint in kTen_i1_equiv reduces precision + double eps = lpath == kTen_i0 ? 1e-6 : 1e-3; + + while (d->time < 1) { + mj_step(m, d); + vector xpos = AsVector(d->geom_xpos + 3*gid, 3); + + mj_step(m_e, d_e); + vector xpos_e = AsVector(d_e->geom_xpos + 3*gid_e, 3); + + EXPECT_THAT(xpos, Pointwise(DoubleNear(eps), xpos_e)); + } + mj_deleteData(d); + mj_deleteModel(m); + mj_deleteData(d_e); + mj_deleteModel(m_e); + } +} + + // --------------------------- connect constraint ------------------------------ // test that bodies hanging on connects lead to expected force sensor readings diff --git a/test/engine/testdata/core_smooth/ten_armature_0.xml b/test/engine/testdata/core_smooth/ten_armature_0.xml new file mode 100644 index 00000000..b27e2438 --- /dev/null +++ b/test/engine/testdata/core_smooth/ten_armature_0.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/engine/testdata/core_smooth/ten_armature_0_compare.xml b/test/engine/testdata/core_smooth/ten_armature_0_compare.xml new file mode 100644 index 00000000..d2875a6e --- /dev/null +++ b/test/engine/testdata/core_smooth/ten_armature_0_compare.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + diff --git a/test/engine/testdata/core_smooth/ten_armature_0_equiv.xml b/test/engine/testdata/core_smooth/ten_armature_0_equiv.xml new file mode 100644 index 00000000..0da57dfb --- /dev/null +++ b/test/engine/testdata/core_smooth/ten_armature_0_equiv.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/test/engine/testdata/core_smooth/ten_armature_1.xml b/test/engine/testdata/core_smooth/ten_armature_1.xml new file mode 100644 index 00000000..35b48b38 --- /dev/null +++ b/test/engine/testdata/core_smooth/ten_armature_1.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/engine/testdata/core_smooth/ten_armature_1_compare.xml b/test/engine/testdata/core_smooth/ten_armature_1_compare.xml new file mode 100644 index 00000000..31f2a02f --- /dev/null +++ b/test/engine/testdata/core_smooth/ten_armature_1_compare.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/engine/testdata/core_smooth/ten_armature_1_equiv.xml b/test/engine/testdata/core_smooth/ten_armature_1_equiv.xml new file mode 100644 index 00000000..dcf62b56 --- /dev/null +++ b/test/engine/testdata/core_smooth/ten_armature_1_equiv.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/engine/testdata/core_smooth/ten_armature_2.xml b/test/engine/testdata/core_smooth/ten_armature_2.xml new file mode 100644 index 00000000..9313d875 --- /dev/null +++ b/test/engine/testdata/core_smooth/ten_armature_2.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/engine/testdata/core_smooth/ten_armature_3.xml b/test/engine/testdata/core_smooth/ten_armature_3.xml new file mode 100644 index 00000000..afaec4e9 --- /dev/null +++ b/test/engine/testdata/core_smooth/ten_armature_3.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/engine/testdata/core_smooth/ten_armature_4.xml b/test/engine/testdata/core_smooth/ten_armature_4.xml new file mode 100644 index 00000000..232c4094 --- /dev/null +++ b/test/engine/testdata/core_smooth/ten_armature_4.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/engine/testdata/core_smooth/ten_armature_offtree.xml b/test/engine/testdata/core_smooth/ten_armature_offtree.xml new file mode 100644 index 00000000..46865eac --- /dev/null +++ b/test/engine/testdata/core_smooth/ten_armature_offtree.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 26c14c500e10189a9f40ff3d74d4901a0c62e27d Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 7 Apr 2025 03:00:34 -0700 Subject: [PATCH 038/191] Fix bug in testspeed.cc for models that do not invoke the constraint solver. PiperOrigin-RevId: 744650422 Change-Id: I0d49b7a3f3069f0c066e8758627e95f973b3a728 --- sample/compile.cc | 16 +++++++--------- sample/testspeed.cc | 13 ++++++------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/sample/compile.cc b/sample/compile.cc index a5a572d8..14382393 100644 --- a/sample/compile.cc +++ b/sample/compile.cc @@ -18,7 +18,6 @@ #include #include #include -#include #include @@ -30,13 +29,12 @@ static constexpr char helpstring[] = " if infile is mjcf, compilation will be timed twice to measure the impact of caching\n\n" " Example: compile model.xml [model.mjb]\n"; -// timer +// timer (seconds) mjtNum gettm(void) { - using std::chrono::steady_clock; - using Microseconds = std::chrono::duration; - static steady_clock::time_point tm_start = steady_clock::now(); - auto elapsed = Microseconds(steady_clock::now() - tm_start); - return elapsed.count(); + using Clock = std::chrono::steady_clock; + using Seconds = std::chrono::duration; + static const Clock::time_point tm_start = Clock::now(); + return Seconds(Clock::now() - tm_start).count(); } // deallocate and print message @@ -140,12 +138,12 @@ int main(int argc, char** argv) { if (type1==typeXML) { double starttime = gettm(); m = mj_loadXML(argv[1], 0, error, 1000); - first = 1e-6 * (gettm() - starttime); + first = gettm() - starttime; if (m) { mj_deleteModel(m); starttime = gettm(); m = mj_loadXML(argv[1], 0, error, 1000); - second = 1e-6 * (gettm() - starttime); + second = gettm() - starttime; } } else { m = mj_loadModel(argv[1], 0); diff --git a/sample/testspeed.cc b/sample/testspeed.cc index 3791bf5b..84eb78bd 100644 --- a/sample/testspeed.cc +++ b/sample/testspeed.cc @@ -36,13 +36,12 @@ int constraints[maxthread]; mjtNum iterations[maxthread]; mjtNum simtime[maxthread]; -// timer +// timer (microseconds) mjtNum gettm(void) { - using std::chrono::steady_clock; - using Microseconds = std::chrono::duration; - static steady_clock::time_point tm_start = steady_clock::now(); - auto elapsed = Microseconds(steady_clock::now() - tm_start); - return elapsed.count(); + using Clock = std::chrono::steady_clock; + using Microseconds = std::chrono::duration; + static const Clock::time_point tm_start = Clock::now(); + return Microseconds(Clock::now() - tm_start).count(); } // deallocate and print message @@ -99,7 +98,7 @@ void simulate(int id, int nstep, mjtNum* ctrl) { contacts[id] += d[id]->ncon; constraints[id] += d[id]->nefc; int nisland = d[id]->solver_nisland; - if (nisland == 1) { + if (nisland == 1 || nisland == 0) { iterations[id] += d[id]->solver_niter[0]; } else { mjtNum niter = 0; From 93251f07d921d0505a98993fce124ffccb022a7e Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 7 Apr 2025 03:53:42 -0700 Subject: [PATCH 039/191] Install timers by default in Python bindings PiperOrigin-RevId: 744663306 Change-Id: Ia322ace94d1f269ff9b0275567b2e453acc94843 --- python/mujoco/bindings_test.py | 14 ++++++++++++++ python/mujoco/structs.cc | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/python/mujoco/bindings_test.py b/python/mujoco/bindings_test.py index fb2ed004..3896b8ce 100644 --- a/python/mujoco/bindings_test.py +++ b/python/mujoco/bindings_test.py @@ -1083,6 +1083,20 @@ Euler integrator, semi-implicit in velocity. ): mujoco.mj_forward(self.model, self.data) + def test_timer_installed_by_default(self): + timer_step = mujoco.mjtTimer.mjTIMER_STEP + self.assertEqual(self.data.timer[timer_step].number, 0) + self.assertEqual(self.data.timer[timer_step].duration, 0.0) + + mujoco.mj_step(self.model, self.data) + self.assertEqual(self.data.timer[timer_step].number, 1) + duration_1 = self.data.timer[timer_step].duration + self.assertGreater(duration_1, 0.0) + + mujoco.mj_step(self.model, self.data, 5) + self.assertEqual(self.data.timer[timer_step].number, 6) + self.assertGreater(self.data.timer[timer_step].duration, duration_1) + def test_mjcb_time(self): class CallCounter: diff --git a/python/mujoco/structs.cc b/python/mujoco/structs.cc index 6925b980..77200386 100644 --- a/python/mujoco/structs.cc +++ b/python/mujoco/structs.cc @@ -18,6 +18,7 @@ #include #include +#include // NOLINT(build/c++11) #include #include #include @@ -553,6 +554,16 @@ MjDataWrapper* MjDataWrapper::FromRawPointer(raw::MjData* m) noexcept { } } +namespace { +// default timer callback (seconds) +mjtNum GetTime() { + using Clock = std::chrono::steady_clock; + using Seconds = std::chrono::duration; + static const Clock::time_point tm_start = Clock::now(); + return Seconds(Clock::now() - tm_start).count(); +} +} // namespace + MjDataWrapper::MjWrapper(MjModelWrapper* model) : WrapperBase(InterceptMjErrors(mj_makeData)(model->get()), &MjDataCapsuleDestructor), @@ -582,6 +593,14 @@ MjDataWrapper::MjWrapper(MjModelWrapper* model) throw UnexpectedError( "MjDataRawPointerMap already contains this raw mjData*"); } + + // install default timer if not already installed + { + py::gil_scoped_acquire gil; + if (!mjcb_time) { + mjcb_time = GetTime; + } + } } MjDataWrapper::MjWrapper(const MjDataWrapper& other) From cc2f57d8208b8ab8e97fdaaff7a465ec135208dc Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Mon, 7 Apr 2025 04:20:17 -0700 Subject: [PATCH 040/191] Change mjSpec and mjModel signature mechanism. The signature now contains the necessary information to safely perform `bind`. The private UIDs are now removed. Fixes an issue of changing signature when compiling a copy of an mjSpec. PiperOrigin-RevId: 744670626 Change-Id: Id3c66419cf2afbe78e91bc4b37d2299f5ec00ab1 --- mjx/mujoco/mjx/_src/support_test.py | 2 +- src/user/user_flexcomp.cc | 2 - src/user/user_model.cc | 96 +++++++++++++++++++++++------ src/user/user_model.h | 7 +-- src/user/user_objects.cc | 13 +--- src/user/user_objects.h | 1 - test/user/user_api_test.cc | 4 ++ 7 files changed, 85 insertions(+), 40 deletions(-) diff --git a/mjx/mujoco/mjx/_src/support_test.py b/mjx/mujoco/mjx/_src/support_test.py index 1fd1971b..373b6a90 100644 --- a/mjx/mujoco/mjx/_src/support_test.py +++ b/mjx/mujoco/mjx/_src/support_test.py @@ -349,7 +349,7 @@ class SupportTest(parameterized.TestCase): self.assertEqual( str(e.exception), 'mjSpec signature does not match mjx.Model signature:' - ' 5495345807332648606 != 270010677651259353', + ' 4300287342280373816 != 9887180086914550999', ) _CONTACTS = """ diff --git a/src/user/user_flexcomp.cc b/src/user/user_flexcomp.cc index 80ee69d4..50c12d68 100644 --- a/src/user/user_flexcomp.cc +++ b/src/user/user_flexcomp.cc @@ -403,14 +403,12 @@ bool mjCFlexcomp::Make(mjsBody* body, char* error, int error_sz) { mjCFlex* flex = model->AddFlex(); mjsFlex* pf = &flex->spec; int id = flex->id; - int uid = flex->uid; *flex = def.Flex(); flex->PointToLocal(); flex->model = model; flex->id = id; - flex->uid = uid; mjs_setString(pf->name, name.c_str()); mjs_setInt(pf->elem, element.data(), element.size()); mjs_setFloat(pf->texcoord, texcoord.data(), texcoord.size()); diff --git a/src/user/user_model.cc b/src/user/user_model.cc index 2b5dc102..c6b181d0 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -201,7 +201,6 @@ mjCModel::mjCModel() { world->mass = 0; mjuu_zerovec(world->inertia, 3); world->id = 0; - world->uid = GetUid(); world->parent = nullptr; world->weldid = 0; world->name = "world"; @@ -297,7 +296,6 @@ void mjCModel::CopyList(std::vector& dest, // copy the element from the other model to this model if (deepcopy_) { source[i]->ForgetKeyframes(); - candidate->uid = source[i]->uid; } else { candidate->AddRef(); } @@ -1038,7 +1036,6 @@ template T* mjCModel::AddObject(vector& list, string type) { T* obj = new T(this); obj->id = (int)list.size(); - obj->uid = GetUid(); list.push_back(obj); spec.element->signature = Signature(); return obj; @@ -1051,7 +1048,6 @@ T* mjCModel::AddObjectDefault(vector& list, string type, mjCDef* def) { T* obj = new T(this, def ? def : defaults_[0]); obj->id = (int)list.size(); obj->classname = def ? def->name : "main"; - obj->uid = GetUid(); list.push_back(obj); spec.element->signature = Signature(); return obj; @@ -3615,7 +3611,7 @@ void mjCModel::SaveState(const std::string& state_name, const T* qpos, const T* } for (auto body : bodies_) { - if (!body->spec.mocap) { + if (!body->spec.mocap || body->mocapid == -1) { continue; } if (mpos) { @@ -4388,6 +4384,9 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) { bodies_[i]->subtreedofs = 0; } + // initialize spec signature (needed if the user changed sensor or joint types) + spec.element->signature = Signature(); + // fill missing names and check that they are all filled for (const auto& asset : meshes_) asset->CopyFromSpec(); for (const auto& asset : skins_) asset->CopyFromSpec(); @@ -4651,7 +4650,7 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) { // special cases that are not caused by user edits if (compiler.fusestatic || compiler.discardvisual || - !spec.element->signature || !pairs_.empty() || !excludes_.empty()) { + !pairs_.empty() || !excludes_.empty()) { spec.element->signature = m->signature; } @@ -4663,21 +4662,78 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) { -uint64_t mjCModel::Signature() { - std::string uid_str; - for (int i = 0; i < mjNOBJECT; ++i) { - if (i == mjOBJ_XBODY || i == mjOBJ_UNKNOWN || i == mjOBJ_DOF) { - continue; - } - if (object_lists_[i] == nullptr) { - throw mjCError(0, "object list %s is null", std::to_string(i).c_str()); - } - uid_str += '|'; - for (mjCBase* object : *object_lists_[i]) { - uid_str += std::to_string(object->uid) + " "; - } +std::string mjCModel::PrintTree(const mjCBody* body, std::string indent) { + std::string tree; + tree += indent + "\n"; + indent += " "; + for (const auto& joint : body->joints) { + tree += indent + "" + std::to_string(joint->nq()) + "\n"; } - return mj_hashString(uid_str.c_str(), UINT64_MAX); + for (uint64_t i = 0; i < body->geoms.size(); ++i) { + tree += indent + "\n"; + } + for (uint64_t i = 0; i < body->sites.size(); ++i) { + tree += indent + "\n"; + } + for (uint64_t i = 0; i < body->cameras.size(); ++i) { + tree += indent + "\n"; + } + for (uint64_t i = 0; i < body->lights.size(); ++i) { + tree += indent + "\n"; + } + for (uint64_t i = 0; i < body->bodies.size(); ++i) { + tree += PrintTree(body->bodies[i], indent); + } + indent.pop_back(); + indent.pop_back(); + tree += indent + "\n"; + return tree; +} + + + +uint64_t mjCModel::Signature() { + std::string tree = "\n" + PrintTree(bodies_[0]); + for (unsigned int i = 0; i < flexes_.size(); ++i) { + tree += "\n"; + } + for (unsigned int i = 0; i < meshes_.size(); ++i) { + tree += "\n"; + } + for (unsigned int i = 0; i < skins_.size(); ++i) { + tree += "\n"; + } + for (unsigned int i = 0; i < hfields_.size(); ++i) { + tree += "\n"; + } + for (unsigned int i = 0; i < textures_.size(); ++i) { + tree += "\n"; + } + for (unsigned int i = 0; i < materials_.size(); ++i) { + tree += "\n"; + } + for (unsigned int i = 0; i < pairs_.size(); ++i) { + tree += "\n"; + } + for (unsigned int i = 0; i < excludes_.size(); ++i) { + tree += "\n"; + } + for (unsigned int i = 1; i < equalities_.size(); ++i) { + tree += "\n"; + } + for (unsigned int i = 0; i < tendons_.size(); ++i) { + tree += "\n"; + } + for (unsigned int i = 0; i < actuators_.size(); ++i) { + tree += "\n"; + } + for (unsigned int i = 0; i < sensors_.size(); ++i) { + tree += "" + std::to_string(sensors_[i]->spec.type) + "\n"; + } + for (unsigned int i = 0; i < keys_.size(); ++i) { + tree += "\n"; + } + return mj_hashString(tree.c_str(), UINT64_MAX); } diff --git a/src/user/user_model.h b/src/user/user_model.h index e8e3cc8c..cfd8082e 100644 --- a/src/user/user_model.h +++ b/src/user/user_model.h @@ -324,9 +324,6 @@ class mjCModel : public mjCModel_, private mjSpec { // set attached flag void SetAttached(bool deepcopy) { attached_ |= !deepcopy; } - // get new uid - int GetUid() { return uid_count_++; } - private: // settings for each defaults class std::vector defaults_; @@ -441,6 +438,9 @@ class mjCModel : public mjCModel_, private mjSpec { void MarkPluginInstance(std::unordered_map& instances, const std::vector& list); + // print the tree of a body + std::string PrintTree(const mjCBody* body, std::string indent = ""); + // generate a signature for the model uint64_t Signature(); @@ -449,7 +449,6 @@ class mjCModel : public mjCModel_, private mjSpec { std::vector key_pending_; // attached keyframes bool deepcopy_; // copy objects when attaching bool attached_ = false; // true if model is attached to a parent model - int uid_count_ = 0; // unique id count for all objects std::unordered_map compiler2spec_; // map from compiler to spec }; #endif // MUJOCO_SRC_USER_USER_MODEL_H_ diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index f52f8cf6..b3208662 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -818,6 +818,7 @@ mjCBody::mjCBody(mjCModel* _model) { mjuu_zerovec(xpos0, 3); mjuu_setvec(xquat0, 1, 0, 0, 0); last_attached = nullptr; + mocapid = -1; // clear object lists bodies.clear(); @@ -840,7 +841,6 @@ mjCBody::mjCBody(mjCModel* _model) { mjCBody::mjCBody(const mjCBody& other, mjCModel* _model) { model = _model; - uid = other.uid; mjSpec* origin = model->FindSpec(other.compiler); compiler = origin ? &origin->compiler : &model->spec.compiler; *this = other; @@ -944,7 +944,6 @@ mjCBody& mjCBody::operator+=(const mjCFrame& other) { frames.back()->frame = other.frame; if (model->deepcopy_) { frames.back()->NameSpace(other_model); - frames.back()->uid = other.uid; } else { frames.back()->AddRef(); } @@ -1037,8 +1036,6 @@ void mjCBody::CopyList(std::vector& dst, const std::vector& src, // increment refcount if shallow copy is made if (!model->deepcopy_) { dst.back()->AddRef(); - } else { - dst.back()->uid = src[i]->uid; } // set namespace @@ -1256,7 +1253,6 @@ mjCBody* mjCBody::AddBody(mjCDef* _def) { obj->parent = this; // update signature - obj->uid = model->GetUid(); model->spec.element->signature = model->Signature(); return obj; } @@ -1271,7 +1267,6 @@ mjCFrame* mjCBody::AddFrame(mjCFrame* _frame) { model->MakeTreeLists(); // update signature - obj->uid = model->GetUid(); model->spec.element->signature = model->Signature(); return obj; } @@ -1295,7 +1290,6 @@ mjCJoint* mjCBody::AddFreeJoint() { // update signature - obj->uid = model->GetUid(); model->spec.element->signature = model->Signature(); return obj; } @@ -1318,7 +1312,6 @@ mjCJoint* mjCBody::AddJoint(mjCDef* _def) { // update signature - obj->uid = model->GetUid(); model->spec.element->signature = model->Signature(); return obj; } @@ -1341,7 +1334,6 @@ mjCGeom* mjCBody::AddGeom(mjCDef* _def) { // update signature - obj->uid = model->GetUid(); model->spec.element->signature = model->Signature(); return obj; } @@ -1364,7 +1356,6 @@ mjCSite* mjCBody::AddSite(mjCDef* _def) { // update signature - obj->uid = model->GetUid(); model->spec.element->signature = model->Signature(); return obj; } @@ -1387,7 +1378,6 @@ mjCCamera* mjCBody::AddCamera(mjCDef* _def) { // update signature - obj->uid = model->GetUid(); model->spec.element->signature = model->Signature(); return obj; } @@ -1410,7 +1400,6 @@ mjCLight* mjCBody::AddLight(mjCDef* _def) { // update signature - obj->uid = model->GetUid(); model->spec.element->signature = model->Signature(); return obj; } diff --git a/src/user/user_objects.h b/src/user/user_objects.h index f04bf850..ce890610 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -219,7 +219,6 @@ class mjCBoundingVolumeHierarchy : public mjCBoundingVolumeHierarchy_ { class mjCBase_ : public mjsElement { public: int id; // object id - int uid; // unique identifier std::string name; // object name std::string classname; // defaults class name std::string info; // error message info set by the user diff --git a/test/user/user_api_test.cc b/test/user/user_api_test.cc index ddde4679..993be5cd 100644 --- a/test/user/user_api_test.cc +++ b/test/user/user_api_test.cc @@ -550,6 +550,10 @@ TEST_F(PluginTest, RecompileCompare) { mjModel* m_new = mj_compile(s, nullptr); mjModel* m_copy = mj_compile(s_copy, nullptr); + // compare signature + EXPECT_EQ(m_old->signature, m_new->signature) << xml; + EXPECT_EQ(m_old->signature, m_copy->signature) << xml; + ASSERT_THAT(m_new, NotNull()) << "Failed to recompile " << xml << ": " << mjs_getError(s); ASSERT_THAT(m_copy, NotNull()) From 58234b2217eee793143f395ab2dbd83b84d5bf60 Mon Sep 17 00:00:00 2001 From: Kyle Bayes Date: Mon, 7 Apr 2025 05:58:00 -0700 Subject: [PATCH 041/191] Replace mjMAXVAL with numerical max limit where necessary in nativeccd. PiperOrigin-RevId: 744693156 Change-Id: I72c3122701be281d468291dfc2ad1e4bb1da5a1c --- src/engine/engine_collision_gjk.c | 27 ++++++++++++------------ src/engine/engine_collision_gjk.h | 8 +++++++ test/engine/engine_collision_gjk_test.cc | 4 ++-- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/engine/engine_collision_gjk.c b/src/engine/engine_collision_gjk.c index 92d49a6a..807942d2 100644 --- a/src/engine/engine_collision_gjk.c +++ b/src/engine/engine_collision_gjk.c @@ -14,7 +14,6 @@ #include "engine/engine_collision_gjk.h" -#include #include #include #include @@ -206,16 +205,16 @@ static void gjk(mjCCDStatus* status, mjCCDObj* obj1, mjCCDObj* obj2) { status->gjk_iterations = k; status->nsimplex = 0; status->nx = 0; - status->dist = mjMAXVAL; + status->dist = mjMAX_LIMIT; return; } - } else if (status->dist_cutoff < mjMAXVAL) { + } else if (status->dist_cutoff < mjMAX_LIMIT) { mjtNum vs = dot3(x_k, s_k), vv = dot3(x_k, x_k); if (dot3(x_k, s_k) > 0 && (vs*vs / vv) >= cutoff2) { status->gjk_iterations = k; status->nsimplex = 0; status->nx = 0; - status->dist = mjMAXVAL; + status->dist = mjMAX_LIMIT; return; } } @@ -227,7 +226,7 @@ static void gjk(mjCCDStatus* status, mjCCDObj* obj1, mjCCDObj* obj2) { int ret = gjkIntersect(status, obj1, obj2); if (ret != -1) { status->nx = 0; - status->dist = ret > 0 ? 0 : mjMAXVAL; + status->dist = ret > 0 ? 0 : mjMAX_LIMIT; return; } k = status->gjk_iterations; @@ -251,7 +250,7 @@ static void gjk(mjCCDStatus* status, mjCCDObj* obj1, mjCCDObj* obj2) { status->gjk_iterations = k; status->nsimplex = 0; status->nx = 0; - status->dist = mjMAXVAL; + status->dist = mjMAX_LIMIT; return; } @@ -399,7 +398,7 @@ static inline mjtNum signedDistance(mjtNum normal[3], const Vertex* v1, const Ve scl3(normal, normal, norm); return dot3(normal, v1->vert); } - return mjMAXVAL; // cannot recover normal (ignore face) + return mjMAX_LIMIT; // cannot recover normal (ignore face) } @@ -607,7 +606,7 @@ static void S3D(mjtNum lambda[4], const mjtNum s1[3], const mjtNum s2[3], const } // find the smallest distance, and use the corresponding barycentric coordinates - mjtNum dmin = mjMAXVAL; + mjtNum dmin = mjMAX_LIMIT; if (!comp1) { mjtNum lambda_2d[3], x[3]; @@ -758,7 +757,7 @@ static void S2D(mjtNum lambda[3], const mjtNum s1[3], const mjtNum s2[3], const } // find the smallest distance, and use the corresponding barycentric coordinates - mjtNum dmin = mjMAXVAL; + mjtNum dmin = mjMAX_LIMIT; if (!comp1) { mjtNum lambda_1d[2], x[3]; @@ -931,7 +930,7 @@ static int polytope2(Polytope* pt, mjCCDStatus* status, mjCCDObj* obj1, mjCCDObj sub3(diff, v2, v1); // find component with smallest magnitude (so cross product is largest) - mjtNum value = mjMAXVAL; + mjtNum value = mjMAX_LIMIT; int index = 0; for (int i = 0; i < 3; i++) { if (mju_abs(diff[i]) < value) { @@ -1351,7 +1350,7 @@ static void epaWitness(const Polytope* pt, const Face* face, mjtNum x1[3], mjtNu // return a face of the expanded polytope that best approximates the pentration depth // witness points are in status->{x1, x2} static Face* epa(mjCCDStatus* status, Polytope* pt, mjCCDObj* obj1, mjCCDObj* obj2) { - mjtNum tolerance = status->tolerance, lower, upper = FLT_MAX; + mjtNum tolerance = status->tolerance, lower, upper = mjMAX_LIMIT; int k, kmax = status->max_iterations; Face* face = NULL, *pface = NULL; // face closest to origin @@ -1359,7 +1358,7 @@ static Face* epa(mjCCDStatus* status, Polytope* pt, mjCCDObj* obj1, mjCCDObj* ob pface = face; // find the face closest to the origin (lower bound for penetration depth) - lower = FLT_MAX; + lower = mjMAX_LIMIT; for (int i = 0; i < pt->nmap; i++) { if (pt->map[i]->dist < lower) { face = pt->map[i]; @@ -1577,7 +1576,7 @@ static mjtNum planeIntersect(mjtNum res[3], const mjtNum pn[3], mjtNum pd, mjtNum ab[3]; sub3(ab, b, a); mjtNum temp = dot3(pn, ab); - if (temp == 0.0) return mjMAXVAL; // parallel; no intersection + if (temp == 0.0) return mjMAX_LIMIT; // parallel; no intersection mjtNum t = (pd - dot3(pn, a)) / temp; if (t >= 0.0 && t <= 1.0) { res[0] = a[0] + t*ab[0]; @@ -2292,7 +2291,7 @@ mjtNum mjc_ccd(const mjCCDConfig* config, mjCCDStatus* status, mjCCDObj* obj1, m if (status->dist > status->tolerance) { inflate(status, full_margin1, full_margin2); if (status->dist > status->dist_cutoff) { - status->dist = mjMAXVAL; + status->dist = mjMAX_LIMIT; } return status->dist; } diff --git a/src/engine/engine_collision_gjk.h b/src/engine/engine_collision_gjk.h index a8f2224b..55ee5f8b 100644 --- a/src/engine/engine_collision_gjk.h +++ b/src/engine/engine_collision_gjk.h @@ -15,6 +15,7 @@ #ifndef MUJOCO_SRC_ENGINE_ENGINE_COLLISION_GJK_H_ #define MUJOCO_SRC_ENGINE_ENGINE_COLLISION_GJK_H_ +#include #include #include @@ -27,6 +28,13 @@ extern "C" { #endif +// numerical max limit +#ifndef mjUSESINGLE + #define mjMAX_LIMIT DBL_MAX +#else + #define mjMAX_LIMIT FLT_MAX +#endif + // max number of EPA iterations #define mjMAX_EPA_ITERATIONS 170 diff --git a/test/engine/engine_collision_gjk_test.cc b/test/engine/engine_collision_gjk_test.cc index 325f6fd7..f4fcb141 100644 --- a/test/engine/engine_collision_gjk_test.cc +++ b/test/engine/engine_collision_gjk_test.cc @@ -71,7 +71,7 @@ void CCDFree(void* data, void* buffer) { } mjtNum GeomDist(mjModel* m, mjData* d, int g1, int g2, mjtNum x1[3], - mjtNum x2[3], mjtNum cutoff = mjMAXVAL) { + mjtNum x2[3], mjtNum cutoff = mjMAX_LIMIT) { mjCCDConfig config; mjCCDStatus status; @@ -211,7 +211,7 @@ TEST_F(MjGjkTest, SphereSphereDistCutoff) { int geom2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); mjtNum dist = GeomDist(model, data, geom1, geom2, nullptr, nullptr, .999999); - EXPECT_EQ(dist, mjMAXVAL); + EXPECT_EQ(dist, mjMAX_LIMIT); mj_deleteData(data); mj_deleteModel(model); } From d7027fb1c053e5b14be32e4d3da561f8b7e4e87c Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Mon, 7 Apr 2025 06:51:21 -0700 Subject: [PATCH 042/191] Do not skip mjSpec's with repeated model names when attaching a new mjSpec. PiperOrigin-RevId: 744705838 Change-Id: Ie47a3c029f443910629619b7f9ff43ab07646725 --- src/user/user_api.cc | 2 +- src/user/user_objects.cc | 4 ++-- test/user/user_api_test.cc | 45 +++++++++++++++++++++++++------------- 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/user/user_api.cc b/src/user/user_api.cc index 6962493f..e21181dd 100644 --- a/src/user/user_api.cc +++ b/src/user/user_api.cc @@ -73,7 +73,7 @@ mjSpec* mj_copySpec(const mjSpec* s) { try { modelC = new mjCModel(*static_cast(s->element)); } catch (mjCError& e) { - modelC->SetError(e); + static_cast(s->element)->SetError(e); return nullptr; } return &modelC->spec; diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index b3208662..827b5e48 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -916,7 +916,7 @@ mjCBody& mjCBody::operator+=(const mjCBody& other) { // attach frame to body mjCBody& mjCBody::operator+=(const mjCFrame& other) { // append a copy of the attached spec - if (other.model != model && !model->FindSpec(mjs_getString(other.model->spec.modelname))) { + if (other.model != model && !model->FindSpec(&other.model->spec.compiler)) { model->AppendSpec(mj_copySpec(&other.model->spec), &other.model->spec.compiler); } @@ -2029,7 +2029,7 @@ mjCFrame& mjCFrame::operator=(const mjCFrame& other) { // attach body to frame mjCFrame& mjCFrame::operator+=(const mjCBody& other) { // append a copy of the attached spec - if (other.model != model && !model->FindSpec(mjs_getString(other.model->spec.modelname))) { + if (other.model != model && !model->FindSpec(&other.model->spec.compiler)) { model->AppendSpec(mj_copySpec(&other.model->spec), &other.model->spec.compiler); } diff --git a/test/user/user_api_test.cc b/test/user/user_api_test.cc index 993be5cd..4db9f25b 100644 --- a/test/user/user_api_test.cc +++ b/test/user/user_api_test.cc @@ -2552,31 +2552,46 @@ TEST_F(MujocoTest, DifferentOptionsInAttachedFrame) { // load specs and compile child mjSpec* parent = mj_parseXMLString(xml_parent, 0, nullptr, 0); EXPECT_THAT(parent, NotNull()); - mjSpec* child = mj_parseXMLString(xml_child, 0, nullptr, 0); - EXPECT_THAT(child, NotNull()); - mjModel* m_child = mj_compile(child, 0); - EXPECT_THAT(m_child, NotNull()); + mjSpec* child1 = mj_parseXMLString(xml_child, 0, nullptr, 0); + EXPECT_THAT(child1, NotNull()); + mjModel* m_child1 = mj_compile(child1, 0); + EXPECT_THAT(m_child1, NotNull()); + mjSpec* child2 = mj_parseXMLString(xml_child, 0, nullptr, 0); + EXPECT_THAT(child2, NotNull()); + mjModel* m_child2 = mj_compile(child1, 0); + EXPECT_THAT(m_child2, NotNull()); // attach child frame to parent worldbody mjsBody* world = mjs_findBody(parent, "world"); EXPECT_THAT(world, NotNull()); - mjsFrame* child_frame = mjs_findFrame(child, "child"); - EXPECT_THAT(child_frame, NotNull()); - mjsElement* attached_frame = - mjs_attach(world->element, child_frame->element, "child-", ""); - EXPECT_THAT(attached_frame, NotNull()); + mjsFrame* child1_frame = mjs_findFrame(child1, "child"); + EXPECT_THAT(child1_frame, NotNull()); + mjsFrame* child2_frame = mjs_findFrame(child2, "child"); + EXPECT_THAT(child2_frame, NotNull()); + mjsElement* attached_frame1 = + mjs_attach(world->element, child1_frame->element, "child-", "-1"); + EXPECT_THAT(attached_frame1, NotNull()); + mjsElement* attached_frame2 = + mjs_attach(world->element, child2_frame->element, "child-", "-2"); + EXPECT_THAT(attached_frame2, NotNull()); // wrap the child frame in the parent frame and compile mjModel* m_attached = mj_compile(parent, 0); EXPECT_THAT(m_attached, NotNull()); - EXPECT_NEAR(m_attached->site_quat[0], m_child->site_quat[0], 1e-6); - EXPECT_NEAR(m_attached->site_quat[1], m_child->site_quat[1], 1e-6); - EXPECT_NEAR(m_attached->site_quat[2], m_child->site_quat[2], 1e-6); - EXPECT_NEAR(m_attached->site_quat[3], m_child->site_quat[3], 1e-6); + EXPECT_NEAR(m_attached->site_quat[0], m_child1->site_quat[0], 1e-6); + EXPECT_NEAR(m_attached->site_quat[1], m_child1->site_quat[1], 1e-6); + EXPECT_NEAR(m_attached->site_quat[2], m_child1->site_quat[2], 1e-6); + EXPECT_NEAR(m_attached->site_quat[3], m_child1->site_quat[3], 1e-6); + EXPECT_NEAR(m_attached->site_quat[4], m_child2->site_quat[0], 1e-6); + EXPECT_NEAR(m_attached->site_quat[5], m_child2->site_quat[1], 1e-6); + EXPECT_NEAR(m_attached->site_quat[6], m_child2->site_quat[2], 1e-6); + EXPECT_NEAR(m_attached->site_quat[7], m_child2->site_quat[3], 1e-6); mj_deleteSpec(parent); - mj_deleteSpec(child); - mj_deleteModel(m_child); + mj_deleteSpec(child1); + mj_deleteModel(m_child1); + mj_deleteSpec(child2); + mj_deleteModel(m_child2); mj_deleteModel(m_attached); } From 606f00f8024a96874ac9221f07e1114ed81e1f3c Mon Sep 17 00:00:00 2001 From: Kyle Bayes Date: Mon, 7 Apr 2025 08:32:58 -0700 Subject: [PATCH 043/191] Remove contact pruning with box-box collisions in nativeccd and add benchmarks. PiperOrigin-RevId: 744732382 Change-Id: I2e20b646c541ee99888f1e964302d1b4363b2dc4 --- src/engine/engine_collision_convex.c | 30 ++++++++++++++++------------ src/engine/engine_collision_convex.h | 12 +++++------ src/engine/engine_collision_gjk.c | 22 ++++++++++---------- test/benchmark/CMakeLists.txt | 2 +- test/benchmark/ccd_benchmark_test.cc | 13 +++++++++++- 5 files changed, 48 insertions(+), 31 deletions(-) diff --git a/src/engine/engine_collision_convex.c b/src/engine/engine_collision_convex.c index b724d280..16b3b05c 100644 --- a/src/engine/engine_collision_convex.c +++ b/src/engine/engine_collision_convex.c @@ -908,24 +908,31 @@ static void mju_rotateFrame(const mjtNum origin[3], const mjtNum rot[9], -// return true if multiccd can run in a single pass -static int singlePass(const mjCCDObj* obj1, const mjCCDObj* obj2) { +// return number of contacts supported by a single pass of narrowphase +static int maxContacts(const mjCCDObj* obj1, const mjCCDObj* obj2) { const mjModel* m = obj1->model; // single pass not supported for margins if (obj1->margin > 0 || obj2->margin > 0) { - return 0; + return 1; } - // supported geoms for single pass + // can return 8 contacts for box-box collision in one pass int type1 = m->geom_type[obj1->geom]; int type2 = m->geom_type[obj2->geom]; + if (type1 == mjGEOM_BOX && type2 == mjGEOM_BOX) { + return 8; + } + + // reduce mesh collisions to 4 contacts max if (type1 == mjGEOM_BOX || type1 == mjGEOM_MESH) { if (type2 == mjGEOM_BOX || type2 == mjGEOM_MESH) { - return 1; + return mjENABLED(mjENBL_MULTICCD) ? 4 : 1; } } - return 0; + + // not supported for other geom types + return 1; } @@ -937,17 +944,14 @@ int mjc_Convex(const mjModel* m, const mjData* d, mjCCDObj obj1, obj2; mjc_initCCDObj(&obj1, m, d, g1, margin); mjc_initCCDObj(&obj2, m, d, g2, margin); - int max_contacts = 1; - - if (mjENABLED(mjENBL_MULTICCD) && singlePass(&obj1, &obj2)) { - max_contacts = 4; - } + int max_contacts = maxContacts(&obj1, &obj2); // find initial contact int ncon = mjc_CCDIteration(m, d, &obj1, &obj2, con, max_contacts, margin); - // nativeccd supports multi Box-Box collision directly - if (!mjDISABLED(mjDSBL_NATIVECCD) && singlePass(&obj1, &obj2)) { + + // no additional contacts needed + if (!mjDISABLED(mjDSBL_NATIVECCD) && max_contacts > 1) { return ncon; } diff --git a/src/engine/engine_collision_convex.h b/src/engine/engine_collision_convex.h index c3a4b220..76df2a28 100644 --- a/src/engine/engine_collision_convex.h +++ b/src/engine/engine_collision_convex.h @@ -81,12 +81,12 @@ void mjc_pointSupport(mjtNum res[3], mjCCDObj* obj, const mjtNum dir[3]); void mjc_lineSupport(mjtNum res[3], mjCCDObj* obj, const mjtNum dir[3]); // pairwise geom collision functions using ccd -int mjc_PlaneConvex (const mjModel* m, const mjData* d, - mjContact* con, int g1, int g2, mjtNum margin); -int mjc_ConvexHField (const mjModel* m, const mjData* d, - mjContact* con, int g1, int g2, mjtNum margin); -int mjc_Convex (const mjModel* m, const mjData* d, - mjContact* con, int g1, int g2, mjtNum margin); +int mjc_PlaneConvex(const mjModel* m, const mjData* d, + mjContact* con, int g1, int g2, mjtNum margin); +int mjc_ConvexHField(const mjModel* m, const mjData* d, + mjContact* con, int g1, int g2, mjtNum margin); +MJAPI int mjc_Convex(const mjModel* m, const mjData* d, + mjContact* con, int g1, int g2, mjtNum margin); // geom-elem or elem-elem or vert-elem collision function using ccd int mjc_ConvexElem (const mjModel* m, const mjData* d, mjContact* con, diff --git a/src/engine/engine_collision_gjk.c b/src/engine/engine_collision_gjk.c index 807942d2..e1ac8739 100644 --- a/src/engine/engine_collision_gjk.c +++ b/src/engine/engine_collision_gjk.c @@ -1863,36 +1863,38 @@ static int boxNormals(mjtNum res[9], int resind[3], int dim, mjCCDObj* obj, const mjtNum* mat = obj->data->geom_xmat + 3*g; if (dim == 3) { + int c = 0; int x = ((v1 & 1) && (v2 & 1) && (v3 & 1)) - (!(v1 & 1) && !(v2 & 1) && !(v3 & 1)); int y = ((v1 & 2) && (v2 & 2) && (v3 & 2)) - (!(v1 & 2) && !(v2 & 2) && !(v3 & 2)); int z = ((v1 & 4) && (v2 & 4) && (v3 & 4)) - (!(v1 & 4) && !(v2 & 4) && !(v3 & 4)); globalcoord(res, mat, NULL, x, y, z); int sgn = x + y + z; - if (x) resind[0] = 0; - if (y) resind[0] = 2; - if (z) resind[0] = 4; + if (x) resind[c++] = 0; + if (y) resind[c++] = 2; + if (z) resind[c++] = 4; if (sgn == -1) resind[0]++; - return 1; + return c == 1 ? 1 : 0; // return 1 only if vertices make a valid face } if (dim == 2) { + int c = 0; int x = ((v1 & 1) && (v2 & 1)) - (!(v1 & 1) && !(v2 & 1)); int y = ((v1 & 2) && (v2 & 2)) - (!(v1 & 2) && !(v2 & 2)); int z = ((v1 & 4) && (v2 & 4)) - (!(v1 & 4) && !(v2 & 4)); if (x) { globalcoord(res, mat, NULL, x, 0, 0); - resind[0] = (x > 0) ? 0 : 1; + resind[c++] = (x > 0) ? 0 : 1; } if (y) { - int i = (x ? 1 : 0); - globalcoord(res + 3*i, mat, NULL, 0, y, 0); - resind[i] = (y > 0) ? 2 : 3; + globalcoord(res + 3*c, mat, NULL, 0, y, 0); + resind[c++] = (y > 0) ? 2 : 3; } if (z) { globalcoord(res + 3, mat, NULL, 0, 0, z); - resind[1] = (z > 0) ? 4 : 5; + resind[c++] = (z > 0) ? 4 : 5; } - return 2; + // TODO(kylebayes): Should be able to recover multiple contacts here. + return c == 2 ? 2 : 0; } if (dim == 1) { diff --git a/test/benchmark/CMakeLists.txt b/test/benchmark/CMakeLists.txt index 0a23fafe..48a5a369 100644 --- a/test/benchmark/CMakeLists.txt +++ b/test/benchmark/CMakeLists.txt @@ -16,7 +16,7 @@ mujoco_test( ccd_benchmark_test MAIN_TARGET benchmark::benchmark_main - ADDITIONAL_LINK_LIBRARIES benchmark::benchmark absl::core_headers + ADDITIONAL_LINK_LIBRARIES benchmark::benchmark absl::core_headers ccd ) mujoco_test( diff --git a/test/benchmark/ccd_benchmark_test.cc b/test/benchmark/ccd_benchmark_test.cc index 1ea8ce37..a7768dce 100644 --- a/test/benchmark/ccd_benchmark_test.cc +++ b/test/benchmark/ccd_benchmark_test.cc @@ -24,6 +24,9 @@ #include #include "test/fixture.h" +#include "src/engine/engine_collision_convex.h" +#include "src/engine/engine_collision_primitive.h" + namespace mujoco { namespace { @@ -116,11 +119,19 @@ void ABSL_ATTRIBUTE_NO_TAIL_CALL BENCHMARK(BM_BoxMesh_LibCCD); void ABSL_ATTRIBUTE_NO_TAIL_CALL BM_BoxBox(benchmark::State& state) { - static TestHarness harness(kBoxBoxPath, "box.xml (BoxBox)"); + static TestHarness harness(kBoxBoxPath, "box.xml (BoxBox)", mjDSBL_NATIVECCD); harness.RunBenchmark(state); } BENCHMARK(BM_BoxBox); +void ABSL_ATTRIBUTE_NO_TAIL_CALL BM_BoxBox_NativeCCD(benchmark::State& state) { + mjCOLLISIONFUNC[mjGEOM_BOX][mjGEOM_BOX] = mjc_Convex; + static TestHarness harness(kBoxBoxPath, "box.xml (NativeCCD)"); + harness.RunBenchmark(state); + mjCOLLISIONFUNC[mjGEOM_BOX][mjGEOM_BOX] = mjc_BoxBox; +} +BENCHMARK(BM_BoxBox_NativeCCD); + void ABSL_ATTRIBUTE_NO_TAIL_CALL BM_Ellipsoid_NativeCCD(benchmark::State& state) { static TestHarness harness(kEllipsoidPath, "ellipsoid.xml (nativeccd)"); From 4e0a4f4d39eb16adaec47c5642942780521779ea Mon Sep 17 00:00:00 2001 From: Baruch Tabanpour Date: Mon, 7 Apr 2025 15:42:51 -0700 Subject: [PATCH 044/191] Defer device puts to the end of make_data. Fixes #2461. PiperOrigin-RevId: 744875869 Change-Id: I8d88d4f73407b44f761d01309e2cc28b43aa305f --- mjx/mujoco/mjx/_src/io.py | 290 +++++++++++++++++++------------------- 1 file changed, 146 insertions(+), 144 deletions(-) diff --git a/mjx/mujoco/mjx/_src/io.py b/mjx/mujoco/mjx/_src/io.py index 1e8c2b5e..aac18731 100644 --- a/mjx/mujoco/mjx/_src/io.py +++ b/mjx/mujoco/mjx/_src/io.py @@ -237,153 +237,154 @@ def make_data( ne, nf, nl, nc = constraint.counts(efc_type) ncon, nefc = dim.size, ne + nf + nl + nc - with jax.default_device(device): - contact = types.Contact( - dist=jp.zeros((ncon,), dtype=float), - pos=jp.zeros((ncon, 3), dtype=float), - frame=jp.zeros((ncon, 3, 3), dtype=float), - includemargin=jp.zeros((ncon,), dtype=float), - friction=jp.zeros((ncon, 5), dtype=float), - solref=jp.zeros((ncon, mujoco.mjNREF), dtype=float), - solreffriction=jp.zeros((ncon, mujoco.mjNREF), dtype=float), - solimp=jp.zeros((ncon, mujoco.mjNIMP), dtype=float), - dim=dim, - # let jax pick contact.geom int precision, for interop with - # jax_enable_x64 - geom1=jp.full((ncon,), -1, dtype=int), - geom2=jp.full((ncon,), -1, dtype=int), - geom=jp.full((ncon, 2), -1, dtype=int), - efc_address=efc_address, + float_ = jp.zeros(1, float).dtype + int_ = jp.zeros(1, int).dtype + contact = types.Contact( + dist=np.zeros((ncon,), dtype=float_), + pos=np.zeros((ncon, 3), dtype=float_), + frame=np.zeros((ncon, 3, 3), dtype=float_), + includemargin=np.zeros((ncon,), dtype=float_), + friction=np.zeros((ncon, 5), dtype=float_), + solref=np.zeros((ncon, mujoco.mjNREF), dtype=float_), + solreffriction=np.zeros((ncon, mujoco.mjNREF), dtype=float_), + solimp=np.zeros((ncon, mujoco.mjNIMP), dtype=float_), + dim=dim, + # let jax pick contact.geom int precision, for interop with + # jax_enable_x64 + geom1=np.full((ncon,), -1, dtype=int_), + geom2=np.full((ncon,), -1, dtype=int_), + geom=np.full((ncon, 2), -1, dtype=int_), + efc_address=efc_address, + ) + + if m.opt.cone == types.ConeType.ELLIPTIC and np.any(contact.dim == 1): + raise NotImplementedError( + 'condim=1 with ConeType.ELLIPTIC not implemented.' ) - if m.opt.cone == types.ConeType.ELLIPTIC and np.any(contact.dim == 1): - raise NotImplementedError( - 'condim=1 with ConeType.ELLIPTIC not implemented.' - ) + zero_fields = { + 'solver_niter': (int_,), + 'time': (float_,), + 'qvel': (m.nv, float_), + 'act': (m.na, float_), + 'qacc_warmstart': (m.nv, float_), + 'ctrl': (m.nu, float_), + 'qfrc_applied': (m.nv, float_), + 'xfrc_applied': (m.nbody, 6, float_), + 'mocap_pos': (m.nmocap, 3, float_), + 'mocap_quat': (m.nmocap, 4, float_), + 'qacc': (m.nv, float_), + 'act_dot': (m.na, float_), + 'userdata': (m.nuserdata, float_), + 'sensordata': (m.nsensordata, float_), + 'xpos': (m.nbody, 3, float_), + 'xquat': (m.nbody, 4, float_), + 'xmat': (m.nbody, 3, 3, float_), + 'xipos': (m.nbody, 3, float_), + 'ximat': (m.nbody, 3, 3, float_), + 'xanchor': (m.njnt, 3, float_), + 'xaxis': (m.njnt, 3, float_), + 'geom_xpos': (m.ngeom, 3, float_), + 'geom_xmat': (m.ngeom, 3, 3, float_), + 'site_xpos': (m.nsite, 3, float_), + 'site_xmat': (m.nsite, 3, 3, float_), + 'cam_xpos': (m.ncam, 3, float_), + 'cam_xmat': (m.ncam, 3, 3, float_), + 'light_xpos': (m.nlight, 3, float_), + 'light_xdir': (m.nlight, 3, float_), + 'subtree_com': (m.nbody, 3, float_), + 'cdof': (m.nv, 6, float_), + 'cinert': (m.nbody, 10, float_), + 'flexvert_xpos': (m.nflexvert, 3, float_), + 'flexelem_aabb': (m.nflexelem, 6, float_), + 'flexedge_J_rownnz': (m.nflexedge, np.int32), + 'flexedge_J_rowadr': (m.nflexedge, np.int32), + 'flexedge_J_colind': (m.nflexedge, m.nv, np.int32), + 'flexedge_J': (m.nflexedge, m.nv, float_), + 'flexedge_length': (m.nflexedge, float_), + 'ten_wrapadr': (m.ntendon, np.int32), + 'ten_wrapnum': (m.ntendon, np.int32), + 'ten_J_rownnz': (m.ntendon, np.int32), + 'ten_J_rowadr': (m.ntendon, np.int32), + 'ten_J_colind': (m.ntendon, m.nv, np.int32), + 'ten_J': (m.ntendon, m.nv, float_), + 'ten_length': (m.ntendon, float_), + 'wrap_obj': (m.nwrap, 2, np.int32), + 'wrap_xpos': (m.nwrap, 6, float_), + 'actuator_length': (m.nu, float_), + 'moment_rownnz': (m.nu, np.int32), + 'moment_rowadr': (m.nu, np.int32), + 'moment_colind': (m.nJmom, np.int32), + 'actuator_moment': (m.nu, m.nv, float_), + 'crb': (m.nbody, 10, float_), + 'qM': (m.nM, float_) if support.is_sparse(m) else (m.nv, m.nv, float_), + 'qLD': (m.nM, float_) if support.is_sparse(m) else (m.nv, m.nv, float_), + 'qLDiagInv': (m.nv, float_) if support.is_sparse(m) else (0, float_), + 'bvh_aabb_dyn': (m.nbvhdynamic, 6, float_), + 'bvh_active': (m.nbvh, np.uint8), + 'flexedge_velocity': (m.nflexedge, float_), + 'ten_velocity': (m.ntendon, float_), + 'actuator_velocity': (m.nu, float_), + 'cvel': (m.nbody, 6, float_), + 'cdof_dot': (m.nv, 6, float_), + 'qfrc_bias': (m.nv, float_), + 'qfrc_spring': (m.nv, float_), + 'qfrc_damper': (m.nv, float_), + 'qfrc_gravcomp': (m.nv, float_), + 'qfrc_fluid': (m.nv, float_), + 'qfrc_passive': (m.nv, float_), + 'subtree_linvel': (m.nbody, 3, float_), + 'subtree_angmom': (m.nbody, 3, float_), + 'qH': (m.nM, float_) if support.is_sparse(m) else (m.nv, m.nv, float_), + 'qHDiagInv': (m.nv, float_), + 'B_rownnz': (m.nbody, np.int32), + 'B_rowadr': (m.nbody, np.int32), + 'B_colind': (m.nB, np.int32), + 'M_rownnz': (m.nv, np.int32), + 'M_rowadr': (m.nv, np.int32), + 'M_colind': (m.nM, np.int32), + 'mapM2M': (m.nM, np.int32), + 'C_rownnz': (m.nv, np.int32), + 'C_rowadr': (m.nv, np.int32), + 'C_colind': (m.nC, np.int32), + 'mapM2C': (m.nC, np.int32), + 'D_rownnz': (m.nv, np.int32), + 'D_rowadr': (m.nv, np.int32), + 'D_diag': (m.nv, np.int32), + 'D_colind': (m.nD, np.int32), + 'mapM2D': (m.nD, np.int32), + 'mapD2M': (m.nM, np.int32), + 'qDeriv': (m.nD, float_), + 'qLU': (m.nD, float_), + 'actuator_force': (m.nu, float_), + 'qfrc_actuator': (m.nv, float_), + 'qfrc_smooth': (m.nv, float_), + 'qacc_smooth': (m.nv, float_), + 'qfrc_constraint': (m.nv, float_), + 'qfrc_inverse': (m.nv, float_), + 'cacc': (m.nbody, 6, float_), + 'cfrc_int': (m.nbody, 6, float_), + 'cfrc_ext': (m.nbody, 6, float_), + 'efc_J': (nefc, m.nv, float_), + 'efc_pos': (nefc, float_), + 'efc_margin': (nefc, float_), + 'efc_frictionloss': (nefc, float_), + 'efc_D': (nefc, float_), + 'efc_aref': (nefc, float_), + 'efc_force': (nefc, float_), + '_qM_sparse': (m.nM, float_), + '_qLD_sparse': (m.nM, float_), + '_qLDiagInv_sparse': (m.nv, float_), + } - zero_fields = { - 'solver_niter': (int,), - 'time': (float,), - 'qvel': (m.nv, float), - 'act': (m.na, float), - 'qacc_warmstart': (m.nv, float), - 'ctrl': (m.nu, float), - 'qfrc_applied': (m.nv, float), - 'xfrc_applied': (m.nbody, 6, float), - 'mocap_pos': (m.nmocap, 3, float), - 'mocap_quat': (m.nmocap, 4, float), - 'qacc': (m.nv, float), - 'act_dot': (m.na, float), - 'userdata': (m.nuserdata, float), - 'sensordata': (m.nsensordata, float), - 'xpos': (m.nbody, 3, float), - 'xquat': (m.nbody, 4, float), - 'xmat': (m.nbody, 3, 3, float), - 'xipos': (m.nbody, 3, float), - 'ximat': (m.nbody, 3, 3, float), - 'xanchor': (m.njnt, 3, float), - 'xaxis': (m.njnt, 3, float), - 'geom_xpos': (m.ngeom, 3, float), - 'geom_xmat': (m.ngeom, 3, 3, float), - 'site_xpos': (m.nsite, 3, float), - 'site_xmat': (m.nsite, 3, 3, float), - 'cam_xpos': (m.ncam, 3, float), - 'cam_xmat': (m.ncam, 3, 3, float), - 'light_xpos': (m.nlight, 3, float), - 'light_xdir': (m.nlight, 3, float), - 'subtree_com': (m.nbody, 3, float), - 'cdof': (m.nv, 6, float), - 'cinert': (m.nbody, 10, float), - 'flexvert_xpos': (m.nflexvert, 3, float), - 'flexelem_aabb': (m.nflexelem, 6, float), - 'flexedge_J_rownnz': (m.nflexedge, jp.int32), - 'flexedge_J_rowadr': (m.nflexedge, jp.int32), - 'flexedge_J_colind': (m.nflexedge, m.nv, jp.int32), - 'flexedge_J': (m.nflexedge, m.nv, float), - 'flexedge_length': (m.nflexedge, float), - 'ten_wrapadr': (m.ntendon, jp.int32), - 'ten_wrapnum': (m.ntendon, jp.int32), - 'ten_J_rownnz': (m.ntendon, jp.int32), - 'ten_J_rowadr': (m.ntendon, jp.int32), - 'ten_J_colind': (m.ntendon, m.nv, jp.int32), - 'ten_J': (m.ntendon, m.nv, float), - 'ten_length': (m.ntendon, float), - 'wrap_obj': (m.nwrap, 2, jp.int32), - 'wrap_xpos': (m.nwrap, 6, float), - 'actuator_length': (m.nu, float), - 'moment_rownnz': (m.nu, jp.int32), - 'moment_rowadr': (m.nu, jp.int32), - 'moment_colind': (m.nJmom, jp.int32), - 'actuator_moment': (m.nu, m.nv, float), - 'crb': (m.nbody, 10, float), - 'qM': (m.nM, float) if support.is_sparse(m) else (m.nv, m.nv, float), - 'qLD': (m.nM, float) if support.is_sparse(m) else (m.nv, m.nv, float), - 'qLDiagInv': (m.nv, float) if support.is_sparse(m) else (0, float), - 'bvh_aabb_dyn': (m.nbvhdynamic, 6, float), - 'bvh_active': (m.nbvh, jp.uint8), - 'flexedge_velocity': (m.nflexedge, float), - 'ten_velocity': (m.ntendon, float), - 'actuator_velocity': (m.nu, float), - 'cvel': (m.nbody, 6, float), - 'cdof_dot': (m.nv, 6, float), - 'qfrc_bias': (m.nv, float), - 'qfrc_spring': (m.nv, float), - 'qfrc_damper': (m.nv, float), - 'qfrc_gravcomp': (m.nv, float), - 'qfrc_fluid': (m.nv, float), - 'qfrc_passive': (m.nv, float), - 'subtree_linvel': (m.nbody, 3, float), - 'subtree_angmom': (m.nbody, 3, float), - 'qH': (m.nM, float) if support.is_sparse(m) else (m.nv, m.nv, float), - 'qHDiagInv': (m.nv, float), - 'B_rownnz': (m.nbody, jp.int32), - 'B_rowadr': (m.nbody, jp.int32), - 'B_colind': (m.nB, jp.int32), - 'M_rownnz': (m.nv, jp.int32), - 'M_rowadr': (m.nv, jp.int32), - 'M_colind': (m.nM, jp.int32), - 'mapM2M': (m.nM, jp.int32), - 'C_rownnz': (m.nv, jp.int32), - 'C_rowadr': (m.nv, jp.int32), - 'C_colind': (m.nC, jp.int32), - 'mapM2C': (m.nC, jp.int32), - 'D_rownnz': (m.nv, jp.int32), - 'D_rowadr': (m.nv, jp.int32), - 'D_diag': (m.nv, jp.int32), - 'D_colind': (m.nD, jp.int32), - 'mapM2D': (m.nD, jp.int32), - 'mapD2M': (m.nM, jp.int32), - 'qDeriv': (m.nD, float), - 'qLU': (m.nD, float), - 'actuator_force': (m.nu, float), - 'qfrc_actuator': (m.nv, float), - 'qfrc_smooth': (m.nv, float), - 'qacc_smooth': (m.nv, float), - 'qfrc_constraint': (m.nv, float), - 'qfrc_inverse': (m.nv, float), - 'cacc': (m.nbody, 6, float), - 'cfrc_int': (m.nbody, 6, float), - 'cfrc_ext': (m.nbody, 6, float), - 'efc_J': (nefc, m.nv, float), - 'efc_pos': (nefc, float), - 'efc_margin': (nefc, float), - 'efc_frictionloss': (nefc, float), - 'efc_D': (nefc, float), - 'efc_aref': (nefc, float), - 'efc_force': (nefc, float), - '_qM_sparse': (m.nM, float), - '_qLD_sparse': (m.nM, float), - '_qLDiagInv_sparse': (m.nv, float), - } + if not _full_compat: + for f in types.Data.fields(): + if f.metadata.get('restricted_to') in ('mujoco', 'mjx'): + zero_fields[f.name] = (0, zero_fields[f.name][-1]) - if not _full_compat: - for f in types.Data.fields(): - if f.metadata.get('restricted_to') in ('mujoco', 'mjx'): - zero_fields[f.name] = (0, zero_fields[f.name][-1]) - - zero_fields = { - k: jp.zeros(v[:-1], dtype=v[-1]) for k, v in zero_fields.items() - } + zero_fields = { + k: np.zeros(v[:-1], dtype=v[-1]) for k, v in zero_fields.items() + } d = types.Data( ne=ne, @@ -391,12 +392,13 @@ def make_data( nl=nl, nefc=nefc, ncon=ncon, - qpos=jp.array(m.qpos0), + qpos=jp.array(m.qpos0, dtype=float_), contact=contact, efc_type=efc_type, eq_active=m.eq_active0, **zero_fields, ) + d = jax.device_put(d, device=device) return d From 16e49f2761355dc1278dce5861536c228a3bee9c Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 8 Apr 2025 03:24:24 -0700 Subject: [PATCH 045/191] Advertise the Model Editing tutorial https://youtu.be/LbANnKMDOHg?si=GiN7UPPhTh89c8aN PiperOrigin-RevId: 745065758 Change-Id: Ic3d3e0603b6f6360580effb52b6cf3438efe2a53 --- README.md | 2 ++ doc/changelog.rst | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/README.md b/README.md index 935c80b7..62d57c19 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,8 @@ running on Google Colab: - The **introductory** tutorial teaches MuJoCo basics: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/google-deepmind/mujoco/blob/main/python/tutorial.ipynb) + - The **Model Editing** tutorial shows how to create and edit models procedurally: + [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/google-deepmind/mujoco/blob/main/python/mjspec.ipynb) - The **rollout** tutorial shows how to use the multithreaded `rollout` module: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/google-deepmind/mujoco/blob/main/python/rollout.ipynb) - The **LQR** tutorial synthesizes a linear-quadratic controller, balancing a diff --git a/doc/changelog.rst b/doc/changelog.rst index b5d51bc0..2c5cbe8b 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -32,9 +32,19 @@ Bug fixes Python bindings ^^^^^^^^^^^^^^^ + +.. youtube:: LbANnKMDOHg + :aspect: 16:7 + :align: right + :width: 240px + +- Added examples of procedural model creation to the Model Editing tutorial: |mjspec_colab| - Added support for nameless :ref:`mjSpec` objects in the ``bind`` method, see the corresponding :ref:`section` in the documentation. +.. |mjspec_colab| image:: https://colab.research.google.com/assets/colab-badge.svg + :target: https://colab.research.google.com/github/google-deepmind/mujoco/blob/main/python/mjspec.ipynb + Version 3.3.0 (Feb 26, 2025) ---------------------------- From 96dda6ea75f63bb484df15a6a1633d0c7ed56997 Mon Sep 17 00:00:00 2001 From: Taylor Howell Date: Tue, 8 Apr 2025 05:15:27 -0700 Subject: [PATCH 046/191] Add tendon actuator force limits and tendon actuator force sensor. PiperOrigin-RevId: 745096883 Change-Id: Ib9acb727fbbfc6b0b0323ee6a889053a7a878056 --- doc/XMLreference.rst | 51 ++++++++++++++++ doc/XMLschema.rst | 27 +++++--- doc/changelog.rst | 2 + doc/includes/references.h | 7 +++ doc/modeling.rst | 5 +- include/mujoco/mjmodel.h | 3 + include/mujoco/mjspec.h | 2 + include/mujoco/mjvisualize.h | 2 + include/mujoco/mjxmacro.h | 2 + mjx/mujoco/mjx/_src/support_test.py | 2 +- mjx/mujoco/mjx/_src/types.py | 4 ++ python/mujoco/introspect/enums.py | 57 ++++++++--------- python/mujoco/introspect/structs.py | 43 +++++++++++++ src/engine/engine_forward.c | 42 ++++++++++++- src/engine/engine_io.c | 1 + src/engine/engine_sensor.c | 14 ++++- src/user/user_model.cc | 4 ++ src/user/user_objects.cc | 31 +++++++++- src/user/user_objects.h | 1 + src/xml/xml_native_reader.cc | 15 +++-- src/xml/xml_native_reader.h | 2 +- src/xml/xml_native_writer.cc | 6 ++ test/engine/engine_forward_test.cc | 54 ++++++++++++++++ .../testdata/actuation/tendon_force_clamp.xml | 55 +++++++++++++++++ test/user/user_objects_test.cc | 45 ++++++++++++++ unity/Runtime/Bindings/MjBindings.cs | 61 ++++++++++--------- 26 files changed, 464 insertions(+), 74 deletions(-) create mode 100644 test/engine/testdata/actuation/tendon_force_clamp.xml diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 6166691b..647a731b 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -4630,12 +4630,28 @@ length X, as in the clip on the right of `this example model solver. If this attribute is "auto", and :at:`autolimits` is set in :ref:`compiler `, length limits will be enabled if range is defined. +.. _tendon-spatial-actuatorfrclimited: + +:at:`actuatorfrclimited`: :at-val:`[false, true, auto], "auto"` + This attribute specifies whether actuator forces acting on the tendon should be clamped. See :ref:`CForceRange` for + details. This attribute interacts with the :ref:`actuatorfrcrange` attribute. If + this attribute is "false", actuator force clamping is disabled. If it is "true", actuator force clamping is enabled. + If this attribute is "auto", and :at:`autolimits` is set in :ref:`compiler `, actuator force clamping will + be enabled if :at:`actuatorfrcrange` is defined. + .. _tendon-spatial-range: :at:`range`: :at-val:`real(2), "0 0"` Range of allowed tendon lengths. Setting this attribute without specifying :at:`limited` is an error, unless :at:`autolimits` is set in :ref:`compiler `. +.. _tendon-spatial-actuatorfrcrange: + +:at:`actuatorfrcrange`: :at-val:`real(2), "0 0"` + Range for clamping total actuator forces acting on this tendon. See :ref:`CForceRange` for details. The compiler + expects the lower bound to be nonpositive and the upper bound to be nonnegative. |br| Setting this attribute without + specifying :at:`actuatorfrclimited` is an error if :at:`compiler-autolimits` is "false". + .. _tendon-spatial-solreflimit: .. _tendon-spatial-solimplimit: @@ -4820,8 +4836,12 @@ as above. .. _tendon-fixed-limited: +.. _tendon-fixed-actuatorfrclimited: + .. _tendon-fixed-range: +.. _tendon-fixed-actuatorfrcrange: + .. _tendon-fixed-solreflimit: .. _tendon-fixed-solimplimit: @@ -6385,6 +6405,33 @@ joint or when a single actuator acts on multiple joints. See :ref:`CForceRange` The joint where actuator forces will be sensed. The sensor output is copied from ``mjData.qfrc_actuator``. +.. _sensor-tendonactuatorfrc: + +:el-prefix:`sensor/` |-| **tendonactuatorfrc** (*) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This element creates an actuator force sensor, measured at a tendon. The quantity being sensed is the total force +contributed by all actuators to a single tendon. This type of sensor is important when multiple actuators act on a +single tendon. See :ref:`CForceRange` for details. + + +.. _sensor-tendonactuatorfrc-name: + +.. _sensor-tendonactuatorfrc-noise: + +.. _sensor-tendonactuatorfrc-cutoff: + +.. _sensor-tendonactuatorfrc-user: + +:at:`name`, :at:`noise`, :at:`cutoff`, :at:`user` + See :ref:`CSensor`. + +.. _sensor-tendonactuatorfrc-tendon: + +:at:`tendon`: :at-val:`string, required` + The tendon where actuator forces will be sensed. + + .. _sensor-ballquat: :el-prefix:`sensor/` |-| **ballquat** (*) @@ -8277,8 +8324,12 @@ if omitted. .. _default-tendon-limited: +.. _default-tendon-actuatorfrclimited: + .. _default-tendon-range: +.. _default-tendon-actuatorfrcrange: + .. _default-tendon-solreflimit: .. _default-tendon-solimplimit: diff --git a/doc/XMLschema.rst b/doc/XMLschema.rst index 56119cd7..9036d1c0 100644 --- a/doc/XMLschema.rst +++ b/doc/XMLschema.rst @@ -621,13 +621,15 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | | | | | :ref:`name` | :ref:`class` | :ref:`group` | :ref:`limited` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`range` | :ref:`solreflimit` | :ref:`solimplimit` | :ref:`solreffriction` | | +| | | | :ref:`actuatorfrclimited` | :ref:`range` | :ref:`actuatorfrcrange` | :ref:`solreflimit` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`solimpfriction` | :ref:`frictionloss` | :ref:`springlength` | :ref:`width` | | +| | | | :ref:`solimplimit` | :ref:`solreffriction` | :ref:`solimpfriction` | :ref:`frictionloss` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`material` | :ref:`margin` | :ref:`stiffness` | :ref:`damping` | | +| | | | :ref:`springlength` | :ref:`width` | :ref:`material` | :ref:`margin` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`armature` | :ref:`rgba` | :ref:`user` | | | +| | | | :ref:`stiffness` | :ref:`damping` | :ref:`armature` | :ref:`rgba` | | +| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +| | | | :ref:`user` | | | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_2| spatial |br| |_2| |L| | | .. table:: | @@ -657,11 +659,13 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | | | | | :ref:`name` | :ref:`class` | :ref:`group` | :ref:`limited` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`range` | :ref:`solreflimit` | :ref:`solimplimit` | :ref:`solreffriction` | | +| | | | :ref:`actuatorfrclimited` | :ref:`range` | :ref:`actuatorfrcrange` | :ref:`solreflimit` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`solimpfriction` | :ref:`frictionloss` | :ref:`springlength` | :ref:`margin` | | +| | | | :ref:`solimplimit` | :ref:`solreffriction` | :ref:`solimpfriction` | :ref:`frictionloss` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`stiffness` | :ref:`damping` | :ref:`armature` | :ref:`user` | | +| | | | :ref:`springlength` | :ref:`margin` | :ref:`stiffness` | :ref:`damping` | | +| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +| | | | :ref:`armature` | :ref:`user` | | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_2| fixed |br| |_2| |L| | | .. table:: | @@ -1004,6 +1008,15 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| sensor |br| |_| |L| | | .. table:: | +| :ref:`tendonactuatorfrc | \* | :class: mjcf-attributes | +| ` | | | +| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +| | | | :ref:`name` | :ref:`tendon` | :ref:`cutoff` | :ref:`noise` | | +| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +| | | | :ref:`user` | | | | | +| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | ++------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| |_| sensor |br| |_| |L| | | .. table:: | | :ref:`ballquat | \* | :class: mjcf-attributes | | ` | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | diff --git a/doc/changelog.rst b/doc/changelog.rst index 2c5cbe8b..deaba59e 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -20,6 +20,8 @@ General bodies when saving to XML. - Added :ref:`orientation` attribute to :ref:`composite`. Moreover, allow the composite to be the direct child of a frame. +- Added :ref:`tendon actuator force limits` and + :ref:`tendon actuator force sensor`. Bug fixes ^^^^^^^^^ diff --git a/doc/includes/references.h b/doc/includes/references.h index 846f0c75..b97f2b93 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -649,6 +649,7 @@ typedef enum mjtSensor_ { // type of sensor mjSENS_ACTUATORVEL, // scalar actuator velocity mjSENS_ACTUATORFRC, // scalar actuator force mjSENS_JOINTACTFRC, // scalar actuator force, measured at the joint + mjSENS_TENDONACTFRC, // scalar actuator force, measured at the tendon // sensors related to ball joints mjSENS_BALLQUAT, // 4D ball joint quaternion @@ -1330,12 +1331,14 @@ struct mjModel_ { int* tendon_matid; // material id for rendering (ntendon x 1) int* tendon_group; // group for visibility (ntendon x 1) mjtByte* tendon_limited; // does tendon have length limits (ntendon x 1) + mjtByte* tendon_actfrclimited; // does tendon have actuator force limits (ntendon x 1) mjtNum* tendon_width; // width for rendering (ntendon x 1) mjtNum* tendon_solref_lim; // constraint solver reference: limit (ntendon x mjNREF) mjtNum* tendon_solimp_lim; // constraint solver impedance: limit (ntendon x mjNIMP) mjtNum* tendon_solref_fri; // constraint solver reference: friction (ntendon x mjNREF) mjtNum* tendon_solimp_fri; // constraint solver impedance: friction (ntendon x mjNIMP) mjtNum* tendon_range; // tendon length limits (ntendon x 2) + mjtNum* tendon_actfrcrange; // range of total actuator force (ntendon x 2) mjtNum* tendon_margin; // min distance for limit detection (ntendon x 1) mjtNum* tendon_stiffness; // stiffness coefficient (ntendon x 1) mjtNum* tendon_damping; // damping coefficient (ntendon x 1) @@ -2186,7 +2189,9 @@ typedef struct mjsTendon_ { // tendon specification // length range int limited; // does tendon have limits (mjtLimited) + int actfrclimited; // does tendon have actuator force limits double range[2]; // length limits + double actfrcrange[2]; // actuator force limits double margin; // margin value for tendon limit detection mjtNum solref_limit[mjNREF]; // solver reference: tendon limits mjtNum solimp_limit[mjNIMP]; // solver impedance: tendon limits @@ -3086,8 +3091,10 @@ struct mjvSceneState_ { int* tendon_matid; int* tendon_group; mjtByte* tendon_limited; + mjtByte* tendon_actfrclimited; mjtNum* tendon_width; mjtNum* tendon_range; + mjtNum* tendon_actfrcrange; mjtNum* tendon_stiffness; mjtNum* tendon_damping; mjtNum* tendon_frictionloss; diff --git a/doc/modeling.rst b/doc/modeling.rst index 4ef331a4..61d79c13 100644 --- a/doc/modeling.rst +++ b/doc/modeling.rst @@ -724,7 +724,10 @@ Force clamping at joint input with :ref:`joint/actuatorfrcrange` sensor to report the total actuator force acting on a joint. The standard :ref:`actuatorfrc` sensor will continue to report the pre-clamped actuator force. -The three clamping options above are non-exclusive and can be combined as required. +Force clamping at tendon input with :ref:`tendon/actuatorfrcrange`: + This tendon attribute clamps input forces from all actuators acting on the tendon. + +The clamping options above are non-exclusive and can be combined as required. .. _CLengthRange: diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index 1699ca0c..b6bdbfc6 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -317,6 +317,7 @@ typedef enum mjtSensor_ { // type of sensor mjSENS_ACTUATORVEL, // scalar actuator velocity mjSENS_ACTUATORFRC, // scalar actuator force mjSENS_JOINTACTFRC, // scalar actuator force, measured at the joint + mjSENS_TENDONACTFRC, // scalar actuator force, measured at the tendon // sensors related to ball joints mjSENS_BALLQUAT, // 4D ball joint quaternion @@ -1031,12 +1032,14 @@ struct mjModel_ { int* tendon_matid; // material id for rendering (ntendon x 1) int* tendon_group; // group for visibility (ntendon x 1) mjtByte* tendon_limited; // does tendon have length limits (ntendon x 1) + mjtByte* tendon_actfrclimited; // does tendon have actuator force limits (ntendon x 1) mjtNum* tendon_width; // width for rendering (ntendon x 1) mjtNum* tendon_solref_lim; // constraint solver reference: limit (ntendon x mjNREF) mjtNum* tendon_solimp_lim; // constraint solver impedance: limit (ntendon x mjNIMP) mjtNum* tendon_solref_fri; // constraint solver reference: friction (ntendon x mjNREF) mjtNum* tendon_solimp_fri; // constraint solver impedance: friction (ntendon x mjNIMP) mjtNum* tendon_range; // tendon length limits (ntendon x 2) + mjtNum* tendon_actfrcrange; // range of total actuator force (ntendon x 2) mjtNum* tendon_margin; // min distance for limit detection (ntendon x 1) mjtNum* tendon_stiffness; // stiffness coefficient (ntendon x 1) mjtNum* tendon_damping; // damping coefficient (ntendon x 1) diff --git a/include/mujoco/mjspec.h b/include/mujoco/mjspec.h index 42b8c03f..3202d9dc 100644 --- a/include/mujoco/mjspec.h +++ b/include/mujoco/mjspec.h @@ -628,7 +628,9 @@ typedef struct mjsTendon_ { // tendon specification // length range int limited; // does tendon have limits (mjtLimited) + int actfrclimited; // does tendon have actuator force limits double range[2]; // length limits + double actfrcrange[2]; // actuator force limits double margin; // margin value for tendon limit detection mjtNum solref_limit[mjNREF]; // solver reference: tendon limits mjtNum solimp_limit[mjNIMP]; // solver impedance: tendon limits diff --git a/include/mujoco/mjvisualize.h b/include/mujoco/mjvisualize.h index 19dad80c..0a757cf1 100644 --- a/include/mujoco/mjvisualize.h +++ b/include/mujoco/mjvisualize.h @@ -593,8 +593,10 @@ struct mjvSceneState_ { int* tendon_matid; int* tendon_group; mjtByte* tendon_limited; + mjtByte* tendon_actfrclimited; mjtNum* tendon_width; mjtNum* tendon_range; + mjtNum* tendon_actfrcrange; mjtNum* tendon_stiffness; mjtNum* tendon_damping; mjtNum* tendon_frictionloss; diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index 0aaf1af6..704ba353 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -475,12 +475,14 @@ XMJV( int, tendon_matid, ntendon, 1 ) \ XMJV( int, tendon_group, ntendon, 1 ) \ XMJV( mjtByte, tendon_limited, ntendon, 1 ) \ + XMJV( mjtByte, tendon_actfrclimited, ntendon, 1 ) \ XMJV( mjtNum, tendon_width, ntendon, 1 ) \ X ( mjtNum, tendon_solref_lim, ntendon, mjNREF ) \ X ( mjtNum, tendon_solimp_lim, ntendon, mjNIMP ) \ X ( mjtNum, tendon_solref_fri, ntendon, mjNREF ) \ X ( mjtNum, tendon_solimp_fri, ntendon, mjNIMP ) \ XMJV( mjtNum, tendon_range, ntendon, 2 ) \ + XMJV( mjtNum, tendon_actfrcrange, ntendon, 2 ) \ X ( mjtNum, tendon_margin, ntendon, 1 ) \ XMJV( mjtNum, tendon_stiffness, ntendon, 1 ) \ XMJV( mjtNum, tendon_damping, ntendon, 1 ) \ diff --git a/mjx/mujoco/mjx/_src/support_test.py b/mjx/mujoco/mjx/_src/support_test.py index 373b6a90..1a0c904c 100644 --- a/mjx/mujoco/mjx/_src/support_test.py +++ b/mjx/mujoco/mjx/_src/support_test.py @@ -349,7 +349,7 @@ class SupportTest(parameterized.TestCase): self.assertEqual( str(e.exception), 'mjSpec signature does not match mjx.Model signature:' - ' 4300287342280373816 != 9887180086914550999', + ' 17856615236057737915 != 12517827274439268436', ) _CONTACTS = """ diff --git a/mjx/mujoco/mjx/_src/types.py b/mjx/mujoco/mjx/_src/types.py index 9052043c..3ee10499 100644 --- a/mjx/mujoco/mjx/_src/types.py +++ b/mjx/mujoco/mjx/_src/types.py @@ -763,11 +763,13 @@ class Model(PyTreeNode): tendon_adr: address of first object in tendon's path (ntendon,) tendon_num: number of objects in tendon's path (ntendon,) tendon_limited: does tendon have length limits (ntendon,) + tendon_actfrclimited: tendon has actuator force limits (ntendon,) tendon_solref_lim: constraint solver reference: limit (ntendon, mjNREF) tendon_solimp_lim: constraint solver impedance: limit (ntendon, mjNIMP) tendon_solref_fri: constraint solver reference: friction (ntendon, mjNREF) tendon_solimp_fri: constraint solver impedance: friction (ntendon, mjNIMP) tendon_range: tendon length limits (ntendon, 2) + tendon_actfrcrange: tendon actuator force limits (ntendon, 2) tendon_margin: min distance for limit detection (ntendon,) tendon_stiffness: stiffness coefficient (ntendon,) tendon_damping: damping coefficient (ntendon,) @@ -1106,11 +1108,13 @@ class Model(PyTreeNode): tendon_adr: np.ndarray tendon_num: np.ndarray tendon_limited: np.ndarray + tendon_actfrclimited: np.ndarray tendon_solref_lim: jax.Array tendon_solimp_lim: jax.Array tendon_solref_fri: jax.Array tendon_solimp_fri: jax.Array tendon_range: jax.Array + tendon_actfrcrange: jax.Array tendon_margin: jax.Array tendon_stiffness: jax.Array tendon_damping: jax.Array diff --git a/python/mujoco/introspect/enums.py b/python/mujoco/introspect/enums.py index 9443566f..dd1a1fad 100644 --- a/python/mujoco/introspect/enums.py +++ b/python/mujoco/introspect/enums.py @@ -340,34 +340,35 @@ ENUMS: Mapping[str, EnumDecl] = dict([ ('mjSENS_ACTUATORVEL', 14), ('mjSENS_ACTUATORFRC', 15), ('mjSENS_JOINTACTFRC', 16), - ('mjSENS_BALLQUAT', 17), - ('mjSENS_BALLANGVEL', 18), - ('mjSENS_JOINTLIMITPOS', 19), - ('mjSENS_JOINTLIMITVEL', 20), - ('mjSENS_JOINTLIMITFRC', 21), - ('mjSENS_TENDONLIMITPOS', 22), - ('mjSENS_TENDONLIMITVEL', 23), - ('mjSENS_TENDONLIMITFRC', 24), - ('mjSENS_FRAMEPOS', 25), - ('mjSENS_FRAMEQUAT', 26), - ('mjSENS_FRAMEXAXIS', 27), - ('mjSENS_FRAMEYAXIS', 28), - ('mjSENS_FRAMEZAXIS', 29), - ('mjSENS_FRAMELINVEL', 30), - ('mjSENS_FRAMEANGVEL', 31), - ('mjSENS_FRAMELINACC', 32), - ('mjSENS_FRAMEANGACC', 33), - ('mjSENS_SUBTREECOM', 34), - ('mjSENS_SUBTREELINVEL', 35), - ('mjSENS_SUBTREEANGMOM', 36), - ('mjSENS_GEOMDIST', 37), - ('mjSENS_GEOMNORMAL', 38), - ('mjSENS_GEOMFROMTO', 39), - ('mjSENS_E_POTENTIAL', 40), - ('mjSENS_E_KINETIC', 41), - ('mjSENS_CLOCK', 42), - ('mjSENS_PLUGIN', 43), - ('mjSENS_USER', 44), + ('mjSENS_TENDONACTFRC', 17), + ('mjSENS_BALLQUAT', 18), + ('mjSENS_BALLANGVEL', 19), + ('mjSENS_JOINTLIMITPOS', 20), + ('mjSENS_JOINTLIMITVEL', 21), + ('mjSENS_JOINTLIMITFRC', 22), + ('mjSENS_TENDONLIMITPOS', 23), + ('mjSENS_TENDONLIMITVEL', 24), + ('mjSENS_TENDONLIMITFRC', 25), + ('mjSENS_FRAMEPOS', 26), + ('mjSENS_FRAMEQUAT', 27), + ('mjSENS_FRAMEXAXIS', 28), + ('mjSENS_FRAMEYAXIS', 29), + ('mjSENS_FRAMEZAXIS', 30), + ('mjSENS_FRAMELINVEL', 31), + ('mjSENS_FRAMEANGVEL', 32), + ('mjSENS_FRAMELINACC', 33), + ('mjSENS_FRAMEANGACC', 34), + ('mjSENS_SUBTREECOM', 35), + ('mjSENS_SUBTREELINVEL', 36), + ('mjSENS_SUBTREEANGMOM', 37), + ('mjSENS_GEOMDIST', 38), + ('mjSENS_GEOMNORMAL', 39), + ('mjSENS_GEOMFROMTO', 40), + ('mjSENS_E_POTENTIAL', 41), + ('mjSENS_E_KINETIC', 42), + ('mjSENS_CLOCK', 43), + ('mjSENS_PLUGIN', 44), + ('mjSENS_USER', 45), ]), )), ('mjtStage', diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index 59b21fb8..51799032 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -3615,6 +3615,14 @@ STRUCTS: Mapping[str, StructDecl] = dict([ doc='does tendon have length limits', array_extent=('ntendon',), ), + StructFieldDecl( + name='tendon_actfrclimited', + type=PointerType( + inner_type=ValueType(name='mjtByte'), + ), + doc='does tendon have actuator force limits', + array_extent=('ntendon',), + ), StructFieldDecl( name='tendon_width', type=PointerType( @@ -3663,6 +3671,14 @@ STRUCTS: Mapping[str, StructDecl] = dict([ doc='tendon length limits', array_extent=('ntendon', 2), ), + StructFieldDecl( + name='tendon_actfrcrange', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='range of total actuator force', + array_extent=('ntendon', 2), + ), StructFieldDecl( name='tendon_margin', type=PointerType( @@ -8135,6 +8151,13 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), doc='', ), + StructFieldDecl( + name='tendon_actfrclimited', + type=PointerType( + inner_type=ValueType(name='mjtByte'), + ), + doc='', + ), StructFieldDecl( name='tendon_width', type=PointerType( @@ -8149,6 +8172,13 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), doc='', ), + StructFieldDecl( + name='tendon_actfrcrange', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='', + ), StructFieldDecl( name='tendon_stiffness', type=PointerType( @@ -11342,6 +11372,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='int'), doc='does tendon have limits (mjtLimited)', ), + StructFieldDecl( + name='actfrclimited', + type=ValueType(name='int'), + doc='does tendon have actuator force limits', + ), StructFieldDecl( name='range', type=ArrayType( @@ -11350,6 +11385,14 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), doc='length limits', ), + StructFieldDecl( + name='actfrcrange', + type=ArrayType( + inner_type=ValueType(name='double'), + extents=(2,), + ), + doc='actuator force limits', + ), StructFieldDecl( name='margin', type=ValueType(name='double'), diff --git a/src/engine/engine_forward.c b/src/engine/engine_forward.c index d55f857b..5771db32 100644 --- a/src/engine/engine_forward.c +++ b/src/engine/engine_forward.c @@ -276,7 +276,7 @@ static void clampVec(mjtNum* vec, const mjtNum* range, const mjtByte* limited, i // (qpos, qvel, ctrl, act) => (qfrc_actuator, actuator_force, act_dot) void mj_fwdActuation(const mjModel* m, mjData* d) { TM_START; - int nv = m->nv, nu = m->nu; + int nv = m->nv, nu = m->nu, ntendon = m->ntendon; mjtNum gain, bias, tau; mjtNum *prm, *force = d->actuator_force; @@ -289,6 +289,9 @@ void mj_fwdActuation(const mjModel* m, mjData* d) { return; } + // any tendon transmission targets with force limits + int tendon_frclimited = 0; + // local, clamped copy of ctrl mj_markStack(d); mjtNum *ctrl = mjSTACKALLOC(d, nu, mjtNum); @@ -384,6 +387,11 @@ void mj_fwdActuation(const mjModel* m, mjData* d) { continue; } + // check for tendon transmission with force limits + if (ntendon && !tendon_frclimited && m->actuator_trntype[i] == mjTRN_TENDON) { + tendon_frclimited = m->tendon_actfrclimited[m->actuator_trnid[2*i]]; + } + // extract gain info prm = m->actuator_gainprm + mjNGAIN*i; @@ -479,6 +487,38 @@ void mj_fwdActuation(const mjModel* m, mjData* d) { } } + // clamp tendon total actuator force + if (tendon_frclimited) { + // compute total force for each tendon + mjtNum* tendon_total_force = mjSTACKALLOC(d, ntendon, mjtNum); + mju_zero(tendon_total_force, ntendon); + for (int i=0; i < nu; i++) { + if (m->actuator_trntype[i] == mjTRN_TENDON) { + int tendon_id = m->actuator_trnid[2*i]; + if (m->tendon_actfrclimited[tendon_id]) { + tendon_total_force[tendon_id] += force[i]; + } + } + } + + // scale tendon actuator forces if limited and outside range + for (int i=0; i < nu; i++) { + if (m->actuator_trntype[i] != mjTRN_TENDON) { + continue; + } + int tendon_id = m->actuator_trnid[2*i]; + mjtNum tendon_force = tendon_total_force[tendon_id]; + if (m->tendon_actfrclimited[tendon_id] && tendon_force) { + const mjtNum* range = m->tendon_actfrcrange + 2 * tendon_id; + if (tendon_force < range[0]) { + force[i] *= range[0] / tendon_force; + } else if (tendon_force > range[1]) { + force[i] *= range[1] / tendon_force; + } + } + } + } + // clamp actuator_force clampVec(force, m->actuator_forcerange, m->actuator_forcelimited, nu, NULL); diff --git a/src/engine/engine_io.c b/src/engine/engine_io.c index f982941f..0f6878e1 100644 --- a/src/engine/engine_io.c +++ b/src/engine/engine_io.c @@ -2079,6 +2079,7 @@ static int sensorSize(mjtSensor sensor_type, int sensor_dim) { case mjSENS_ACTUATORVEL: case mjSENS_ACTUATORFRC: case mjSENS_JOINTACTFRC: + case mjSENS_TENDONACTFRC: case mjSENS_JOINTLIMITPOS: case mjSENS_JOINTLIMITVEL: case mjSENS_JOINTLIMITFRC: diff --git a/src/engine/engine_sensor.c b/src/engine/engine_sensor.c index baad81c4..936dc085 100644 --- a/src/engine/engine_sensor.c +++ b/src/engine/engine_sensor.c @@ -698,8 +698,8 @@ void mj_sensorVel(const mjModel* m, mjData* d) { // acceleration/force-dependent sensors void mj_sensorAcc(const mjModel* m, mjData* d) { int rootid, bodyid, objtype, objid, adr, nusersensor = 0; - int ne = d->ne, nf = d->nf, nefc = d->nefc; - mjtNum tmp[6], conforce[6], conray[3]; + int ne = d->ne, nf = d->nf, nefc = d->nefc, nu = m->nu; + mjtNum tmp[6], conforce[6], conray[3], frc; mjContact* con; // disabled sensors: return @@ -825,6 +825,16 @@ void mj_sensorAcc(const mjModel* m, mjData* d) { d->sensordata[adr] = d->qfrc_actuator[m->jnt_dofadr[objid]]; break; + case mjSENS_TENDONACTFRC: // tendonactfrc + frc = 0.0; + for (int j=0; j < nu; j++) { + if (m->actuator_trntype[j] == mjTRN_TENDON && m->actuator_trnid[2*j] == objid) { + frc += d->actuator_force[j]; + } + } + d->sensordata[adr] = frc; + break; + case mjSENS_JOINTLIMITFRC: // jointlimitfrc d->sensordata[adr] = 0; for (int j=ne+nf; j < nefc; j++) { diff --git a/src/user/user_model.cc b/src/user/user_model.cc index c6b181d0..ff78e190 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -3351,6 +3351,7 @@ void mjCModel::CopyObjects(mjModel* m) { m->tendon_matid[i] = pte->matid; m->tendon_group[i] = pte->group; m->tendon_limited[i] = (mjtByte)pte->is_limited(); + m->tendon_actfrclimited[i] = (mjtByte)pte->is_actfrclimited(); m->tendon_width[i] = (mjtNum)pte->width; mjuu_copyvec(m->tendon_solref_lim+mjNREF*i, pte->solref_limit, mjNREF); mjuu_copyvec(m->tendon_solimp_lim+mjNIMP*i, pte->solimp_limit, mjNIMP); @@ -3358,6 +3359,8 @@ void mjCModel::CopyObjects(mjModel* m) { mjuu_copyvec(m->tendon_solimp_fri+mjNIMP*i, pte->solimp_friction, mjNIMP); m->tendon_range[2*i] = (mjtNum)pte->range[0]; m->tendon_range[2*i+1] = (mjtNum)pte->range[1]; + m->tendon_actfrcrange[2*i] = (mjtNum)pte->actfrcrange[0]; + m->tendon_actfrcrange[2*i+1] = (mjtNum)pte->actfrcrange[1]; m->tendon_margin[i] = (mjtNum)pte->margin; m->tendon_stiffness[i] = (mjtNum)pte->stiffness; m->tendon_damping[i] = (mjtNum)pte->damping; @@ -5005,6 +5008,7 @@ bool mjCModel::CopyBack(const mjModel* m) { // tendons for (int i=0; i < ntendon; i++) { mjuu_copyvec(tendons_[i]->range, m->tendon_range+2*i, 2); + mjuu_copyvec(tendons_[i]->actfrcrange, m->tendon_actfrcrange+2*i, 2); mjuu_copyvec(tendons_[i]->solref_limit, m->tendon_solref_lim+mjNREF*i, mjNREF); mjuu_copyvec(tendons_[i]->solimp_limit, m->tendon_solimp_lim+mjNIMP*i, mjNIMP); mjuu_copyvec(tendons_[i]->solref_friction, m->tendon_solref_fri+mjNREF*i, mjNREF); diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 827b5e48..d609621c 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -5388,7 +5388,9 @@ mjCTendon& mjCTendon::operator=(const mjCTendon& other) { bool mjCTendon::is_limited() const { return islimited(limited, range); } - +bool mjCTendon::is_actfrclimited() const { + return islimited(actfrclimited, actfrcrange); +} void mjCTendon::PointToLocal() { spec.element = static_cast(this); @@ -5686,6 +5688,21 @@ void mjCTendon::Compile(void) { throw mjCError(this, "invalid limits in tendon"); } + // if limited is auto, set to 1 if range is specified, otherwise unlimited + if (actfrclimited == mjLIMITED_AUTO) { + bool hasactfrcrange = !(actfrcrange[0] == 0 && actfrcrange[1] == 0); + checklimited(this, compiler->autolimits, "tendon", "", actfrclimited, + hasactfrcrange); + } + + // check actfrclimits + if (actfrcrange[0] >= actfrcrange[1] && is_actfrclimited()) { + throw mjCError(this, "invalid actuatorfrcrange in tendon"); + } + if ((actfrcrange[0] > 0 || actfrcrange[1] < 0) && is_actfrclimited()) { + throw mjCError(this, "invalid actuatorfrcrange in tendon"); + } + // check springlength if (springlength[0] > springlength[1]) { throw mjCError(this, "invalid springlength in tendon"); @@ -6479,6 +6496,18 @@ void mjCSensor::Compile(void) { } break; + case mjSENS_TENDONACTFRC: + // must be attached to tendon + if (objtype != mjOBJ_TENDON) { + throw mjCError(this, "sensor must be attached to tendon"); + } + + // set + dim = 1; + datatype = mjDATATYPE_REAL; + needstage = mjSTAGE_ACC; + break; + case mjSENS_TENDONPOS: case mjSENS_TENDONVEL: // must be attached to tendon diff --git a/src/user/user_objects.h b/src/user/user_objects.h index ce890610..16aa57af 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -1523,6 +1523,7 @@ class mjCTendon : public mjCTendon_, private mjsTendon { void SetModel(mjCModel* _model); bool is_limited() const; + bool is_actfrclimited() const; private: void Compile(void); // compiler diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index f3afd4a2..a5bf6824 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -363,8 +363,8 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { {"tendon", "*", "0"}, {"<"}, - {"spatial", "*", "19", "name", "class", "group", "limited", "range", - "solreflimit", "solimplimit", "solreffriction", "solimpfriction", + {"spatial", "*", "21", "name", "class", "group", "limited", "actuatorfrclimited", "range", + "actuatorfrcrange", "solreflimit", "solimplimit", "solreffriction", "solimpfriction", "frictionloss", "springlength", "width", "material", "margin", "stiffness", "damping", "armature", "rgba", "user"}, {"<"}, @@ -372,8 +372,8 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { {"geom", "*", "2", "geom", "sidesite"}, {"pulley", "*", "1", "divisor"}, {">"}, - {"fixed", "*", "16", "name", "class", "group", "limited", "range", - "solreflimit", "solimplimit", "solreffriction", "solimpfriction", + {"fixed", "*", "18", "name", "class", "group", "limited", "actuatorfrclimited", "range", + "actuatorfrcrange","solreflimit", "solimplimit", "solreffriction", "solimpfriction", "frictionloss", "springlength", "margin", "stiffness", "damping", "armature", "user"}, {"<"}, {"joint", "*", "2", "joint", "coef"}, @@ -455,6 +455,7 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { {"actuatorvel", "*", "5", "name", "actuator", "cutoff", "noise", "user"}, {"actuatorfrc", "*", "5", "name", "actuator", "cutoff", "noise", "user"}, {"jointactuatorfrc", "*", "5", "name", "joint", "cutoff", "noise", "user"}, + {"tendonactuatorfrc", "*", "5", "name", "tendon", "cutoff", "noise", "user"}, {"ballquat", "*", "5", "name", "joint", "cutoff", "noise", "user"}, {"ballangvel", "*", "5", "name", "joint", "cutoff", "noise", "user"}, {"jointlimitpos", "*", "5", "name", "joint", "cutoff", "noise", "user"}, @@ -2052,12 +2053,14 @@ void mjXReader::OneTendon(XMLElement* elem, mjsTendon* tendon) { mjs_setString(tendon->material, material.c_str()); } MapValue(elem, "limited", &tendon->limited, TFAuto_map, 3); + MapValue(elem, "actuatorfrclimited", &tendon->actfrclimited, TFAuto_map, 3); ReadAttr(elem, "width", 1, &tendon->width, text); ReadAttr(elem, "solreflimit", mjNREF, tendon->solref_limit, text, false, false); ReadAttr(elem, "solimplimit", mjNIMP, tendon->solimp_limit, text, false, false); ReadAttr(elem, "solreffriction", mjNREF, tendon->solref_friction, text, false, false); ReadAttr(elem, "solimpfriction", mjNIMP, tendon->solimp_friction, text, false, false); ReadAttr(elem, "range", 2, tendon->range, text); + ReadAttr(elem, "actuatorfrcrange", 2, tendon->actfrcrange, text); ReadAttr(elem, "margin", 1, &tendon->margin, text); ReadAttr(elem, "stiffness", 1, &tendon->stiffness, text); ReadAttr(elem, "damping", 1, &tendon->damping, text); @@ -3973,6 +3976,10 @@ void mjXReader::Sensor(XMLElement* section) { sensor->type = mjSENS_JOINTACTFRC; sensor->objtype = mjOBJ_JOINT; ReadAttrTxt(elem, "joint", objname, true); + } else if (type=="tendonactuatorfrc") { + sensor->type = mjSENS_TENDONACTFRC; + sensor->objtype = mjOBJ_TENDON; + ReadAttrTxt(elem, "tendon", objname, true); } // sensors related to ball joints diff --git a/src/xml/xml_native_reader.h b/src/xml/xml_native_reader.h index 0e568f99..010ea2d5 100644 --- a/src/xml/xml_native_reader.h +++ b/src/xml/xml_native_reader.h @@ -102,7 +102,7 @@ class mjXReader : public mjXBase { }; // MJCF schema -#define nMJCF 237 +#define nMJCF 238 extern const char* MJCF[nMJCF][mjXATTRNUM]; #endif // MUJOCO_SRC_XML_XML_NATIVE_READER_H_ diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index 5f2d44cf..001ecf18 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -725,7 +725,9 @@ void mjXWriter::OneTendon(XMLElement* elem, const mjCTendon* tendon, mjCDef* def WriteAttr(elem, "solimpfriction", mjNIMP, tendon->solimp_friction, def->Tendon().solimp_friction, true); WriteAttrKey(elem, "limited", TFAuto_map, 3, tendon->limited, def->Tendon().limited); + WriteAttrKey(elem, "actuatorfrclimited", TFAuto_map, 3, tendon->actfrclimited, def->Tendon().actfrclimited); WriteAttr(elem, "range", 2, tendon->range, def->Tendon().range); + WriteAttr(elem, "actuatorfrcrange", 2, tendon->actfrcrange, def->Tendon().actfrcrange); WriteAttr(elem, "margin", 1, &tendon->margin, &def->Tendon().margin); WriteAttr(elem, "stiffness", 1, &tendon->stiffness, &def->Tendon().stiffness); WriteAttr(elem, "damping", 1, &tendon->damping, &def->Tendon().damping); @@ -2033,6 +2035,10 @@ void mjXWriter::Sensor(XMLElement* root) { elem = InsertEnd(section, "jointactuatorfrc"); WriteAttrTxt(elem, "joint", sensor->get_objname()); break; + case mjSENS_TENDONACTFRC: + elem = InsertEnd(section, "tendonactuatorfrc"); + WriteAttrTxt(elem, "tendon", sensor->get_objname()); + break; // sensors related to ball joints case mjSENS_BALLQUAT: diff --git a/test/engine/engine_forward_test.cc b/test/engine/engine_forward_test.cc index 16a52f43..d345b065 100644 --- a/test/engine/engine_forward_test.cc +++ b/test/engine/engine_forward_test.cc @@ -46,6 +46,8 @@ static const char* const kDampedActuatorsPath = "engine/testdata/derivative/damped_actuators.xml"; static const char* const kJointForceClamp = "engine/testdata/actuation/joint_force_clamp.xml"; +static const char* const kTendonForceClamp = + "engine/testdata/actuation/tendon_force_clamp.xml"; using ::testing::Pointwise; using ::testing::DoubleNear; @@ -1393,5 +1395,57 @@ TEST_F(ActuatorTest, DisableActuatorOutOfRange) { mj_deleteModel(model); } +TEST_F(ActuatorTest, TendonActuatorForceRange) { + const std::string xml_path = GetTestDataFilePath(kTendonForceClamp); + mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0); + mjData* data = mj_makeData(model); + + EXPECT_EQ(model->tendon_actfrclimited[0], 0); + EXPECT_EQ(model->tendon_actfrcrange[0], 0); + EXPECT_EQ(model->tendon_actfrcrange[1], 0); + + EXPECT_EQ(model->tendon_actfrclimited[1], 1); + EXPECT_EQ(model->tendon_actfrcrange[2], -1); + EXPECT_EQ(model->tendon_actfrcrange[3], 1); + + EXPECT_EQ(model->tendon_actfrclimited[2], 1); + EXPECT_EQ(model->tendon_actfrcrange[4], -10); + EXPECT_EQ(model->tendon_actfrcrange[5], 10); + + EXPECT_EQ(model->tendon_actfrclimited[3], 1); + EXPECT_EQ(model->tendon_actfrcrange[6], 0); + EXPECT_EQ(model->tendon_actfrcrange[7], 1); + + data->ctrl[0] = 1; + data->ctrl[1] = 1; + data->ctrl[2] = 1; + + data->ctrl[3] = -1; + data->ctrl[4] = 1; + + data->ctrl[5] = -20; + data->ctrl[6] = 5; + data->ctrl[7] = -5; + + mj_forward(model, data); + + EXPECT_NEAR(data->actuator_force[0], 1, 1e-6); + EXPECT_NEAR(data->actuator_force[1], 1, 1e-6); + EXPECT_NEAR(data->actuator_force[2], 1, 1e-6); + EXPECT_NEAR(data->actuator_force[3], -1, 1e-6); + EXPECT_NEAR(data->actuator_force[4], 1, 1e-6); + EXPECT_NEAR(data->actuator_force[5], -10, 1e-6); + EXPECT_NEAR(data->actuator_force[6], 5, 1e-6); + EXPECT_NEAR(data->actuator_force[7], -5, 1e-6); + + EXPECT_EQ(data->sensordata[0], 3); + EXPECT_EQ(data->sensordata[1], 0); + EXPECT_EQ(data->sensordata[2], -10); + EXPECT_EQ(data->sensordata[3], 0); + + mj_deleteData(data); + mj_deleteModel(model); +} + } // namespace } // namespace mujoco diff --git a/test/engine/testdata/actuation/tendon_force_clamp.xml b/test/engine/testdata/actuation/tendon_force_clamp.xml new file mode 100644 index 00000000..51952b3a --- /dev/null +++ b/test/engine/testdata/actuation/tendon_force_clamp.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/user/user_objects_test.cc b/test/user/user_objects_test.cc index b9f13cd4..37bec917 100644 --- a/test/user/user_objects_test.cc +++ b/test/user/user_objects_test.cc @@ -2070,6 +2070,51 @@ TEST_F(TendonTest, SiteBetweenPulleyNotAllowed) { EXPECT_THAT(error.data(), HasSubstr("line 9")); } +TEST_F(TendonTest, ActuatorForceRangeNotAllowed) { + std::string xml = R"( + + + + + + + + + + + + + + + + )"; + + std::array error; + std::string str_replace = "{}"; + size_t rng_ind = xml.find(str_replace); + + std::string xml0 = xml; + std::string range0 = "-2 -1"; + xml0.replace(rng_ind, str_replace.length(), range0); + mjModel* m0 = LoadModelFromString(xml0.c_str(), error.data(), error.size()); + EXPECT_THAT(m0, IsNull()); + EXPECT_THAT(error.data(), HasSubstr("invalid actuatorfrcrange in tendon")); + + std::string xml1 = xml; + std::string range1 = "1 2"; + xml1.replace(rng_ind, str_replace.length(), range1); + mjModel* m1 = LoadModelFromString(xml1.c_str(), error.data(), error.size()); + EXPECT_THAT(m1, IsNull()); + EXPECT_THAT(error.data(), HasSubstr("invalid actuatorfrcrange in tendon")); + + std::string xml2 = xml; + std::string range2 = "1 0"; + xml2.replace(rng_ind, str_replace.length(), range2); + mjModel* m2 = LoadModelFromString(xml2.c_str(), error.data(), error.size()); + EXPECT_THAT(m2, IsNull()); + EXPECT_THAT(error.data(), HasSubstr("invalid actuatorfrcrange in tendon")); +} + // ------------- tests for tendon springrange ---------------------------------- using SpringrangeTest = MujocoTest; diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index f7d6ac29..ca702ca0 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -357,34 +357,35 @@ public enum mjtSensor : int{ mjSENS_ACTUATORVEL = 14, mjSENS_ACTUATORFRC = 15, mjSENS_JOINTACTFRC = 16, - mjSENS_BALLQUAT = 17, - mjSENS_BALLANGVEL = 18, - mjSENS_JOINTLIMITPOS = 19, - mjSENS_JOINTLIMITVEL = 20, - mjSENS_JOINTLIMITFRC = 21, - mjSENS_TENDONLIMITPOS = 22, - mjSENS_TENDONLIMITVEL = 23, - mjSENS_TENDONLIMITFRC = 24, - mjSENS_FRAMEPOS = 25, - mjSENS_FRAMEQUAT = 26, - mjSENS_FRAMEXAXIS = 27, - mjSENS_FRAMEYAXIS = 28, - mjSENS_FRAMEZAXIS = 29, - mjSENS_FRAMELINVEL = 30, - mjSENS_FRAMEANGVEL = 31, - mjSENS_FRAMELINACC = 32, - mjSENS_FRAMEANGACC = 33, - mjSENS_SUBTREECOM = 34, - mjSENS_SUBTREELINVEL = 35, - mjSENS_SUBTREEANGMOM = 36, - mjSENS_GEOMDIST = 37, - mjSENS_GEOMNORMAL = 38, - mjSENS_GEOMFROMTO = 39, - mjSENS_E_POTENTIAL = 40, - mjSENS_E_KINETIC = 41, - mjSENS_CLOCK = 42, - mjSENS_PLUGIN = 43, - mjSENS_USER = 44, + mjSENS_TENDONACTFRC = 17, + mjSENS_BALLQUAT = 18, + mjSENS_BALLANGVEL = 19, + mjSENS_JOINTLIMITPOS = 20, + mjSENS_JOINTLIMITVEL = 21, + mjSENS_JOINTLIMITFRC = 22, + mjSENS_TENDONLIMITPOS = 23, + mjSENS_TENDONLIMITVEL = 24, + mjSENS_TENDONLIMITFRC = 25, + mjSENS_FRAMEPOS = 26, + mjSENS_FRAMEQUAT = 27, + mjSENS_FRAMEXAXIS = 28, + mjSENS_FRAMEYAXIS = 29, + mjSENS_FRAMEZAXIS = 30, + mjSENS_FRAMELINVEL = 31, + mjSENS_FRAMEANGVEL = 32, + mjSENS_FRAMELINACC = 33, + mjSENS_FRAMEANGACC = 34, + mjSENS_SUBTREECOM = 35, + mjSENS_SUBTREELINVEL = 36, + mjSENS_SUBTREEANGMOM = 37, + mjSENS_GEOMDIST = 38, + mjSENS_GEOMNORMAL = 39, + mjSENS_GEOMFROMTO = 40, + mjSENS_E_POTENTIAL = 41, + mjSENS_E_KINETIC = 42, + mjSENS_CLOCK = 43, + mjSENS_PLUGIN = 44, + mjSENS_USER = 45, } public enum mjtStage : int{ mjSTAGE_NONE = 0, @@ -5569,12 +5570,14 @@ public unsafe struct mjModel_ { public int* tendon_matid; public int* tendon_group; public byte* tendon_limited; + public byte* tendon_actfrclimited; public double* tendon_width; public double* tendon_solref_lim; public double* tendon_solimp_lim; public double* tendon_solref_fri; public double* tendon_solimp_fri; public double* tendon_range; + public double* tendon_actfrcrange; public double* tendon_margin; public double* tendon_stiffness; public double* tendon_damping; @@ -6398,8 +6401,10 @@ public unsafe struct model { public int* tendon_matid; public int* tendon_group; public byte* tendon_limited; + public byte* tendon_actfrclimited; public double* tendon_width; public double* tendon_range; + public double* tendon_actfrcrange; public double* tendon_stiffness; public double* tendon_damping; public double* tendon_frictionloss; From fc516ee3c1eacbb43cf82909434d7e07ae6ec6b7 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 8 Apr 2025 14:06:17 -0400 Subject: [PATCH 047/191] revert import order --- python/mujoco/viewer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/mujoco/viewer.py b/python/mujoco/viewer.py index e78e1229..234bf784 100644 --- a/python/mujoco/viewer.py +++ b/python/mujoco/viewer.py @@ -27,10 +27,9 @@ from typing import Callable, List, Optional, Tuple, Union import weakref import glfw -import numpy as np - import mujoco from mujoco import _simulate +import numpy as np if not glfw._glfw: # pylint: disable=protected-access raise RuntimeError('GLFW dynamic library handle is not available') From 55e3ca3acfbb787e8c697f88a2813791ba1ba178 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 8 Apr 2025 11:21:15 -0700 Subject: [PATCH 048/191] Improvements to `minimize.least_squares`. All changes make functionality more similar to SciPy least squares: - Use adaptive findiff epsilon. - Make termination on step size relative to norm(x). - Add termination on gradient norm. - Make default tolerances like SciPy's. PiperOrigin-RevId: 745221614 Change-Id: Iee93256651fca8154c97fa3bdaa9c67ede28e573 --- python/mujoco/minimize.py | 51 ++++++++++++++++++++++++++-------- python/mujoco/minimize_test.py | 9 +++--- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/python/mujoco/minimize.py b/python/mujoco/minimize.py index 449d4bb8..665c4fd9 100644 --- a/python/mujoco/minimize.py +++ b/python/mujoco/minimize.py @@ -36,6 +36,7 @@ class Status(enum.Enum): NO_IMPROVEMENT = enum.auto() MAX_ITER = enum.auto() DX_TOL = enum.auto() + G_TOL = enum.auto() _STATUS_MESSAGE = { @@ -43,6 +44,7 @@ _STATUS_MESSAGE = { Status.NO_IMPROVEMENT: 'insufficient reduction.', Status.MAX_ITER: 'maximum iterations reached.', Status.DX_TOL: 'norm(dx) < tol.', + Status.G_TOL: 'norm(gradient) < tol.', } @@ -57,6 +59,7 @@ class IterLog: regularizer: Value of the regularizer used for this iteration. residual: Optional value of the residual at the candidate. jacobian: Optional value of the Jacobian at the candidate. + grad: Optional value of the gradient at the candidate. step: Optional change in decision variable during this iteration. """ @@ -66,6 +69,7 @@ class IterLog: regularizer: np.float64 residual: Optional[np.ndarray] = None jacobian: Optional[np.ndarray] = None + grad: Optional[np.ndarray] = None step: Optional[np.ndarray] = None @@ -141,11 +145,12 @@ def least_squares( bounds: Optional[Sequence[np.ndarray]] = None, jacobian: Optional[Callable[[np.ndarray, np.ndarray], np.ndarray]] = None, norm: Norm = Quadratic(), - eps: float = 1e-6, + eps: float = np.finfo(np.float64).eps ** 0.5, mu_min: float = 1e-6, mu_max: float = 1e8, - mu_factor: float = 10.0**0.1, - tol: float = 1e-6, + mu_factor: float = 10.0 ** 0.1, + xtol: float = 1e-8, + gtol: float = 1e-8, max_iter: int = 100, verbose: Union[Verbosity, int] = Verbosity.ITER, output: Optional[TextIO] = None, @@ -166,7 +171,8 @@ def least_squares( mu_min: Minimum value of the regularizer. mu_max: Maximum value of the regularizer. mu_factor: Factor for increasing or decreasing the regularizer. - tol: Termination tolerance on the step size. + xtol: Termination tolerance on relative step size. + gtol: Termination tolerance on gradient norm. max_iter: Maximum number of iterations. verbose: Verbosity level. output: Optional file or StringIO to which to print messages. @@ -281,6 +287,23 @@ def least_squares( # Get gradient, Gauss-Newton Hessian. grad, hess = norm.grad_hess(r, jac) + # Get free (unclamped) gradient. + if bounds is None: + grad_free = grad + else: + clamped_lower = (x == bounds[0]) & (grad > 0) + clamped_upper = (x == bounds[1]) & (grad < 0) + clamped = clamped_lower | clamped_upper + grad_free = grad[~clamped] + + # Check termination condition on gradient norm. + g_norm = np.linalg.norm(grad_free) + if g_norm <= gtol: + status = Status.G_TOL + if g_norm == 0: + print('Zero gradient norm: exact minimum found?', file=output) + break + # Bounds relative to x dlower = None if bounds is None else bounds[0] - x dupper = None if bounds is None else bounds[1] - x @@ -353,13 +376,15 @@ def least_squares( # Append log to trace, call iter_callback. log = IterLog(candidate=x, objective=y, reduction=reduction, regularizer=mu) if verbose >= Verbosity.FULLITER.value: - log = dataclasses.replace(log, residual=r, jacobian=jac, step=dx) + log = dataclasses.replace( + log, residual=r, jacobian=jac, grad=grad, step=dx + ) trace.append(log) if iter_callback is not None: iter_callback(trace) - # Check for success. - if dx_norm < tol: + # Check termination condition on step norm. + if dx_norm < xtol * (xtol + np.linalg.norm(x)): status = Status.DX_TOL break @@ -376,7 +401,7 @@ def least_squares( # Append final log to trace, call iter_callback. # Note: unlike other iter logs, values are computed at the end point. yfinal = norm.value(r) - red = np.float64(0.0) # No reduction sice we didn't take a step. + red = np.float64(0.0) # No reduction since we didn't take a step. log = IterLog(candidate=x, objective=yfinal, reduction=red, regularizer=mu) trace.append(log) if iter_callback is not None: @@ -430,13 +455,15 @@ def jacobian_fd( """ n = x.size if bounds is None: - eps_vec = eps * np.ones(n) + eps_vec = eps * np.ones((n, 1)) else: mid = 0.5 * (bounds[1] - bounds[0]) - eps_vec = np.where(x > mid, -eps, eps).flatten() - xh = x + np.diag(eps_vec) + eps_vec = np.where(x > mid, -eps, eps) + eps_vec *= np.maximum(1.0, np.abs(x)) + eps_vec = (eps_vec + x) - x + xh = x + np.diag(eps_vec.flatten()) rh = residual(xh) - jac = (rh - r) / eps_vec + jac = (rh - r) / eps_vec.T return jac, n_res + n diff --git a/python/mujoco/minimize_test.py b/python/mujoco/minimize_test.py index e642a8e1..5ba90b2f 100644 --- a/python/mujoco/minimize_test.py +++ b/python/mujoco/minimize_test.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== -"""Tests for minimize.py.""" import io @@ -32,7 +31,7 @@ class MinimizeTest(absltest.TestCase): x, _ = minimize.least_squares(x0, residual, output=out) expected_x = np.array((1.0, 1.0)) np.testing.assert_array_almost_equal(x, expected_x) - self.assertIn('norm(dx) < tol', out.getvalue()) + self.assertIn('norm(gradient) < tol', out.getvalue()) def test_start_at_minimum(self) -> None: def residual(x): @@ -43,7 +42,7 @@ class MinimizeTest(absltest.TestCase): x, _ = minimize.least_squares(x0, residual, output=out) expected_x = np.array((1.0, 1.0)) np.testing.assert_array_almost_equal(x, expected_x) - self.assertIn('norm(dx) < tol', out.getvalue()) + self.assertIn('norm(gradient) < tol', out.getvalue()) self.assertIn('exact minimum found', out.getvalue()) def test_jac_callback(self) -> None: @@ -61,7 +60,7 @@ class MinimizeTest(absltest.TestCase): ) expected_x = np.array((1.0, 1.0)) np.testing.assert_array_almost_equal(x, expected_x) - self.assertIn('norm(dx) < tol', out.getvalue()) + self.assertIn('norm(gradient) < tol', out.getvalue()) self.assertIn('Jacobian matches', out.getvalue()) # Try with bad Jacobian, ask least_squares to check it. @@ -116,7 +115,7 @@ class MinimizeTest(absltest.TestCase): x0, residual, bounds=bounds_types['inbounds'], output=out ) np.testing.assert_array_almost_equal(x, expected_x) - self.assertIn('norm(dx) < tol', out.getvalue()) + self.assertIn('norm(gradient) < tol', out.getvalue()) # Test different bounds conditions. for bounds in bounds_types.values(): From 5031f881589cc3d6d471234c8887c83e87e40321 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 8 Apr 2025 14:54:51 -0700 Subject: [PATCH 049/191] In `minimize.least_squares`, compute values at the solution at full verbosity. PiperOrigin-RevId: 745301888 Change-Id: Ibd8e35e405bb2cebc7ac59a6dfc77118a6e6ba9f --- python/mujoco/minimize.py | 17 +++++++++++++++++ python/mujoco/minimize_test.py | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/python/mujoco/minimize.py b/python/mujoco/minimize.py index 665c4fd9..b06f3eb1 100644 --- a/python/mujoco/minimize.py +++ b/python/mujoco/minimize.py @@ -403,6 +403,23 @@ def least_squares( yfinal = norm.value(r) red = np.float64(0.0) # No reduction since we didn't take a step. log = IterLog(candidate=x, objective=yfinal, reduction=red, regularizer=mu) + + # If full verbosity requested, compute values at the final point. + if verbose >= Verbosity.FULLITER.value: + # Get Jacobian jac. + t_start = time.time() + if jacobian is None: + jac, n_res = jacobian_fd(residual, x, r, eps, n_res, bounds) + t_res += time.time() - t_start + else: + jac = jacobian(x, r) + t_jac += time.time() - t_start + n_jac += 1 + + # Get gradient, add to log. + grad, _ = norm.grad_hess(r, jac) + log = dataclasses.replace(log, residual=r, jacobian=jac, grad=grad) + trace.append(log) if iter_callback is not None: iter_callback(trace) diff --git a/python/mujoco/minimize_test.py b/python/mujoco/minimize_test.py index 5ba90b2f..1792643d 100644 --- a/python/mujoco/minimize_test.py +++ b/python/mujoco/minimize_test.py @@ -127,8 +127,8 @@ class MinimizeTest(absltest.TestCase): output=out, verbose=minimize.Verbosity.FULLITER, ) - self.assertIn(' < tol', out.getvalue()) - grad = trace[-2].jacobian.T @ trace[-2].residual + self.assertIn('norm(gradient) < tol', out.getvalue()) + grad = trace[-1].grad # If x_i is on the boundary, gradient points out, otherwise it is 0. for i, xi in enumerate(x): if xi == bounds[0][i]: From 8fc616bf8fb00e4be99107d59e09e32b53395db5 Mon Sep 17 00:00:00 2001 From: Taylor Howell Date: Wed, 9 Apr 2025 04:27:48 -0700 Subject: [PATCH 050/191] Add tendon actuator force limits to MJX. PiperOrigin-RevId: 745529685 Change-Id: I43c372116daefc3dd2ed6ad1bb9c6e3978bc8527 --- doc/changelog.rst | 4 ++ mjx/mujoco/mjx/_src/forward.py | 29 +++++++++++ mjx/mujoco/mjx/_src/forward_test.py | 16 ++++++ .../test_data/actuator/tendon_force_clamp.xml | 49 +++++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 mjx/mujoco/mjx/test_data/actuator/tendon_force_clamp.xml diff --git a/doc/changelog.rst b/doc/changelog.rst index deaba59e..f2590378 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -23,6 +23,10 @@ General - Added :ref:`tendon actuator force limits` and :ref:`tendon actuator force sensor`. +MJX +^^^ +- Added tendon actuator force limits. + Bug fixes ^^^^^^^^^ - :ref:`mj_jacDot` was missing a term that accounts for the motion of the point with respect to diff --git a/mjx/mujoco/mjx/_src/forward.py b/mjx/mujoco/mjx/_src/forward.py index 71a6e3f5..6ad1ae71 100644 --- a/mjx/mujoco/mjx/_src/forward.py +++ b/mjx/mujoco/mjx/_src/forward.py @@ -38,6 +38,7 @@ from mujoco.mjx._src.types import GainType from mujoco.mjx._src.types import IntegratorType from mujoco.mjx._src.types import JointType from mujoco.mjx._src.types import Model +from mujoco.mjx._src.types import TrnType # pylint: enable=g-importing-member import numpy as np @@ -178,6 +179,34 @@ def fwd_actuation(m: Model, d: Data) -> Data: jp.array(m.actuator_acc0), group_by='u', ) + + # tendon total force clamping + if np.any(m.tendon_actfrclimited): + (tendon_actfrclimited_id,) = np.nonzero(m.tendon_actfrclimited) + actuator_tendon = m.actuator_trntype == TrnType.TENDON + + force_mask = [ + actuator_tendon & (m.actuator_trnid[:, 0] == tendon_id) + for tendon_id in tendon_actfrclimited_id + ] + force_ids = np.concatenate([np.nonzero(mask)[0] for mask in force_mask]) + force_mat = np.array(force_mask)[:, force_ids] + tendon_total_force = force_mat @ force[force_ids] + + force_scaling = jp.where( + tendon_total_force < m.tendon_actfrcrange[tendon_actfrclimited_id, 0], + m.tendon_actfrcrange[tendon_actfrclimited_id, 0] / tendon_total_force, + 1, + ) + force_scaling = jp.where( + tendon_total_force > m.tendon_actfrcrange[tendon_actfrclimited_id, 1], + m.tendon_actfrcrange[tendon_actfrclimited_id, 1] / tendon_total_force, + force_scaling, + ) + + tendon_forces = force[force_ids] * (force_mat.T @ force_scaling) + force = force.at[force_ids].set(tendon_forces) + forcerange = jp.where( m.actuator_forcelimited[:, None], m.actuator_forcerange, diff --git a/mjx/mujoco/mjx/_src/forward_test.py b/mjx/mujoco/mjx/_src/forward_test.py index c6466449..efdbcd65 100644 --- a/mjx/mujoco/mjx/_src/forward_test.py +++ b/mjx/mujoco/mjx/_src/forward_test.py @@ -17,6 +17,7 @@ from absl.testing import absltest from absl.testing import parameterized import jax +from jax import numpy as jp import mujoco from mujoco import mjx from mujoco.mjx._src import test_util @@ -196,6 +197,21 @@ class ActuatorTest(parameterized.TestCase): dx = jax.jit(mjx.euler)(mx, dx) _assert_attr_eq(d, dx, 'act') + def test_tendon_force_clamp(self): + m = test_util.load_test_file('actuator/tendon_force_clamp.xml') + d = mujoco.MjData(m) + mx = mjx.put_model(m) + dx = mjx.put_data(m, d) + + dx = dx.replace(ctrl=jp.array([1.0, 1.0, 1.0, -1.0, 1.0, -20.0, 5.0, -5.0])) + dx = mjx.forward(mx, dx) + + _assert_eq( + dx.actuator_force, + jp.array([1.0, 1.0, 1.0, -1.0, 1.0, -10.0, 5.0, -5.0]), + 'actuator_force', + ) + if __name__ == '__main__': absltest.main() diff --git a/mjx/mujoco/mjx/test_data/actuator/tendon_force_clamp.xml b/mjx/mujoco/mjx/test_data/actuator/tendon_force_clamp.xml new file mode 100644 index 00000000..43d4c69b --- /dev/null +++ b/mjx/mujoco/mjx/test_data/actuator/tendon_force_clamp.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 85b323c7fb92338c094e1b601b7ebfa7992403cc Mon Sep 17 00:00:00 2001 From: Kyle Bayes Date: Wed, 9 Apr 2025 06:17:58 -0700 Subject: [PATCH 051/191] Remove square roots when possible in EPA improving numerical precision. PiperOrigin-RevId: 745559836 Change-Id: I3f8b23406e93ccfc4bc3499bfbed829b35462a74 --- src/engine/engine_collision_gjk.c | 148 ++++++++++++++--------- test/engine/engine_collision_gjk_test.cc | 64 ++++++++++ 2 files changed, 153 insertions(+), 59 deletions(-) diff --git a/src/engine/engine_collision_gjk.c b/src/engine/engine_collision_gjk.c index e1ac8739..cb58530d 100644 --- a/src/engine/engine_collision_gjk.c +++ b/src/engine/engine_collision_gjk.c @@ -24,6 +24,9 @@ #include "engine/engine_util_blas.h" #include "engine/engine_util_errmem.h" +#define mjMINVAL2 (mjMINVAL * mjMINVAL) +#define mjMAXVAL2 (mjMAXVAL * mjMAXVAL) + // subdistance algorithm for GJK that computes the barycentric coordinates of the point in a // simplex closest to the origin // implementation adapted from Montanari et al, ToG 2017 @@ -50,7 +53,7 @@ typedef struct { int verts[3]; // indices of the three vertices of the face in the polytope int adj[3]; // adjacent faces, one for each edge: [v1,v2], [v2,v3], [v3,v1] mjtNum v[3]; // projection of the origin on face, can be used as face normal - mjtNum dist; // norm of v; negative if deleted + mjtNum dist2; // squared norm of v; negative if deleted int index; // index in map; -1: not in map, -2: deleted from polytope } Face; @@ -78,7 +81,7 @@ static int epaSupport(Polytope* pt, mjCCDObj* obj1, mjCCDObj* obj2, // make copy of vertex in polytope and return its index static int insertVertex(Polytope* pt, const Vertex* v); -// attach a face to the polytope with the given vertex indices; return distance to origin +// attach a face to the polytope with the given vertex indices; return squared distance to origin static mjtNum attachFace(Polytope* pt, int v1, int v2, int v3, int adj1, int adj2, int adj3); // return 1 if objects are in contact; 0 if not; -1 if inconclusive @@ -318,7 +321,7 @@ static void gjkSupport(Vertex* v, mjCCDObj* obj1, mjCCDObj* obj2, // mjc_support requires a normalized direction mjtNum norm = dot3(x_k, x_k); - if (norm > mjMINVAL*mjMINVAL) { + if (norm > mjMINVAL2) { norm = 1/mju_sqrt(norm); scl3(dir_neg, x_k, norm); scl3(dir, dir_neg, -1); @@ -392,10 +395,9 @@ static inline mjtNum signedDistance(mjtNum normal[3], const Vertex* v1, const Ve sub3(diff1, v3->vert, v1->vert); sub3(diff2, v2->vert, v1->vert); cross3(normal, diff1, diff2); - mjtNum norm = dot3(normal, normal); - if (norm > mjMINVAL*mjMINVAL && norm < mjMAXVAL*mjMAXVAL) { - norm = 1/mju_sqrt(norm); - scl3(normal, normal, norm); + mjtNum norm2 = dot3(normal, normal); + if (norm2 > mjMINVAL2 && norm2 < mjMAXVAL2) { + scl3(normal, normal, 1 / mju_sqrt(norm2)); return dot3(normal, v1->vert); } return mjMAX_LIMIT; // cannot recover normal (ignore face) @@ -964,27 +966,27 @@ static int polytope2(Polytope* pt, mjCCDStatus* status, mjCCDObj* obj1, mjCCDObj mjtNum* v5 = pt->verts[v5i].vert; // build hexahedron - if (attachFace(pt, v1i, v3i, v4i, 1, 3, 2) < mjMINVAL) { + if (attachFace(pt, v1i, v3i, v4i, 1, 3, 2) < mjMINVAL2) { replaceSimplex3(pt, status, v1i, v3i, v4i); return polytope3(pt, status, obj1, obj2); } - if (attachFace(pt, v1i, v5i, v3i, 2, 4, 0) < mjMINVAL) { + if (attachFace(pt, v1i, v5i, v3i, 2, 4, 0) < mjMINVAL2) { replaceSimplex3(pt, status, v1i, v5i, v3i); return polytope3(pt, status, obj1, obj2); } - if (attachFace(pt, v1i, v4i, v5i, 0, 5, 1) < mjMINVAL) { + if (attachFace(pt, v1i, v4i, v5i, 0, 5, 1) < mjMINVAL2) { replaceSimplex3(pt, status, v1i, v4i, v5i); return polytope3(pt, status, obj1, obj2); } - if (attachFace(pt, v2i, v4i, v3i, 5, 0, 4) < mjMINVAL) { + if (attachFace(pt, v2i, v4i, v3i, 5, 0, 4) < mjMINVAL2) { replaceSimplex3(pt, status, v2i, v4i, v3i); return polytope3(pt, status, obj1, obj2); } - if (attachFace(pt, v2i, v3i, v5i, 3, 1, 5) < mjMINVAL) { + if (attachFace(pt, v2i, v3i, v5i, 3, 1, 5) < mjMINVAL2) { replaceSimplex3(pt, status, v2i, v3i, v5i); return polytope3(pt, status, obj1, obj2); } - if (attachFace(pt, v2i, v5i, v4i, 4, 2, 3) < mjMINVAL) { + if (attachFace(pt, v2i, v5i, v4i, 4, 2, 3) < mjMINVAL2) { replaceSimplex3(pt, status, v2i, v5i, v4i); return polytope3(pt, status, obj1, obj2); } @@ -1120,22 +1122,22 @@ static int polytope3(Polytope* pt, mjCCDStatus* status, mjCCDObj* obj1, mjCCDObj } // create hexahedron for EPA - if (attachFace(pt, v4i, v1i, v2i, 1, 3, 2) < mjMINVAL) { + if (attachFace(pt, v4i, v1i, v2i, 1, 3, 2) < mjMINVAL2) { return mjEPA_P3_ORIGIN_ON_FACE; } - if (attachFace(pt, v4i, v3i, v1i, 2, 4, 0) < mjMINVAL) { + if (attachFace(pt, v4i, v3i, v1i, 2, 4, 0) < mjMINVAL2) { return mjEPA_P3_ORIGIN_ON_FACE; } - if (attachFace(pt, v4i, v2i, v3i, 0, 5, 1) < mjMINVAL) { + if (attachFace(pt, v4i, v2i, v3i, 0, 5, 1) < mjMINVAL2) { return mjEPA_P3_ORIGIN_ON_FACE; } - if (attachFace(pt, v5i, v2i, v1i, 5, 0, 4) < mjMINVAL) { + if (attachFace(pt, v5i, v2i, v1i, 5, 0, 4) < mjMINVAL2) { return mjEPA_P3_ORIGIN_ON_FACE; } - if (attachFace(pt, v5i, v1i, v3i, 3, 1, 5) < mjMINVAL) { + if (attachFace(pt, v5i, v1i, v3i, 3, 1, 5) < mjMINVAL2) { return mjEPA_P3_ORIGIN_ON_FACE; } - if (attachFace(pt, v5i, v3i, v2i, 4, 2, 3) < mjMINVAL) { + if (attachFace(pt, v5i, v3i, v2i, 4, 2, 3) < mjMINVAL2) { return mjEPA_P3_ORIGIN_ON_FACE; } @@ -1160,19 +1162,19 @@ static int polytope4(Polytope* pt, mjCCDStatus* status, mjCCDObj* obj1, mjCCDObj int v4 = insertVertex(pt, status->simplex + 3); // if the origin is on a face, replace the 3-simplex with a 2-simplex - if (attachFace(pt, v1, v2, v3, 1, 3, 2) < mjMINVAL) { + if (attachFace(pt, v1, v2, v3, 1, 3, 2) < mjMINVAL2) { replaceSimplex3(pt, status, v1, v2, v3); return polytope3(pt, status, obj1, obj2); } - if (attachFace(pt, v1, v4, v2, 2, 3, 0) < mjMINVAL) { + if (attachFace(pt, v1, v4, v2, 2, 3, 0) < mjMINVAL2) { replaceSimplex3(pt, status, v1, v4, v2); return polytope3(pt, status, obj1, obj2); } - if (attachFace(pt, v1, v3, v4, 0, 3, 1) < mjMINVAL) { + if (attachFace(pt, v1, v3, v4, 0, 3, 1) < mjMINVAL2) { replaceSimplex3(pt, status, v1, v3, v4); return polytope3(pt, status, obj1, obj2); } - if (attachFace(pt, v4, v3, v2, 2, 0, 1) < mjMINVAL) { + if (attachFace(pt, v4, v3, v2, 2, 0, 1) < mjMINVAL2) { replaceSimplex3(pt, status, v4, v3, v2); return polytope3(pt, status, obj1, obj2); } @@ -1222,7 +1224,7 @@ static inline int maxFaces(Polytope* pt) { -// attach a face to the polytope with the given vertex indices; return distance to origin +// attach a face to the polytope with the given vertex indices; return squared distance to origin static inline mjtNum attachFace(Polytope* pt, int v1, int v2, int v3, int adj1, int adj2, int adj3) { Face* face = &pt->faces[pt->nfaces++]; @@ -1240,10 +1242,10 @@ static inline mjtNum attachFace(Polytope* pt, int v1, int v2, int v3, if (ret) { return 0; } - face->dist = norm3(face->v); + face->dist2 = dot3(face->v, face->v); face->index = -1; - return face->dist; + return face->dist2; } @@ -1267,10 +1269,8 @@ static inline int getEdge(Face* face, int vertex) { // recursive call to build horizon; return 1 if face is visible from w otherwise 0 static int horizonRec(Polytope* pt, Face* face, int e) { - mjtNum dist2 = face->dist * face->dist; - // v is visible from w so it is deleted and adjacent faces are checked - if (dot3(face->v, pt->horizon.w) > dist2) { + if (dot3(face->v, pt->horizon.w) - face->dist2 > mjMINVAL) { deleteFace(pt, face); // recursively search the adjacent faces on the next two edges @@ -1350,7 +1350,7 @@ static void epaWitness(const Polytope* pt, const Face* face, mjtNum x1[3], mjtNu // return a face of the expanded polytope that best approximates the pentration depth // witness points are in status->{x1, x2} static Face* epa(mjCCDStatus* status, Polytope* pt, mjCCDObj* obj1, mjCCDObj* obj2) { - mjtNum tolerance = status->tolerance, lower, upper = mjMAX_LIMIT; + mjtNum tolerance = status->tolerance, lower2, upper = mjMAX_LIMIT, upper2 = mjMAX_LIMIT; int k, kmax = status->max_iterations; Face* face = NULL, *pface = NULL; // face closest to origin @@ -1358,31 +1358,35 @@ static Face* epa(mjCCDStatus* status, Polytope* pt, mjCCDObj* obj1, mjCCDObj* ob pface = face; // find the face closest to the origin (lower bound for penetration depth) - lower = mjMAX_LIMIT; + lower2 = mjMAX_LIMIT; for (int i = 0; i < pt->nmap; i++) { - if (pt->map[i]->dist < lower) { + if (pt->map[i]->dist2 < lower2) { face = pt->map[i]; - lower = face->dist; + lower2 = face->dist2; } } // face not valid, return previous face - if (lower > upper || !face) { + if (lower2 > upper2 || !face) { face = pface; break; } // check if lower bound is 0 - if (lower <= 0) { + if (lower2 <= 0) { mju_warning("EPA: origin lies on affine hull of face"); break; } // compute support point w from the closest face's normal + mjtNum lower = mju_sqrt(lower2); int wi = epaSupport(pt, obj1, obj2, face->v, lower); mjtNum* w = pt->verts[wi].vert; mjtNum upper_k = dot3(face->v, w) / lower; // upper bound for kth iteration - if (upper_k < upper) upper = upper_k; + if (upper_k < upper) { + upper = upper_k; + upper2 = upper * upper; + } if (upper - lower < tolerance) { break; } @@ -1411,16 +1415,16 @@ static Face* epa(mjCCDStatus* status, Polytope* pt, mjCCDObj* obj1, mjCCDObj* ob int v1 = horFace->verts[horEdge], v2 = horFace->verts[(horEdge + 1) % 3]; horFace->adj[horEdge] = nfaces; - mjtNum dist = attachFace(pt, wi, v2, v1, nfaces + nedges - 1, horIndex, nfaces + 1); + mjtNum dist2 = attachFace(pt, wi, v2, v1, nfaces + nedges - 1, horIndex, nfaces + 1); // unrecoverable numerical issue - if (dist == 0) { + if (dist2 == 0) { face = NULL; break; } // store face in map - if (dist >= lower && dist <= upper) { + if (dist2 >= lower2 && dist2 <= upper2) { int i = pt->nmap++; pt->map[i] = &pt->faces[pt->nfaces - 1]; pt->map[i]->index = i; @@ -1436,16 +1440,16 @@ static Face* epa(mjCCDStatus* status, Polytope* pt, mjCCDObj* obj1, mjCCDObj* ob v1 = horFace->verts[horEdge]; v2 = horFace->verts[(horEdge + 1) % 3]; horFace->adj[horEdge] = cur; - dist = attachFace(pt, wi, v2, v1, cur - 1, horIndex, next); + dist2 = attachFace(pt, wi, v2, v1, cur - 1, horIndex, next); // unrecoverable numerical issue - if (dist == 0) { + if (dist2 == 0) { face = NULL; break; } // store face in map - if (dist >= lower && dist <= upper) { + if (dist2 >= lower2 && dist2 <= upper2) { int idx = pt->nmap++; pt->map[idx] = &pt->faces[pt->nfaces - 1]; pt->map[idx]->index = idx; @@ -1463,7 +1467,7 @@ static Face* epa(mjCCDStatus* status, Polytope* pt, mjCCDObj* obj1, mjCCDObj* ob if (face) { epaWitness(pt, face, status->x1, status->x2); status->nx = 1; - status->dist = -face->dist; + status->dist = -mju_sqrt(face->dist2); } else { status->nx = 0; status->dist = 0; @@ -1565,7 +1569,7 @@ static mjtNum planeNormal(mjtNum res[3], const mjtNum v1[3], const mjtNum v2[3], // find what side of a plane a point p lies static int halfspace(const mjtNum a[3], const mjtNum n[3], const mjtNum p[3]) { mjtNum diff[3] = {p[0] - a[0], p[1] - a[1], p[2] - a[2]}; - return dot3(diff, n) >= 0.0; + return dot3(diff, n) > -mjMINVAL; } @@ -1855,13 +1859,37 @@ static int meshEdgeNormals(mjtNum* res, mjtNum* endverts, int dim, mjCCDObj* obj +// try recovering box normal from collision normal +static int boxNormals2(mjtNum res[9], int resind[3], const mjtNum mat[9], const mjtNum n[3]) { + // list of box face normals + mjtNum normals[18] = {1, 0, 0, -1, 0, 0, + 0, 1, 0, 0, -1, 0, + 0, 0, 1, 0, 0, -1}; + + // get local coordinates of the normal + mjtNum local_n[3]; + local_n[0] = mat[0]*n[0] + mat[3]*n[1] + mat[6]*n[2]; + local_n[1] = mat[1]*n[0] + mat[4]*n[1] + mat[7]*n[2]; + local_n[2] = mat[2]*n[0] + mat[5]*n[1] + mat[8]*n[2]; + scl3(local_n, local_n, 1/mju_sqrt(dot3(local_n, local_n))); + + // determine if there is a side close to the normal + for (int i = 0; i < 6; i++) { + if (dot3(local_n, normals + 3*i) > mjFACE_TOL) { + globalcoord(res, mat, NULL, normals[3*i], normals[3*i + 1], normals[3*i + 2]); + resind[0] = i; + return 1; + } + } + return 0; +} + + + // compute possible face normals of a box given up to 3 vertices static int boxNormals(mjtNum res[9], int resind[3], int dim, mjCCDObj* obj, - int v1, int v2, int v3) { - // box data - int g = 3*obj->geom; - const mjtNum* mat = obj->data->geom_xmat + 3*g; - + int v1, int v2, int v3, const mjtNum dir[3]) { + const mjtNum* mat = obj->data->geom_xmat + 9*obj->geom; if (dim == 3) { int c = 0; int x = ((v1 & 1) && (v2 & 1) && (v3 & 1)) - (!(v1 & 1) && !(v2 & 1) && !(v3 & 1)); @@ -1873,7 +1901,7 @@ static int boxNormals(mjtNum res[9], int resind[3], int dim, mjCCDObj* obj, if (y) resind[c++] = 2; if (z) resind[c++] = 4; if (sgn == -1) resind[0]++; - return c == 1 ? 1 : 0; // return 1 only if vertices make a valid face + return c == 1 ? 1 : boxNormals2(res, resind, mat, dir); } if (dim == 2) { @@ -1893,8 +1921,7 @@ static int boxNormals(mjtNum res[9], int resind[3], int dim, mjCCDObj* obj, globalcoord(res + 3, mat, NULL, 0, 0, z); resind[c++] = (z > 0) ? 4 : 5; } - // TODO(kylebayes): Should be able to recover multiple contacts here. - return c == 2 ? 2 : 0; + return c == 2 ? 2 : boxNormals2(res, resind, mat, dir); } if (dim == 1) { @@ -2107,14 +2134,18 @@ static void multicontact(Polytope* pt, Face* face, mjCCDStatus* status, mjtNum n1[3 * mjMAX_POLYVERT], n2[3 * mjMAX_POLYVERT]; // normals of possible face collisions int idx1[mjMAX_POLYVERT], idx2[mjMAX_POLYVERT]; // indices of faces + mjtNum dir[3], dir_neg[3]; + sub3(dir, status->x2, status->x1); + sub3(dir_neg, status->x1, status->x2); + // get all possible face normals for each geom if (obj1->geom_type == mjGEOM_BOX) { - nnorms1 = boxNormals(n1, idx1, nface1, obj1, v11i, v12i, v13i); + nnorms1 = boxNormals(n1, idx1, nface1, obj1, v11i, v12i, v13i, dir_neg); } else if (obj1->geom_type == mjGEOM_MESH) { nnorms1 = meshNormals(n1, idx1, nface1, obj1, v11i, v12i, v13i); } if (obj2->geom_type == mjGEOM_BOX) { - nnorms2 = boxNormals(n2, idx2, nface2, obj2, v21i, v22i, v23i); + nnorms2 = boxNormals(n2, idx2, nface2, obj2, v21i, v22i, v23i, dir); } else if (obj2->geom_type == mjGEOM_MESH) { nnorms2 = meshNormals(n2, idx2, nface2, obj2, v21i, v22i, v23i); } @@ -2181,25 +2212,24 @@ static void multicontact(Polytope* pt, Face* face, mjCCDStatus* status, // TODO(kylebayes): this approximates the contact direction, by scaling the face normal by the // single contact direction's magnitude. This is effective, but polygonClip should compute // this for each contact point. - mjtNum diff[3], approx_dir[3]; - sub3(diff, status->x2, status->x1); + mjtNum approx_dir[3]; // face1 is an edge; clip face1 against face2 if (edgecon1) { - scl3(approx_dir, n2 + 3*j, norm3(diff)); + scl3(approx_dir, n2 + 3*j, norm3(dir)); polygonClip(status, face2, nface2, face1, nface1, n2 + 3*j, approx_dir); return; } // face2 is an edge; clip face2 against face1 if (edgecon2) { - scl3(approx_dir, n1 + 3*j, -norm3(diff)); + scl3(approx_dir, n1 + 3*j, -norm3(dir)); polygonClip(status, face1, nface1, face2, nface2, n1 + 3*j, approx_dir); return; } // face-face collision - scl3(approx_dir, n2 + 3*j, norm3(diff)); + scl3(approx_dir, n2 + 3*j, norm3(dir)); polygonClip(status, face1, nface1, face2, nface2, n1 + 3*i, approx_dir); } diff --git a/test/engine/engine_collision_gjk_test.cc b/test/engine/engine_collision_gjk_test.cc index f4fcb141..934a034e 100644 --- a/test/engine/engine_collision_gjk_test.cc +++ b/test/engine/engine_collision_gjk_test.cc @@ -1088,6 +1088,70 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD11) { mj_deleteModel(model); } +TEST_F(MjGjkTest, BoxBoxMultiCCD12) { + static constexpr char xml[] = R"( + + + + + + )"; + + std::array error; + mjModel* model = LoadModelFromString(xml, error.data(), error.size()); + ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error.data(); + + mjData* data = mj_makeData(model); + mj_forward(model, data); + + mjtNum* xpos = data->geom_xpos; + mjtNum* xmat = data->geom_xmat; + + xmat[0] = 1.0000000000000000000000000000000000000000; + xmat[1] = 0.0000000000000000000000000000000000000000; + xmat[2] = 0.0000000000000000000000000000000000000000; + xmat[3] = 0.0000000000000000000000000000000000000000; + xmat[4] = 1.0000000000000000000000000000000000000000; + xmat[5] = -0.0000000000000000032154383478277941584027; + xmat[6] = 0.0000000000000000000000000000000000000000; + xmat[7] = 0.0000000000000000032154383478277941584027; + xmat[8] = 1.0000000000000000000000000000000000000000; + + xpos[0] = 0.0164299999999999862820843077315657865256; + xpos[1] = -0.0764300000000000256950016819246229715645; + xpos[2] = 0.1252706891962387103500731200256268493831; + + xpos = data->geom_xpos + 3; + xmat = data->geom_xmat + 9; + + xmat[0] = 1.0000000000000000000000000000000000000000; + xmat[1] = 0.0000000000000000000000000000000000000000; + xmat[2] = 0.0000000000000000000000000000000000000000; + xmat[3] = 0.0000000000000000000000000000000000000000; + xmat[4] = 1.0000000000000000000000000000000000000000; + xmat[5] = -0.0000000000000000018997602302052549055743; + xmat[6] = 0.0000000000000000000000000000000000000000; + xmat[7] = 0.0000000000000000018997602302052549055743; + xmat[8] = 1.0000000000000000000000000000000000000000; + + xpos[0] = 0.0164299999999999862820843077315657865256; + xpos[1] = -0.0764300000000000256950016819246229715645; + xpos[2] = 0.1748374248948718623353215662064030766487; + + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; + std::vector dir, pos; + mjtNum dist; + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 8); + + EXPECT_EQ(ncons, 4); + + mj_deleteData(data); + mj_deleteModel(model); +} + TEST_F(MjGjkTest, SmallBoxMesh) { static constexpr char xml[] = R"( From 5c12ea871f975a43e0951f39851f9a85e344a0d4 Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Wed, 9 Apr 2025 07:29:44 -0700 Subject: [PATCH 052/191] Split MjSpec out into header to allow use by other pybind modules. This should enable other pybind modules to accept arguments of type MjSpec. PiperOrigin-RevId: 745580987 Change-Id: I435a8d264149369e3fbee14c5d2c628ef74b2541 --- python/mujoco/CMakeLists.txt | 2 + python/mujoco/specs.cc | 95 +------------------------ python/mujoco/specs_wrapper.cc | 124 +++++++++++++++++++++++++++++++++ python/mujoco/specs_wrapper.h | 45 ++++++++++++ 4 files changed, 172 insertions(+), 94 deletions(-) create mode 100644 python/mujoco/specs_wrapper.cc create mode 100644 python/mujoco/specs_wrapper.h diff --git a/python/mujoco/CMakeLists.txt b/python/mujoco/CMakeLists.txt index e6c067f7..03e45b95 100644 --- a/python/mujoco/CMakeLists.txt +++ b/python/mujoco/CMakeLists.txt @@ -417,6 +417,8 @@ endif() mujoco_pybind11_module( _specs specs.cc + specs_wrapper.h + specs_wrapper.cc specs.cc.inc ) target_link_libraries( diff --git a/python/mujoco/specs.cc b/python/mujoco/specs.cc index a87fcf4e..ec444f52 100644 --- a/python/mujoco/specs.cc +++ b/python/mujoco/specs.cc @@ -30,6 +30,7 @@ #include #include "errors.h" #include "indexers.h" // IWYU pragma: keep +#include "specs_wrapper.h" // IWYU pragma: keep #include "raw.h" #include "structs.h" // IWYU pragma: keep #include @@ -70,100 +71,6 @@ using MjDoubleRef10 = Eigen::Ref>; using MjDoubleRef11 = Eigen::Ref>; using MjDoubleRefVec = Eigen::Ref; -struct MjSpec { - MjSpec() : ptr(mj_makeSpec()) {} - MjSpec(raw::MjSpec* ptr, const py::dict& assets_ = {}) : ptr(ptr) { - for (const auto [key, value] : assets_) { - assets[key] = value; - } - } - - // copy constructor and assignment - MjSpec(const MjSpec& other) : ptr(mj_copySpec(other.ptr)) { - override_assets = other.override_assets; - for (const auto [key, value] : other.assets) { - assets[key] = value; - } - parent = other.parent; - } - MjSpec& operator=(const MjSpec& other) { - override_assets = other.override_assets; - ptr = mj_copySpec(other.ptr); - for (const auto [key, value] : other.assets) { - assets[key] = value; - } - parent = other.parent; - return *this; - } - - // move constructor and move assignment - MjSpec(MjSpec&& other) : ptr(other.ptr) { - override_assets = other.override_assets; - other.ptr = nullptr; - for (const auto [key, value] : other.assets) { - assets[key] = value; - } - other.assets.clear(); - parent = other.parent; - other.parent = nullptr; - } - MjSpec& operator=(MjSpec&& other) { - override_assets = other.override_assets; - ptr = other.ptr; - other.ptr = nullptr; - for (const auto [key, value] : other.assets) { - assets[key] = value; - } - other.assets.clear(); - parent = other.parent; - other.parent = nullptr; - return *this; - } - - ~MjSpec() { - mj_deleteSpec(ptr); - } - - raw::MjModel* Compile() { - if (assets.empty()) { - auto m = mj_compile(ptr, 0); - if (!m || mjs_isWarning(ptr)) { - throw py::value_error(mjs_getError(ptr)); - } - return m; - } - mjVFS vfs; - mj_defaultVFS(&vfs); - for (const auto& asset : assets) { - std::string buffer_name = - _impl::StripPath(py::cast(asset.first).c_str()); - std::string buffer = py::cast(asset.second); - const int vfs_error = InterceptMjErrors(mj_addBufferVFS)( - &vfs, buffer_name.c_str(), buffer.c_str(), buffer.size()); - if (vfs_error) { - mj_deleteVFS(&vfs); - if (vfs_error == 2) { - throw py::value_error("Repeated file name in assets dict: " + - buffer_name); - } else { - throw py::value_error("Asset failed to load: " + buffer_name); - } - } - } - auto m = mj_compile(ptr, &vfs); - if (!m || mjs_isWarning(ptr)) { - throw py::value_error(mjs_getError(ptr)); - } - mj_deleteVFS(&vfs); - return m; - } - - raw::MjSpec* ptr; - py::dict assets; - bool override_assets = true; - MjSpec* parent = nullptr; -}; - template static raw::MjSpec* LoadSpecFileImpl( const std::string& filename, diff --git a/python/mujoco/specs_wrapper.cc b/python/mujoco/specs_wrapper.cc new file mode 100644 index 00000000..18ffd3b5 --- /dev/null +++ b/python/mujoco/specs_wrapper.cc @@ -0,0 +1,124 @@ +// Copyright 2024 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. + +#include "specs_wrapper.h" + +#include // IWYU pragma: keep +#include +#include // IWYU pragma: keep +#include // IWYU pragma: keep + +#include // IWYU pragma: keep +#include +#include "errors.h" +#include "indexers.h" // IWYU pragma: keep +#include "raw.h" +#include "structs.h" // IWYU pragma: keep +#include +#include +#include +#include + +namespace py = ::pybind11; + +namespace mujoco::python { + +MjSpec::MjSpec() : ptr(mj_makeSpec()) {} +MjSpec::MjSpec(raw::MjSpec* ptr, const py::dict& assets_) : ptr(ptr) { + for (const auto [key, value] : assets_) { + assets[key] = value; + } +} + +// copy constructor and assignment +MjSpec::MjSpec(const MjSpec& other) : ptr(mj_copySpec(other.ptr)) { + override_assets = other.override_assets; + for (const auto [key, value] : other.assets) { + assets[key] = value; + } + parent = other.parent; +} + +MjSpec& MjSpec::operator=(const MjSpec& other) { + override_assets = other.override_assets; + ptr = mj_copySpec(other.ptr); + for (const auto [key, value] : other.assets) { + assets[key] = value; + } + parent = other.parent; + return *this; +} + +// move constructor and move assignment +MjSpec::MjSpec(MjSpec&& other) : ptr(other.ptr) { + override_assets = other.override_assets; + other.ptr = nullptr; + for (const auto [key, value] : other.assets) { + assets[key] = value; + } + other.assets.clear(); + parent = other.parent; + other.parent = nullptr; +} + +MjSpec& MjSpec::operator=(MjSpec&& other) { + override_assets = other.override_assets; + ptr = other.ptr; + other.ptr = nullptr; + for (const auto [key, value] : other.assets) { + assets[key] = value; + } + other.assets.clear(); + parent = other.parent; + other.parent = nullptr; + return *this; +} + +MjSpec::~MjSpec() { mj_deleteSpec(ptr); } + +raw::MjModel* MjSpec::Compile() { + if (assets.empty()) { + auto m = mj_compile(ptr, 0); + if (!m || mjs_isWarning(ptr)) { + throw py::value_error(mjs_getError(ptr)); + } + return m; + } + mjVFS vfs; + mj_defaultVFS(&vfs); + for (const auto& asset : assets) { + std::string buffer_name = + _impl::StripPath(py::cast(asset.first).c_str()); + std::string buffer = py::cast(asset.second); + const int vfs_error = InterceptMjErrors(mj_addBufferVFS)( + &vfs, buffer_name.c_str(), buffer.c_str(), buffer.size()); + if (vfs_error) { + mj_deleteVFS(&vfs); + if (vfs_error == 2) { + throw py::value_error("Repeated file name in assets dict: " + + buffer_name); + } else { + throw py::value_error("Asset failed to load: " + buffer_name); + } + } + } + auto m = mj_compile(ptr, &vfs); + if (!m || mjs_isWarning(ptr)) { + throw py::value_error(mjs_getError(ptr)); + } + mj_deleteVFS(&vfs); + return m; +} + +} // namespace mujoco::python diff --git a/python/mujoco/specs_wrapper.h b/python/mujoco/specs_wrapper.h new file mode 100644 index 00000000..2ebaac60 --- /dev/null +++ b/python/mujoco/specs_wrapper.h @@ -0,0 +1,45 @@ +// Copyright 2024 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. + +#include "raw.h" +#include +#include +#include +#include + +namespace py = ::pybind11; + +namespace mujoco::python { + +struct MjSpec { + MjSpec(); + MjSpec(raw::MjSpec* ptr, const py::dict& assets_ = {}); + + // copy constructor and assignment + MjSpec(const MjSpec& other); + MjSpec& operator=(const MjSpec& other); + + // move constructor and move assignment + MjSpec(MjSpec&& other); + MjSpec& operator=(MjSpec&& other); + ~MjSpec(); + + raw::MjModel* Compile(); + + raw::MjSpec* ptr; + py::dict assets; + bool override_assets = true; + MjSpec* parent = nullptr; +}; +} // namespace mujoco::python From da1850760ae019aa88610385bdd51e1176d0beec Mon Sep 17 00:00:00 2001 From: Gabe Oppenheimer Date: Wed, 9 Apr 2025 08:39:27 -0700 Subject: [PATCH 053/191] Prepare for v3.3.1 release. PiperOrigin-RevId: 745602958 Change-Id: I412fd58bb063959d5a151f4bca9b3ba26e942d02 --- cmake/MujocoDependencies.cmake | 4 +-- doc/changelog.rst | 45 +++++++++++++++++----------------- python/mujoco/CMakeLists.txt | 4 +-- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/cmake/MujocoDependencies.cmake b/cmake/MujocoDependencies.cmake index 004aedc4..9d5dc01c 100644 --- a/cmake/MujocoDependencies.cmake +++ b/cmake/MujocoDependencies.cmake @@ -39,12 +39,12 @@ set(MUJOCO_DEP_VERSION_qhull CACHE STRING "Version of `qhull` to be fetched." ) set(MUJOCO_DEP_VERSION_Eigen3 - 66f7f51b7e069d0a03a21157fa60b24aece69aeb + 464c1d097891a1462ab28bf8bb763c1683883892 CACHE STRING "Version of `Eigen3` to be fetched." ) set(MUJOCO_DEP_VERSION_abseil - 9ac7062b1860d895fb5a8cbf58c3e9ef8f674b5f # LTS 20250127.0 + d9e4955c65cd4367dd6bf46f4ccb8cd3d100540b # LTS 20250127.1 CACHE STRING "Version of `abseil` to be fetched." ) diff --git a/doc/changelog.rst b/doc/changelog.rst index f2590378..fa30391f 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -1,27 +1,26 @@ ========= Changelog ========= - -Upcoming version (not yet released) ------------------------------------ +Version 3.3.1 (Apr 9, 2025) +---------------------------- .. admonition:: Breaking API changes :class: attention - - The default value of the flag for toggling :ref:`internal flex contacts` was changed from - "true" to "false". This feature has proven to be counterintuitive for users. - - All of the attach functions (``mjs_attachBody``, ``mjs_attachFrame``, ``mjs_attachToSite``, - ``mjs_attachFrameToSite``) have been removed and replaced by a single function :ref:`mjs_attach`. + 1. The default value of the flag for toggling :ref:`internal flex contacts` was changed from + "true" to "false". This feature has proven to be counterintuitive for users. + 2. All of the attach functions (``mjs_attachBody``, ``mjs_attachFrame``, ``mjs_attachToSite``, + ``mjs_attachFrameToSite``) have been removed and replaced by a single function :ref:`mjs_attach`. General ^^^^^^^ -- Added :ref:`tendon armature`: inertia associated with changes in tendon length. -- Added the :ref:`compiler/saveinertial` flag, writing explicit inertial clauses for all - bodies when saving to XML. -- Added :ref:`orientation` attribute to :ref:`composite`. Moreover, allow the - composite to be the direct child of a frame. -- Added :ref:`tendon actuator force limits` and - :ref:`tendon actuator force sensor`. +3. Added :ref:`tendon armature`: inertia associated with changes in tendon length. +4. Added the :ref:`compiler/saveinertial` flag, writing explicit inertial clauses for all + bodies when saving to XML. +5. Added :ref:`orientation` attribute to :ref:`composite`. Moreover, allow the + composite to be the direct child of a frame. +6. Added :ref:`tendon actuator force limits` and + :ref:`tendon actuator force sensor`. MJX ^^^ @@ -29,12 +28,12 @@ MJX Bug fixes ^^^^^^^^^ -- :ref:`mj_jacDot` was missing a term that accounts for the motion of the point with respect to - which the Jacobian is computed, now fixed. -- Fixed a bug that caused the parent frame of elements in the child worldbody to be incorrectly set when attaching an - mjSpec to a frame or a site. -- Fixed a bug that caused shadow rendering to flicker on platforms (e.g., MacOS) that do not support ARB_clip_control. - Fixed in collaboration with :github:user:`aftersomemath`. +7. :ref:`mj_jacDot` was missing a term that accounts for the motion of the point with respect to + which the Jacobian is computed, now fixed. +8. Fixed a bug that caused the parent frame of elements in the child worldbody to be incorrectly set when attaching an + mjSpec to a frame or a site. +9. Fixed a bug that caused shadow rendering to flicker on platforms (e.g., MacOS) that do not support ARB_clip_control. + Fixed in collaboration with :github:user:`aftersomemath`. Python bindings ^^^^^^^^^^^^^^^ @@ -44,9 +43,9 @@ Python bindings :align: right :width: 240px -- Added examples of procedural model creation to the Model Editing tutorial: |mjspec_colab| -- Added support for nameless :ref:`mjSpec` objects in the ``bind`` method, see the corresponding :ref:`section` - in the documentation. +10. Added examples of procedural model creation to the Model Editing tutorial: |mjspec_colab| +11. Added support for nameless :ref:`mjSpec` objects in the ``bind`` method, see the corresponding :ref:`section` + in the documentation. .. |mjspec_colab| image:: https://colab.research.google.com/assets/colab-badge.svg :target: https://colab.research.google.com/github/google-deepmind/mujoco/blob/main/python/mjspec.ipynb diff --git a/python/mujoco/CMakeLists.txt b/python/mujoco/CMakeLists.txt index 03e45b95..2a59248e 100644 --- a/python/mujoco/CMakeLists.txt +++ b/python/mujoco/CMakeLists.txt @@ -140,7 +140,7 @@ findorfetch( GIT_REPO https://github.com/abseil/abseil-cpp GIT_TAG - 9ac7062b1860d895fb5a8cbf58c3e9ef8f674b5f # LTS 20250127.0 + d9e4955c65cd4367dd6bf46f4ccb8cd3d100540b # LTS 20250127.1 TARGETS ${MUJOCO_PYTHON_ABSL_TARGETS} EXCLUDE_FROM_ALL @@ -173,7 +173,7 @@ findorfetch( GIT_REPO https://gitlab.com/libeigen/eigen GIT_TAG - 66f7f51b7e069d0a03a21157fa60b24aece69aeb + 464c1d097891a1462ab28bf8bb763c1683883892 TARGETS Eigen3::Eigen EXCLUDE_FROM_ALL From eadd13038a144a806841ce0c00b32f578ee825a4 Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Wed, 9 Apr 2025 09:27:40 -0700 Subject: [PATCH 054/191] Move Mujoco struct wrappers definitions into library separate from pybind module. This allows other modules to depend on functions such as FromRawPointer that previously were only defined in the _struct python module. PiperOrigin-RevId: 745619670 Change-Id: I9f1af60cb665c00d853b7ca38201b37302d278af --- python/mujoco/CMakeLists.txt | 20 + python/mujoco/structs.cc | 1616 +++-------------------------- python/mujoco/structs_wrappers.cc | 1328 ++++++++++++++++++++++++ 3 files changed, 1505 insertions(+), 1459 deletions(-) create mode 100644 python/mujoco/structs_wrappers.cc diff --git a/python/mujoco/CMakeLists.txt b/python/mujoco/CMakeLists.txt index 2a59248e..ac15ccd5 100644 --- a/python/mujoco/CMakeLists.txt +++ b/python/mujoco/CMakeLists.txt @@ -278,6 +278,25 @@ target_link_libraries( raw ) +add_library(structs_wrappers STATIC + structs_wrappers.cc + serialization.h + indexers.cc +) +target_include_directories(structs_wrappers PRIVATE ${Python3_INCLUDE_DIRS}) +target_link_libraries( + structs_wrappers + PRIVATE absl::flat_hash_map + absl::span + crossplatform + mujoco + raw + structs_header + pybind11::headers + Eigen3::Eigen +) + + add_library(functions_header INTERFACE) target_sources(functions_header INTERFACE functions.h) set_target_properties(functions_header PROPERTIES PUBLIC_HEADER functions.h) @@ -403,6 +422,7 @@ target_link_libraries( func_wrap function_traits structs_header + structs_wrappers ) if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/specs.cc.inc) diff --git a/python/mujoco/structs.cc b/python/mujoco/structs.cc index 77200386..3cf6eb82 100644 --- a/python/mujoco/structs.cc +++ b/python/mujoco/structs.cc @@ -16,37 +16,23 @@ #include -#include -#include -#include // NOLINT(build/c++11) -#include -#include #include -#include -#include #include #include #include -#include #include #include #include -#include -#include -#include #include #include -#include #include #include #include "errors.h" #include "function_traits.h" #include "indexer_xmacro.h" #include "indexers.h" -#include "private.h" #include "raw.h" -#include "serialization.h" #include #include #include @@ -69,13 +55,9 @@ namespace { // (dim0, dim1). #define X_ARRAY_SHAPE(dim0, dim1) XArrayShapeImpl(#dim1)((dim0), (dim1)) -std::vector XArrayShapeImpl1D(int dim0, int dim1) { - return {dim0}; -} +std::vector XArrayShapeImpl1D(int dim0, int dim1) { return {dim0}; } -std::vector XArrayShapeImpl2D(int dim0, int dim1) { - return {dim0, dim1}; -} +std::vector XArrayShapeImpl2D(int dim0, int dim1) { return {dim0, dim1}; } constexpr auto XArrayShapeImpl(const std::string_view dim1_str) { if (dim1_str == "1") { @@ -85,334 +67,6 @@ constexpr auto XArrayShapeImpl(const std::string_view dim1_str) { } } -inline std::size_t NConMax(const mjData* d) { - return d->narena / sizeof(mjContact); -} - -} // namespace - -// ==================== MJOPTION =============================================== -#define X(var, dim) , var(InitPyArray(std::array{dim}, ptr_->var, owner_)) -MjOptionWrapper::MjWrapper() - : WrapperBase([]() { - raw::MjOption* const opt = new raw::MjOption; - mj_defaultOption(opt); - return opt; - }()) - MJOPTION_VECTORS {} - -MjOptionWrapper::MjWrapper(raw::MjOption* ptr, py::handle owner) - : WrapperBase(ptr, owner) - MJOPTION_VECTORS {} -#undef X - -MjOptionWrapper::MjWrapper(const MjOptionWrapper& other) - : MjOptionWrapper() { - *this->ptr_ = *other.ptr_; -} - -// ==================== MJVISUAL =============================================== -#define X(var) var(InitPyArray(ptr_->var, owner_)) -MjVisualHeadlightWrapper::MjWrapper() - : WrapperBase(new raw::MjVisualHeadlight{}), - X(ambient), - X(diffuse), - X(specular) {} - -MjVisualHeadlightWrapper::MjWrapper( - raw::MjVisualHeadlight* ptr, py::handle owner) - : WrapperBase(ptr, owner), - X(ambient), - X(diffuse), - X(specular) {} -#undef X - -MjVisualHeadlightWrapper::MjWrapper(const MjVisualHeadlightWrapper& other) - : MjVisualHeadlightWrapper() { - *this->ptr_ = *other.ptr_; -} - -#define X(var) var(InitPyArray(ptr_->var, owner_)) -MjVisualRgbaWrapper::MjWrapper() - : WrapperBase(new raw::MjVisualRgba{}), - X(fog), - X(haze), - X(force), - X(inertia), - X(joint), - X(actuator), - X(actuatornegative), - X(actuatorpositive), - X(com), - X(camera), - X(light), - X(selectpoint), - X(connect), - X(contactpoint), - X(contactforce), - X(contactfriction), - X(contacttorque), - X(contactgap), - X(rangefinder), - X(constraint), - X(slidercrank), - X(crankbroken), - X(frustum) {} - -MjVisualRgbaWrapper::MjWrapper(raw::MjVisualRgba* ptr, py::handle owner) - : WrapperBase(ptr, owner), - X(fog), - X(haze), - X(force), - X(inertia), - X(joint), - X(actuator), - X(actuatornegative), - X(actuatorpositive), - X(com), - X(camera), - X(light), - X(selectpoint), - X(connect), - X(contactpoint), - X(contactforce), - X(contactfriction), - X(contacttorque), - X(contactgap), - X(rangefinder), - X(constraint), - X(slidercrank), - X(crankbroken), - X(frustum) {} -#undef X - -MjVisualRgbaWrapper::MjWrapper(const MjVisualRgbaWrapper& other) - : MjVisualRgbaWrapper() { - *this->ptr_ = *other.ptr_; -} - -MjVisualWrapper::MjWrapper() - : WrapperBase(new raw::MjVisual{}), - headlight(&ptr_->headlight, owner_), - rgba(&ptr_->rgba, owner_) {} - -MjVisualWrapper::MjWrapper(raw::MjVisual* ptr, py::handle owner) - : WrapperBase(ptr, owner), - headlight(&ptr_->headlight, owner_), - rgba(&ptr_->rgba, owner_) {} - - -MjVisualWrapper::MjWrapper(const MjVisualWrapper& other) - : MjVisualWrapper() { - *this->ptr_ = *other.ptr_; -} - -// ==================== MJMODEL ================================================ -static void MjModelCapsuleDestructor(PyObject* pyobj) { - mj_deleteModel( - static_cast(PyCapsule_GetPointer(pyobj, nullptr))); -} - -static absl::flat_hash_map& -MjModelRawPointerMap() { - static auto* hash_map = - new absl::flat_hash_map(); - return *hash_map; -} - -MjModelWrapper* MjModelWrapper::FromRawPointer(raw::MjModel* m) noexcept { - try { - auto& map = MjModelRawPointerMap(); - { - py::gil_scoped_acquire gil; - auto found = map.find(m); - return found != map.end() ? found->second : nullptr; - } - } catch (...) { - return nullptr; - } -} - -#undef MJ_M -#define MJ_M(x) ptr_->x -#define X(dtype, var, dim0, dim1) \ - , var (InitPyArray(X_ARRAY_SHAPE(ptr_->dim0, dim1), ptr_->var, owner_)) -MjModelWrapper::MjWrapper(raw::MjModel* ptr) - : WrapperBase(ptr, &MjModelCapsuleDestructor), - opt(&ptr->opt, owner_), - vis(&ptr->vis, owner_), - stat(&ptr->stat, owner_) - MJMODEL_POINTERS, - text_data_bytes(ptr->text_data, ptr->ntextdata), - names_bytes(ptr->names, ptr->nnames), - paths_bytes(ptr->paths, ptr->npaths), - indexer_(ptr, owner_) { - bool is_newly_inserted = false; - { - py::gil_scoped_acquire gil; - is_newly_inserted = MjModelRawPointerMap().insert({ptr_, this}).second; - } - if (!is_newly_inserted) { - throw UnexpectedError( - "MjModelWrapper(mjModel*): MjModelRawPointerMap already contains this " - "raw mjModel*"); - } -} - -MjModelWrapper::MjWrapper(MjModelWrapper&& other) - : WrapperBase(other.ptr_, other.owner_), - opt(&ptr_->opt, owner_), - vis(&ptr_->vis, owner_), - stat(&ptr_->stat, owner_) - MJMODEL_POINTERS, - text_data_bytes(ptr_->text_data, ptr_->ntextdata), - names_bytes(ptr_->names, ptr_->nnames), - paths_bytes(ptr_->paths, ptr_->npaths), - indexer_(ptr_, owner_) { - bool is_newly_inserted = false; - { - py::gil_scoped_acquire gil; - is_newly_inserted = - MjModelRawPointerMap().insert_or_assign(ptr_, this).second; - } - if (is_newly_inserted) { - throw UnexpectedError( - "MjModelRawPointerMap does not contains the moved-from mjModel*"); - } - other.ptr_ = nullptr; -} -#undef X -#undef MJ_M -#define MJ_M(x) x - -// Delegating to the MjModelWrapper::MjWrapper(raw::MjModel*) constructor, -// no need to modify MjModelRawPointerMap here. -MjModelWrapper::MjWrapper(const MjModelWrapper& other) - : MjModelWrapper(InterceptMjErrors(mj_copyModel)(NULL, other.get())) {} - -MjModelWrapper::~MjWrapper() { - if (ptr_) { - bool erased = false; - { - py::gil_scoped_acquire gil; - erased = MjModelRawPointerMap().erase(ptr_); - } - if (!erased) { - std::cerr << "MjModelRawPointerMap does not contain this raw mjModel*" - << std::endl; - std::terminate(); - } - } -} - -// Helper function for both LoadXMLFile and LoadBinaryFile. -// Creates a temporary MJB from the assets dictionary if one is supplied. -template -static raw::MjModel* LoadModelFileImpl( - const std::string& filename, - const std::vector& assets, - LoadFunc&& loadfunc) { - mjVFS vfs; - mjVFS* vfs_ptr = nullptr; - if (!assets.empty()) { - mj_defaultVFS(&vfs); - vfs_ptr = &vfs; - for (const auto& asset : assets) { - std::string buffer_name = StripPath(asset.name); - const int vfs_error = InterceptMjErrors(mj_addBufferVFS)( - vfs_ptr, buffer_name.c_str(), asset.content, asset.content_size); - if (vfs_error) { - mj_deleteVFS(vfs_ptr); - if (vfs_error == 2) { - throw py::value_error("Repeated file name in assets dict: " + - buffer_name); - } else { - throw py::value_error("Asset failed to load: " + buffer_name); - } - } - } - } - - raw::MjModel* model = loadfunc(filename.c_str(), vfs_ptr); - mj_deleteVFS(vfs_ptr); - if (model && !model->buffer) { - mj_deleteModel(model); - model = nullptr; - } - return model; -} - -MjModelWrapper MjModelWrapper::LoadXMLFile( - const std::string& filename, - const std::optional>& assets) { - const auto converted_assets = ConvertAssetsDict(assets); - raw::MjModel* model; - { - py::gil_scoped_release no_gil; - char error[1024]; - model = LoadModelFileImpl( - filename, converted_assets, - [&error](const char* filename, const mjVFS* vfs) { - return InterceptMjErrors(mj_loadXML)( - filename, vfs, error, sizeof(error)); - }); - if (!model) { - throw py::value_error(error); - } - } - return MjModelWrapper(model); -} - -MjModelWrapper MjModelWrapper::LoadBinaryFile( - const std::string& filename, - const std::optional>& assets) { - const auto converted_assets = ConvertAssetsDict(assets); - raw::MjModel* model; - { - py::gil_scoped_release no_gil; - model = LoadModelFileImpl( - filename, converted_assets, InterceptMjErrors(mj_loadModel)); - if (!model) { - throw py::value_error("mj_loadModel: failed to load from mjb"); - } - } - return MjModelWrapper(model); -} - -MjModelWrapper MjModelWrapper::LoadXML( - const std::string& xml, - const std::optional>& assets) { - auto converted_assets = ConvertAssetsDict(assets); - raw::MjModel* model; - { - py::gil_scoped_release no_gil; - std::string model_filename = "model_.xml"; - if (assets.has_value()) { - while (assets->find(model_filename) != assets->end()) { - model_filename = - model_filename.substr(0, model_filename.size() - 4) + "_.xml"; - } - } - converted_assets.emplace_back( - model_filename.c_str(), xml.c_str(), xml.length()); - char error[1024]; - model = LoadModelFileImpl( - model_filename, converted_assets, - [&error](const char* filename, const mjVFS* vfs) { - return InterceptMjErrors(mj_loadXML)( - filename, vfs, error, sizeof(error)); - }); - if (!model) { - throw py::value_error(error); - } - } - return MjModelWrapper(model); -} - -MjModelWrapper MjModelWrapper::WrapRawModel(raw::MjModel* m) { - return MjModelWrapper(m); -} - py::tuple RecompileSpec(raw::MjSpec* spec, const MjModelWrapper& old_m, const MjDataWrapper& old_d) { raw::MjModel* m = static_cast(mju_malloc(sizeof(mjModel))); @@ -428,950 +82,8 @@ py::tuple RecompileSpec(raw::MjSpec* spec, const MjModelWrapper& old_m, return py::make_tuple(m_pyobj, d_pyobj); } -namespace { -// A byte at the start of serialized mjModel structs, which can be incremented -// when we change the serialization logic to reject pickles from an unsupported -// future version. -constexpr static char kSerializationVersion = 1; - -void CheckInput(const std::istream& input, std::string class_name) { - if (input.fail()) { - throw py::value_error("Invalid serialized " + class_name + "."); - } -} - } // namespace -void MjModelWrapper::Serialize(std::ostream& output) const { - WriteChar(output, kSerializationVersion); - - int model_size = mj_sizeModel(get()); - WriteInt(output, model_size); - std::string buffer(model_size, 0); - mj_saveModel(get(), nullptr, buffer.data(), model_size); - WriteBytes(output, buffer.data(), model_size); -} - -std::unique_ptr MjModelWrapper::Deserialize( - std::istream& input) { - CheckInput(input, "mjModel"); - - char serializationVersion = ReadChar(input); - CheckInput(input, "mjModel"); - - if (serializationVersion != kSerializationVersion) { - throw py::value_error("Incompatible serialization version."); - } - - std::size_t model_size = ReadInt(input); - CheckInput(input, "mjModel"); - if (model_size < 0) { - throw py::value_error("Invalid serialized mjModel."); - } - std::string model_bytes(model_size, 0); - ReadBytes(input, model_bytes.data(), model_size); - CheckInput(input, "mjModel"); - - raw::MjModel* model = LoadModelFileImpl( - "model.mjb", - {{"model.mjb", model_bytes.data(), static_cast(model_size)}}, - InterceptMjErrors(mj_loadModel)); - if (!model) { - throw py::value_error("Invalid serialized mjModel."); - } - return std::unique_ptr(new MjModelWrapper(model)); -} - -// ==================== MJCONTACT ============================================== -#define X(var) var(InitPyArray(ptr_->var, owner_)) -MjContactWrapper::MjWrapper() - : WrapperBase(new raw::MjContact{}), - X(pos), - X(frame), - X(friction), - X(solref), - X(solreffriction), - X(solimp), - X(H), - X(geom), - X(flex), - X(elem), - X(vert) {} - -MjContactWrapper::MjWrapper(raw::MjContact* ptr, py::handle owner) - : WrapperBase(ptr, owner), - X(pos), - X(frame), - X(friction), - X(solref), - X(solreffriction), - X(solimp), - X(H), - X(geom), - X(flex), - X(elem), - X(vert) {} -#undef X - -MjContactWrapper::MjWrapper(const MjContactWrapper& other) - : MjContactWrapper() { - *this->ptr_ = *other.ptr_; -} - -MjContactList::MjStructList(raw::MjContact* ptr, int nconmax, - int* ncon, py::handle owner) - : StructListBase(ptr, nconmax, owner, /* lazy = */ true), - ncon_(ncon) {} - -// Slicing -MjContactList::MjStructList(MjContactList& other, py::slice slice) - : StructListBase(other, slice), - ncon_(other.ncon_) {} - -// ==================== MJDATA ================================================= -static void MjDataCapsuleDestructor(PyObject* pyobj) { - mj_deleteData( - static_cast(PyCapsule_GetPointer(pyobj, nullptr))); -} - -absl::flat_hash_map& -MjDataRawPointerMap() { - static auto* hash_map = - new absl::flat_hash_map(); - return *hash_map; -} - -MjDataWrapper* MjDataWrapper::FromRawPointer(raw::MjData* m) noexcept { - try { - auto& map = MjDataRawPointerMap(); - { - py::gil_scoped_acquire gil; - auto found = map.find(m); - return found != map.end() ? found->second : nullptr; - } - } catch (...) { - return nullptr; - } -} - -namespace { -// default timer callback (seconds) -mjtNum GetTime() { - using Clock = std::chrono::steady_clock; - using Seconds = std::chrono::duration; - static const Clock::time_point tm_start = Clock::now(); - return Seconds(Clock::now() - tm_start).count(); -} -} // namespace - -MjDataWrapper::MjWrapper(MjModelWrapper* model) - : WrapperBase(InterceptMjErrors(mj_makeData)(model->get()), - &MjDataCapsuleDestructor), -#undef MJ_M -#define MJ_M(x) model->get()->x -#define X(dtype, var, dim0, dim1) \ - var(InitPyArray(X_ARRAY_SHAPE(model->get()->dim0, dim1), ptr_->var, owner_)), - MJDATA_POINTERS -#undef MJ_M -#define MJ_M(x) (x) -#undef X - - contact(MjContactList(ptr_->contact, NConMax(ptr_), &ptr_->ncon, owner_)), - -#define X(dtype, var, dim0, dim1) var(InitPyArray(ptr_->var, owner_)), - MJDATA_VECTOR -#undef X - model_(model), - model_ref_(py::cast(model_)), - indexer_(ptr_, model_->get(), owner_) { - bool is_newly_inserted = false; - { - py::gil_scoped_acquire gil; - is_newly_inserted = MjDataRawPointerMap().insert({ptr_, this}).second; - } - if (!is_newly_inserted) { - throw UnexpectedError( - "MjDataRawPointerMap already contains this raw mjData*"); - } - - // install default timer if not already installed - { - py::gil_scoped_acquire gil; - if (!mjcb_time) { - mjcb_time = GetTime; - } - } -} - -MjDataWrapper::MjWrapper(const MjDataWrapper& other) - : WrapperBase(other.Copy(), &MjDataCapsuleDestructor), -#undef MJ_M -#define MJ_M(x) other.model_->get()->x -#define X(dtype, var, dim0, dim1) \ - var(InitPyArray(X_ARRAY_SHAPE(other.model_->get()->dim0, dim1), ptr_->var, \ - owner_)), - MJDATA_POINTERS -#undef MJ_M -#define MJ_M(x) (x) -#undef X - - contact(MjContactList(ptr_->contact, NConMax(ptr_), &ptr_->ncon, owner_)), - -#define X(dtype, var, dim0, dim1) var(InitPyArray(ptr_->var, owner_)), - MJDATA_VECTOR -#undef X - model_(other.model_), - model_ref_(other.model_ref_), - indexer_(ptr_, model_->get(), owner_) { - bool is_newly_inserted = false; - { - py::gil_scoped_acquire gil; - is_newly_inserted = MjDataRawPointerMap().insert({ptr_, this}).second; - } - if (!is_newly_inserted) { - throw UnexpectedError( - "MjDataRawPointerMap already contains this raw mjData*"); - } -} - -MjDataWrapper::MjWrapper(MjDataWrapper&& other) - : WrapperBase(other.ptr_, other.owner_), -#undef MJ_M -#define MJ_M(x) other.model_->get()->x -#define X(dtype, var, dim0, dim1) \ - var(InitPyArray(X_ARRAY_SHAPE(other.model_->get()->dim0, dim1), ptr_->var, \ - owner_)), - MJDATA_POINTERS -#undef MJ_M -#define MJ_M(x) (x) -#undef X - - contact(MjContactList(ptr_->contact, NConMax(ptr_), &ptr_->ncon, owner_)), - -#define X(dtype, var, dim0, dim1) var(InitPyArray(ptr_->var, owner_)), - MJDATA_VECTOR -#undef X - model_(other.model_), - model_ref_(std::move(other.model_ref_)), - indexer_(ptr_, model_->get(), owner_) { - bool is_newly_inserted = false; - { - py::gil_scoped_acquire gil; - is_newly_inserted = - MjDataRawPointerMap().insert_or_assign(ptr_, this).second; - } - if (is_newly_inserted) { - throw UnexpectedError( - "MjDataRawPointerMap does not contains the moved-from mjData*"); - } - other.ptr_ = nullptr; -} - -MjDataWrapper::MjWrapper(const MjDataWrapper& other, MjModelWrapper* model) - : WrapperBase(other.Copy(), &MjDataCapsuleDestructor), -#undef MJ_M -#define MJ_M(x) other.model_->get()->x -#define X(dtype, var, dim0, dim1) \ - var(InitPyArray(X_ARRAY_SHAPE(other.model_->get()->dim0, dim1), ptr_->var, \ - owner_)), - MJDATA_POINTERS -#undef MJ_M -#define MJ_M(x) (x) -#undef X - - contact(MjContactList(ptr_->contact, NConMax(ptr_), &ptr_->ncon, owner_)), - -#define X(dtype, var, dim0, dim1) var(InitPyArray(ptr_->var, owner_)), - MJDATA_VECTOR -#undef X - model_(model), - model_ref_(py::cast(model_)), - indexer_(ptr_, model_->get(), owner_) { - bool is_newly_inserted = false; - { - py::gil_scoped_acquire gil; - is_newly_inserted = MjDataRawPointerMap().insert({ptr_, this}).second; - } - if (!is_newly_inserted) { - throw UnexpectedError( - "MjDataRawPointerMap already contains this raw mjData*"); - } -} - -MjDataWrapper::MjWrapper(MjModelWrapper* model, raw::MjData* d) - : WrapperBase(d, &MjDataCapsuleDestructor), -#undef MJ_M -#define MJ_M(x) model->get()->x -#define X(dtype, var, dim0, dim1) \ - var(InitPyArray(X_ARRAY_SHAPE(model->get()->dim0, dim1), ptr_->var, owner_)), - MJDATA_POINTERS -#undef MJ_M -#define MJ_M(x) (x) -#undef X - - contact(MjContactList(ptr_->contact, NConMax(ptr_), &ptr_->ncon, owner_)), - -#define X(dtype, var, dim0, dim1) var(InitPyArray(ptr_->var, owner_)), - MJDATA_VECTOR -#undef X - model_(model), - model_ref_(py::cast(model_)), - indexer_(ptr_, model_->get(), owner_) { - bool is_newly_inserted = false; - { - py::gil_scoped_acquire gil; - is_newly_inserted = MjDataRawPointerMap().insert({ptr_, this}).second; - } - if (!is_newly_inserted) { - throw UnexpectedError( - "MjDataRawPointerMap already contains this raw mjData*"); - } -} - -MjDataWrapper::~MjWrapper() { - if (ptr_) { - bool erased = false; - { - py::gil_scoped_acquire gil; - erased = MjDataRawPointerMap().erase(ptr_); - } - if (!erased) { - std::cerr << "MjDataRawPointerMap does not contain this raw mjData*" - << std::endl; - std::terminate(); - } - } -} - -void MjDataWrapper::Serialize(std::ostream& output) const { - // TODO: Replace this custom serialization with a protobuf - WriteChar(output, kSerializationVersion); - - model_->Serialize(output); - - // Write struct and scalar fields -#define X(var) WriteBytes(output, &ptr_->var, sizeof(ptr_->var)) - X(maxuse_stack); - X(maxuse_arena); - X(maxuse_con); - X(maxuse_efc); - X(solver); - X(timer); - X(warning); - X(ncon); - X(ne); - X(nf); - X(nJ); - X(nA); - X(nefc); - X(nisland); - X(time); - X(energy); -#undef X - - // Write buffer and arena contents - { - MJDATA_POINTERS_PREAMBLE((this->model_->get())) - -#define X(type, name, nr, nc) \ - WriteBytes(output, ptr_->name, sizeof(type)*(this->model_->get()->nr)*(nc)); - MJDATA_POINTERS -#undef X - -#undef MJ_M -#define MJ_M(x) this->model_->get()->x -#undef MJ_D -#define MJ_D(x) this->ptr_->x -#define X(type, name, nr, nc) \ - if ((nr) * (nc)) { \ - WriteBytes(output, ptr_->name, \ - ptr_->name ? sizeof(type) * (nr) * (nc) : 0); \ - } - - MJDATA_ARENA_POINTERS_CONTACT - MJDATA_ARENA_POINTERS_SOLVER - if (mj_isDual(this->model_->get())) { - MJDATA_ARENA_POINTERS_DUAL - } - if (this->ptr_->nisland) { - MJDATA_ARENA_POINTERS_ISLAND - } -#undef MJ_M -#define MJ_M(x) x -#undef MJ_D -#define MJ_D(x) x -#undef X - } -} - -MjDataWrapper MjDataWrapper::Deserialize(std::istream& input) { - char serializationVersion = ReadChar(input); - CheckInput(input, "mjData"); - - if (serializationVersion != kSerializationVersion) { - throw py::value_error("Incompatible serialization version."); - } - - // Read the model that was used to create the mjData. - std::unique_ptr m_wrapper = - MjModelWrapper::Deserialize(input); - raw::MjModel& m = *m_wrapper->get(); - - bool is_dual = mj_isDual(&m); - - raw::MjData* d = mj_makeData(&m); - if (!d) { - throw py::value_error("Failed to create mjData."); - } - - // Read structs and scalar fields -#define X(var) \ - ReadBytes(input, (void*) &d->var, sizeof(d->var)); \ - CheckInput(input, "mjData"); - - X(maxuse_stack); - X(maxuse_arena); - X(maxuse_con); - X(maxuse_efc); - X(solver); - X(timer); - X(warning); - X(ncon); - X(ne); - X(nf); - X(nJ); - X(nA); - X(nefc); - X(nisland); - X(time); - X(energy); -#undef X - - // Read buffer and arena contents - { - MJDATA_POINTERS_PREAMBLE((&m)) - -#define X(type, name, nr, nc) \ - ReadBytes(input, d->name, sizeof(type)*(m.nr)*(nc)); - MJDATA_POINTERS -#undef X - -#undef MJ_M -#define MJ_M(x) m.x -#undef MJ_D -#define MJ_D(x) d->x -// arena pointers might be null, so we need to check the size before allocating. -#define X(type, name, nr, nc) \ - if ((nr) * (nc)) { \ - std::size_t actual_nbytes = ReadInt(input); \ - if (actual_nbytes) { \ - if (actual_nbytes != sizeof(type) * (nr) * (nc)) { \ - input.setstate(input.rdstate() | std::ios_base::failbit); \ - } else { \ - d->name = static_castname)>( \ - mj_arenaAllocByte(d, sizeof(type) * (nr) * (nc), alignof(type))); \ - input.read(reinterpret_cast(d->name), actual_nbytes); \ - } \ - } else { \ - d->name = nullptr; \ - } \ - } - - MJDATA_ARENA_POINTERS_CONTACT - MJDATA_ARENA_POINTERS_SOLVER - if (is_dual) { - MJDATA_ARENA_POINTERS_DUAL - } - if (d->nisland) { - MJDATA_ARENA_POINTERS_ISLAND - } -#undef MJ_M -#define MJ_M(x) x -#undef MJ_D -#define MJ_D(x) x -#undef X - } - CheckInput(input, "mjData"); - - // All bytes should have been used. - input.ignore(1); - if (!input.eof()) { - throw py::value_error("Invalid serialized mjData."); - } - - return MjDataWrapper(m_wrapper.release(), d); -} - -raw::MjData* MjDataWrapper::Copy() const { - const raw::MjModel* m = model_->get(); - return InterceptMjErrors(mj_copyData)(NULL, m, this->ptr_); -} - -// ==================== MJSTATISTIC ============================================ -#define X(var) var(InitPyArray(ptr_->var, owner_)) -MjStatisticWrapper::MjWrapper() - : WrapperBase(new raw::MjStatistic{}), - X(center) {} - -MjStatisticWrapper::MjWrapper(raw::MjStatistic* ptr, py::handle owner) - : WrapperBase(ptr, owner), - X(center) {} -#undef X - -MjStatisticWrapper::MjWrapper(const MjStatisticWrapper& other) - : MjStatisticWrapper() { - *this->ptr_ = *other.ptr_; -} - -// ==================== MJWARNINGSTAT ========================================== -MjWarningStatWrapper::MjWrapper() - : WrapperBase(new raw::MjWarningStat{}) {} - -MjWarningStatWrapper::MjWrapper(raw::MjWarningStat* ptr, py::handle owner) - : WrapperBase(ptr, owner) {} - -MjWarningStatWrapper::MjWrapper(const MjWarningStatWrapper& other) - : MjWarningStatWrapper() { - *this->ptr_ = *other.ptr_; -} - -#define X(type, var) \ - var(std::vector{num}, std::vector{sizeof(raw::MjWarningStat)}, \ - &ptr->var, owner) -MjWarningStatList::MjStructList(raw::MjWarningStat* ptr, int num, - py::handle owner) - : StructListBase(ptr, num, owner), X(int, lastinfo), X(int, number) {} -#undef X - -// Slicing -#define X(type, var) var(other.var[slice]) -MjWarningStatList::MjStructList(MjWarningStatList& other, py::slice slice) - : StructListBase(other, slice), - X(int, lastinfo), - X(int, number) {} -#undef X - -// ==================== MJTIMERSTAT ============================================ -MjTimerStatWrapper::MjWrapper() - : WrapperBase(new raw::MjTimerStat{}) {} - -MjTimerStatWrapper::MjWrapper(raw::MjTimerStat* ptr, py::handle owner) - : WrapperBase(ptr, owner) {} - -MjTimerStatWrapper::MjWrapper(const MjTimerStatWrapper& other) - : MjTimerStatWrapper() { - *this->ptr_ = *other.ptr_; -} - -#define X(type, var) \ - var(std::vector{num}, std::vector{sizeof(raw::MjTimerStat)}, \ - &ptr->var, owner) -MjTimerStatList::MjStructList(raw::MjTimerStat* ptr, int num, py::handle owner) - : StructListBase(ptr, num, owner), - X(mjtNum, duration), - X(int, number) {} -#undef X - -// Slicing -#define X(type, var) var(other.var[slice]) -MjTimerStatList::MjStructList(MjTimerStatList& other, py::slice slice) - : StructListBase(other, slice), - X(mjtNum, duration), - X(int, number) {} -#undef X - -// ==================== MJSOLVERSTAT =========================================== -MjSolverStatWrapper::MjWrapper() - : WrapperBase(new raw::MjSolverStat{}) {} - -MjSolverStatWrapper::MjWrapper(raw::MjSolverStat* ptr, py::handle owner) - : WrapperBase(ptr, owner) {} - -MjSolverStatWrapper::MjWrapper(const MjSolverStatWrapper& other) - : MjSolverStatWrapper() { - *this->ptr_ = *other.ptr_; -} - -#define X(type, var) \ - var(std::vector{num}, std::vector{sizeof(raw::MjSolverStat)}, \ - &ptr->var, owner) -MjSolverStatList::MjStructList(raw::MjSolverStat* ptr, int num, - py::handle owner) - : StructListBase(ptr, num, owner), - X(mjtNum, improvement), - X(mjtNum, gradient), - X(mjtNum, lineslope), - X(int, nactive), - X(int, nchange), - X(int, neval), - X(int, nupdate) {} -#undef X -#undef XN - -// Slicing -#define X(type, var) var(other.var[slice]) -MjSolverStatList::MjStructList(MjSolverStatList& other, py::slice slice) - : StructListBase(other, slice), - X(mjtNum, improvement), - X(mjtNum, gradient), - X(mjtNum, lineslope), - X(int, nactive), - X(int, nchange), - X(int, neval), - X(int, nupdate) {} -#undef X - -// ==================== MJVPERTURB ============================================= -#define X(var) var(InitPyArray(ptr_->var, owner_)) -MjvPerturbWrapper::MjWrapper() - : WrapperBase([]() { - raw::MjvPerturb* const pert = new raw::MjvPerturb; - mjv_defaultPerturb(pert); - return pert; - }()), - X(refpos), - X(refquat), - X(refselpos), - X(localpos) {} -#undef X - -MjvPerturbWrapper::MjWrapper(const MjvPerturbWrapper& other) - : MjvPerturbWrapper() { - *this->ptr_ = *other.ptr_; -} - -// ==================== MJVCAMERA ============================================== -#define X(var) var(InitPyArray(ptr_->var, owner_)) -MjvCameraWrapper::MjWrapper() - : WrapperBase([]() { - raw::MjvCamera* const cam = new raw::MjvCamera; - mjv_defaultCamera(cam); - return cam; - }()), - X(lookat) {} -#undef X - -MjvCameraWrapper::MjWrapper(const MjvCameraWrapper& other) - : MjvCameraWrapper() { - *this->ptr_ = *other.ptr_; -} - -// ==================== MJVGLCAMERA ============================================ -#define X(var) var(InitPyArray(ptr_->var, owner_)) -MjvGLCameraWrapper::MjWrapper() - : WrapperBase(new raw::MjvGLCamera{}), - X(pos), - X(forward), - X(up) {} - -MjvGLCameraWrapper::MjWrapper(raw::MjvGLCamera* ptr, py::handle owner) - : WrapperBase(ptr, owner), - X(pos), - X(forward), - X(up) {} -#undef X - -MjvGLCameraWrapper::MjWrapper(raw::MjvGLCamera&& other) - : MjvGLCameraWrapper() { - *this->ptr_ = other; -} - -MjvGLCameraWrapper::MjWrapper(const MjvGLCameraWrapper& other) - : MjvGLCameraWrapper() { - *this->ptr_ = *other.ptr_; -} - -// ==================== MJVGEOM ================================================ -#define X(var) var(InitPyArray(ptr_->var, owner_)) -MjvGeomWrapper::MjWrapper() - : WrapperBase(new raw::MjvGeom{}), - X(size), - X(pos), - mat([this]() { - static_assert(sizeof(ptr_->mat) == sizeof(ptr_->mat[0])*9); - return InitPyArray(std::array{3, 3}, ptr_->mat, owner_); - }()), - X(rgba) { - mjv_initGeom(ptr_, mjGEOM_NONE, nullptr, nullptr, nullptr, nullptr); -} - -MjvGeomWrapper::MjWrapper(raw::MjvGeom* ptr, py::handle owner) - : WrapperBase(ptr, owner), - X(size), - X(pos), - mat([this]() { - static_assert(sizeof(ptr_->mat) == sizeof(ptr_->mat[0])*9); - return InitPyArray(std::array{3, 3}, ptr_->mat, owner_); - }()), - X(rgba) {} -#undef X - -MjvGeomWrapper::MjWrapper(const MjvGeomWrapper& other) - : MjvGeomWrapper() { - *this->ptr_ = *other.ptr_; -} - -// ==================== MJVLIGHT =============================================== -#define X(var) var(InitPyArray(ptr_->var, owner_)) -MjvLightWrapper::MjWrapper() - : WrapperBase(new raw::MjvLight{}), - X(pos), - X(dir), - X(attenuation), - X(ambient), - X(diffuse), - X(specular) {} - -MjvLightWrapper::MjWrapper(raw::MjvLight* ptr, py::handle owner) - : WrapperBase(ptr, owner), - X(pos), - X(dir), - X(attenuation), - X(ambient), - X(diffuse), - X(specular) {} -#undef X - -MjvLightWrapper::MjWrapper(const MjvLightWrapper& other) - : MjvLightWrapper() { - *this->ptr_ = *other.ptr_; -} - -// ==================== MJVOPTION ============================================== -#define X(var) var(InitPyArray(ptr_->var, owner_)) -MjvOptionWrapper::MjWrapper() - : WrapperBase([]() { - raw::MjvOption* const opt = new raw::MjvOption; - mjv_defaultOption(opt); - return opt; - }()), - X(geomgroup), - X(sitegroup), - X(jointgroup), - X(tendongroup), - X(actuatorgroup), - X(flexgroup), - X(skingroup), - X(flags) {} -#undef X - -MjvOptionWrapper::MjWrapper(const MjvOptionWrapper& other) - : MjvOptionWrapper() { - *this->ptr_ = *other.ptr_; -} - -// ==================== MJVSCENE =============================================== -static void MjvSceneCapsuleDestructor(PyObject* pyobj) { - py::gil_scoped_acquire gil; - auto* scn = static_cast(PyCapsule_GetPointer(pyobj, nullptr)); - if (scn) { - mjv_freeScene(scn); - delete scn; - } -} - -#define X(var) var(InitPyArray(ptr_->var, owner_)) -#define XN(var, n) var(InitPyArray(std::array{n}, ptr_->var, owner_)) -MjvSceneWrapper::MjWrapper() - : WrapperBase([]() { - raw::MjvScene *const scn = new raw::MjvScene; - mjv_defaultScene(scn); - InterceptMjErrors(mjv_makeScene)(nullptr, scn, 0); - return scn; - }(), &MjvSceneCapsuleDestructor), - nskinvert(0), - XN(geoms, 0), - XN(geomorder, 0), - XN(flexedgeadr, 0), - XN(flexedgenum, 0), - XN(flexvertadr, 0), - XN(flexvertnum, 0), - XN(flexfaceadr, 0), - XN(flexfacenum, 0), - XN(flexfaceused, 0), - XN(flexedge, 0), - XN(flexvert, 0), - XN(flexface, 0), - XN(flexnormal, 0), - XN(flextexcoord, 0), - XN(skinfacenum, 0), - XN(skinvertadr, 0), - XN(skinvertnum, 0), - XN(skinvert, 0), - XN(skinnormal, 0), - X(lights), - X(camera), - X(translate), - X(rotate), - X(flags), - X(framergb) {} - -#define XN(var, n) var(InitPyArray(std::array{n}, ptr_->var, owner_)) -MjvSceneWrapper::MjWrapper(const MjModelWrapper& model, int maxgeom) - : WrapperBase( - [maxgeom](const raw::MjModel* m) { - raw::MjvScene *const scn = new raw::MjvScene; - mjv_defaultScene(scn); - InterceptMjErrors(mjv_makeScene)(m, scn, maxgeom); - return scn; - }(model.get()), - &MjvSceneCapsuleDestructor), - nskinvert([](const raw::MjModel* m) { - int nskinvert = 0; - for (int i = 0; i < m->nskin; ++i) { - nskinvert += m->skin_vertnum[i]; - } - return nskinvert; - }(model.get())), - nflexface([](const raw::MjModel* m) { - int nflexface = 0; - int flexfacenum = 0; - for (int f=0; f < m->nflex; f++) { - if (m->flex_dim[f] == 0) { - // 1D : 0 - flexfacenum = 0; - } else if (m->flex_dim[f] == 2) { - // 2D: 2*fragments + 2*elements - flexfacenum = 2*m->flex_shellnum[f] + 2*m->flex_elemnum[f]; - } else { - // 3D: max(fragments, 4*maxlayer) - // find number of elements in biggest layer - int maxlayer = 0, layer = 0, nlayer = 1; - while (nlayer) { - nlayer = 0; - for (int e=0; e < m->flex_elemnum[f]; e++) { - if (m->flex_elemlayer[m->flex_elemadr[f]+e] == layer) { - nlayer++; - } - } - maxlayer = mjMAX(maxlayer, nlayer); - layer++; - } - flexfacenum = mjMAX(m->flex_shellnum[f], 4*maxlayer); - } - - // accumulate over flexes - nflexface += flexfacenum; - } - return nflexface; - }(model.get())), - nflexedge(model.get()->nflexedge), - nflexvert(model.get()->nflexvert), - XN(geoms, ptr_->maxgeom), - XN(geomorder, ptr_->maxgeom), - XN(flexedgeadr, ptr_->nflex), - XN(flexedgenum, ptr_->nflex), - XN(flexvertadr, ptr_->nflex), - XN(flexvertnum, ptr_->nflex), - XN(flexfaceadr, ptr_->nflex), - XN(flexfacenum, ptr_->nflex), - XN(flexfaceused, ptr_->nflex), - XN(flexedge, 2*nflexedge), - XN(flexvert, 3*nflexvert), - XN(flexface, 9*nflexface), - XN(flexnormal, 9*nflexface), - XN(flextexcoord, 6*nflexface), - XN(skinfacenum, ptr_->nskin), - XN(skinvertadr, ptr_->nskin), - XN(skinvertnum, ptr_->nskin), - XN(skinvert, 3*nskinvert), - XN(skinnormal, 3*nskinvert), - X(lights), - X(camera), - X(translate), - X(rotate), - X(flags), - X(framergb) {} -#undef X -#undef XN - -template -static T* MallocAndCopy(const T* src, int count) { - if (src) { - T* out = static_cast(mju_malloc(count * sizeof(T))); - std::memcpy(out, src, count * sizeof(T)); - return out; - } else { - return nullptr; - } -} - -MjvSceneWrapper::MjWrapper(const MjvSceneWrapper& other) - : MjvSceneWrapper() { - mjv_freeScene(ptr_); - *ptr_ = *other.ptr_; - -#define XN(var, n) \ - ptr_->var = MallocAndCopy(other.ptr_->var, n); \ - var = InitPyArray(std::array{n}, ptr_->var, owner_); - - XN(geoms, ptr_->ngeom); - XN(geomorder, ptr_->ngeom); - XN(flexedgeadr, ptr_->nflex); - XN(flexedgenum, ptr_->nflex); - XN(flexvertadr, ptr_->nflex); - XN(flexvertnum, ptr_->nflex); - XN(flexfaceadr, ptr_->nflex); - XN(flexfacenum, ptr_->nflex); - XN(flexfaceused, ptr_->nflex); - XN(flexedge, 2*nflexedge); - XN(flexvert, 3*nflexvert); - XN(flexface, 9*nflexface); - XN(flexnormal, 9*nflexface); - XN(flextexcoord, 6*nflexface); - XN(skinfacenum, ptr_->nskin); - XN(skinvertadr, ptr_->nskin); - XN(skinvertnum, ptr_->nskin); - XN(skinvert, 3*nskinvert); - XN(skinnormal, 3*nskinvert); - -#undef XN -} - -// ==================== MJVFIGURE ============================================== -#define X(var) var(InitPyArray(ptr_->var, owner_)) -MjvFigureWrapper::MjWrapper() - : WrapperBase([]() { - raw::MjvFigure* const fig = new raw::MjvFigure; - mjv_defaultFigure(fig); - return fig; - }()), - X(flg_ticklabel), - X(gridsize), - X(gridrgb), - X(figurergba), - X(panergba), - X(legendrgba), - X(textrgb), - X(linergb), - X(range), - X(highlight), - X(linepnt), - X(linedata), - X(xaxispixel), - X(yaxispixel), - X(xaxisdata), - X(yaxisdata), -#undef X - - linename([](raw::MjvFigure* ptr, py::handle owner) { -// Use a macro to help us static_assert that the array extents here are kept -// in sync with mjVisualize.h. -#define MAKE_STR_ARRAY(N1, N2) \ - static_assert( \ - std::is_same_v); \ - return py::array(py::dtype("|S" #N2), N1, ptr->linename, owner); - - MAKE_STR_ARRAY(mjMAXLINE, 100); - -#undef MAKE_STR_ARRAY - }(ptr_, owner_)) {} - -MjvFigureWrapper::MjWrapper(const MjvFigureWrapper& other) - : MjvFigureWrapper() { - *this->ptr_ = *other.ptr_; -} - PYBIND11_MODULE(_structs, m) { py::module_::import("mujoco._enums"); @@ -1439,8 +151,8 @@ PYBIND11_MODULE(_structs, m) { << self.attr("__class__").attr("__name__").cast(); #define X(type, var) \ - result << "\n " #var ": "; \ - StructReprImpl(self.attr(#var), result, 2); + result << "\n " #var ": "; \ + StructReprImpl(self.attr(#var), result, 2); X(raw::MjVisualGlobal, global_) X(raw::MjVisualQuality, quality) @@ -1457,10 +169,10 @@ PYBIND11_MODULE(_structs, m) { mjVisualGlobal.def("__copy__", [](const raw::MjVisualGlobal& other) { return raw::MjVisualGlobal(other); }); - mjVisualGlobal.def( - "__deepcopy__", [](const raw::MjVisualGlobal& other, py::dict) { - return raw::MjVisualGlobal(other); - }); + mjVisualGlobal.def("__deepcopy__", + [](const raw::MjVisualGlobal& other, py::dict) { + return raw::MjVisualGlobal(other); + }); DefineStructFunctions(mjVisualGlobal); #define X(var) mjVisualGlobal.def_readwrite(#var, &raw::MjVisualGlobal::var) X(orthographic); @@ -1481,10 +193,10 @@ PYBIND11_MODULE(_structs, m) { mjVisualQuality.def("__copy__", [](const raw::MjVisualQuality& other) { return raw::MjVisualQuality(other); }); - mjVisualQuality.def( - "__deepcopy__", [](const raw::MjVisualQuality& other, py::dict) { - return raw::MjVisualQuality(other); - }); + mjVisualQuality.def("__deepcopy__", + [](const raw::MjVisualQuality& other, py::dict) { + return raw::MjVisualQuality(other); + }); DefineStructFunctions(mjVisualQuality); #define X(var) mjVisualQuality.def_readwrite(#var, &raw::MjVisualQuality::var) X(shadowsize); @@ -1495,21 +207,20 @@ PYBIND11_MODULE(_structs, m) { #undef X py::class_ mjVisualHeadlight(mjVisual, "Headlight"); - mjVisualHeadlight.def( - "__copy__", [](const MjVisualHeadlightWrapper& other) { - return MjVisualHeadlightWrapper(other); - }); - mjVisualHeadlight.def( - "__deepcopy__", [](const MjVisualHeadlightWrapper& other, py::dict) { - return MjVisualHeadlightWrapper(other); - }); + mjVisualHeadlight.def("__copy__", [](const MjVisualHeadlightWrapper& other) { + return MjVisualHeadlightWrapper(other); + }); + mjVisualHeadlight.def("__deepcopy__", + [](const MjVisualHeadlightWrapper& other, py::dict) { + return MjVisualHeadlightWrapper(other); + }); DefineStructFunctions(mjVisualHeadlight); - #define X(var) \ - DefinePyArray(mjVisualHeadlight, #var, &MjVisualHeadlightWrapper::var) +#define X(var) \ + DefinePyArray(mjVisualHeadlight, #var, &MjVisualHeadlightWrapper::var) X(ambient); X(diffuse); X(specular); - #undef X +#undef X mjVisualHeadlight.def_property( "active", [](const MjVisualHeadlightWrapper& c) { return c.get()->active; }, @@ -1521,10 +232,9 @@ PYBIND11_MODULE(_structs, m) { mjVisualMap.def("__copy__", [](const raw::MjVisualMap& other) { return raw::MjVisualMap(other); }); - mjVisualMap.def( - "__deepcopy__", [](const raw::MjVisualMap& other, py::dict) { - return raw::MjVisualMap(other); - }); + mjVisualMap.def("__deepcopy__", [](const raw::MjVisualMap& other, py::dict) { + return raw::MjVisualMap(other); + }); DefineStructFunctions(mjVisualMap); #define X(var) mjVisualMap.def_readwrite(#var, &raw::MjVisualMap::var) X(stiffness); @@ -1546,10 +256,10 @@ PYBIND11_MODULE(_structs, m) { mjVisualScale.def("__copy__", [](const raw::MjVisualScale& other) { return raw::MjVisualScale(other); }); - mjVisualScale.def( - "__deepcopy__", [](const raw::MjVisualScale& other, py::dict) { - return raw::MjVisualScale(other); - }); + mjVisualScale.def("__deepcopy__", + [](const raw::MjVisualScale& other, py::dict) { + return raw::MjVisualScale(other); + }); DefineStructFunctions(mjVisualScale); #define X(var) mjVisualScale.def_readwrite(#var, &raw::MjVisualScale::var) X(forcewidth); @@ -1575,10 +285,10 @@ PYBIND11_MODULE(_structs, m) { mjVisualRgba.def("__copy__", [](const MjVisualRgbaWrapper& other) { return MjVisualRgbaWrapper(other); }); - mjVisualRgba.def( - "__deepcopy__", [](const MjVisualRgbaWrapper& other, py::dict) { - return MjVisualRgbaWrapper(other); - }); + mjVisualRgba.def("__deepcopy__", + [](const MjVisualRgbaWrapper& other, py::dict) { + return MjVisualRgbaWrapper(other); + }); DefineStructFunctions(mjVisualRgba); #define X(var) DefinePyArray(mjVisualRgba, #var, &MjVisualRgbaWrapper::var) X(fog); @@ -1626,26 +336,26 @@ PYBIND11_MODULE(_structs, m) { // ==================== MJMODEL ============================================== py::class_ mjModel(m, "MjModel"); mjModel.def_static( - "from_xml_string", &MjModelWrapper::LoadXML, - py::arg("xml"), py::arg_v("assets", py::none()), + "from_xml_string", &MjModelWrapper::LoadXML, py::arg("xml"), + py::arg_v("assets", py::none()), py::doc( -R"(Loads an MjModel from an XML string and an optional assets dictionary.)")); + R"(Loads an MjModel from an XML string and an optional assets dictionary.)")); mjModel.def_static("_from_model_ptr", [](uintptr_t addr) { return MjModelWrapper::WrapRawModel(reinterpret_cast(addr)); }); mjModel.def_static( - "from_xml_path", &MjModelWrapper::LoadXMLFile, - py::arg("filename"), py::arg_v("assets", py::none()), + "from_xml_path", &MjModelWrapper::LoadXMLFile, py::arg("filename"), + py::arg_v("assets", py::none()), py::doc( -R"(Loads an MjModel from an XML file and an optional assets dictionary. + R"(Loads an MjModel from an XML file and an optional assets dictionary. The filename for the XML can also refer to a key in the assets dictionary. This is useful for example when the XML is not available as a file on disk.)")); mjModel.def_static( - "from_binary_path", &MjModelWrapper::LoadBinaryFile, - py::arg("filename"), py::arg_v("assets", py::none()), + "from_binary_path", &MjModelWrapper::LoadBinaryFile, py::arg("filename"), + py::arg_v("assets", py::none()), py::doc( -R"(Loads an MjModel from an MJB file and an optional assets dictionary. + R"(Loads an MjModel from an MJB file and an optional assets dictionary. The filename for the MJB can also refer to a key in the assets dictionary. This is useful for example when the MJB is not available as a file on disk.)")); @@ -1705,34 +415,37 @@ This is useful for example when the MJB is not available as a file on disk.)")); return py::tuple(py::cast(fields)); }); -#define X(dtype, var, dim0, dim1) \ - if constexpr (std::string_view(#var) != "text_data" && \ - std::string_view(#var) != "names" && \ - std::string_view(#var) != "paths") { \ - DefinePyArray(mjModel, #var, &MjModelWrapper::var); \ +#define X(dtype, var, dim0, dim1) \ + if constexpr (std::string_view(#var) != "text_data" && \ + std::string_view(#var) != "names" && \ + std::string_view(#var) != "paths") { \ + DefinePyArray(mjModel, #var, &MjModelWrapper::var); \ } MJMODEL_POINTERS #undef X - mjModel.def_property_readonly( - "text_data", [](const MjModelWrapper& m) -> const auto& { - // Return the full bytes array of concatenated text data - return m.text_data_bytes; - }); - mjModel.def_property_readonly( - "names", [](const MjModelWrapper& m) -> const auto& { - // Return the full bytes array of concatenated names - return m.names_bytes; - }); - mjModel.def_property_readonly( - "paths", [](const MjModelWrapper& m) -> const auto& { - // Return the full bytes array of concatenated paths - return m.paths_bytes; - }); - mjModel.def_property_readonly( - "signature", [](const MjModelWrapper& m) -> const uint64_t& { - return m.get()->signature; - }); + mjModel.def_property_readonly("text_data", + [](const MjModelWrapper& m) -> const auto& { + // Return the full bytes array of concatenated + // text data + return m.text_data_bytes; + }); + mjModel.def_property_readonly("names", + [](const MjModelWrapper& m) -> const auto& { + // Return the full bytes array of concatenated + // names + return m.names_bytes; + }); + mjModel.def_property_readonly("paths", + [](const MjModelWrapper& m) -> const auto& { + // Return the full bytes array of concatenated + // paths + return m.paths_bytes; + }); + mjModel.def_property_readonly("signature", + [](const MjModelWrapper& m) -> const uint64_t& { + return m.get()->signature; + }); #define XGROUP(MjModelGroupedViews, field, nfield, FIELD_XMACROS) \ mjModel.def( \ @@ -1740,28 +453,28 @@ This is useful for example when the MJB is not available as a file on disk.)")); [](MjModelWrapper& m, int i) -> auto& { return m.indexer().field(i); }, \ py::return_value_policy::reference_internal); \ mjModel.def( \ - #field, [](MjModelWrapper& m, std::string_view name) -> auto& { \ + #field, \ + [](MjModelWrapper& m, std::string_view name) -> auto& { \ return m.indexer().field##_by_name(name); \ }, \ py::return_value_policy::reference_internal, py::arg_v("name", "")); - MJMODEL_VIEW_GROUPS #undef XGROUP -#define XGROUP(spectype, field) \ - mjModel.def( \ - "bind_scalar", \ - [](MjModelWrapper& m, spectype& spec) -> auto& { \ - if (mjs_getSpec(spec.element)->element->signature != \ - m.get()->signature) { \ - throw py::value_error( \ - "The mjSpec does not match mjModel. Please recompile " \ - "the mjSpec."); \ - } \ - return m.indexer().field(mjs_getId(spec.element)); \ - }, \ - py::return_value_policy::reference_internal, \ +#define XGROUP(spectype, field) \ + mjModel.def( \ + "bind_scalar", \ + [](MjModelWrapper& m, spectype& spec) -> auto& { \ + if (mjs_getSpec(spec.element)->element->signature != \ + m.get()->signature) { \ + throw py::value_error( \ + "The mjSpec does not match mjModel. Please recompile " \ + "the mjSpec."); \ + } \ + return m.indexer().field(mjs_getId(spec.element)); \ + }, \ + py::return_value_policy::reference_internal, \ py::arg_v("spec", py::none())); MJMODEL_BIND_GROUPS @@ -1773,7 +486,8 @@ This is useful for example when the MJB is not available as a file on disk.)")); [](MjModelWrapper& m, int i) -> auto& { return m.indexer().field(i); }, \ py::return_value_policy::reference_internal); \ mjModel.def( \ - #altname, [](MjModelWrapper& m, std::string_view name) -> auto& { \ + #altname, \ + [](MjModelWrapper& m, std::string_view name) -> auto& { \ return m.indexer().field##_by_name(name); \ }, \ py::return_value_policy::reference_internal, py::arg_v("name", "")); @@ -1787,12 +501,10 @@ This is useful for example when the MJB is not available as a file on disk.)")); py::class_ groupedViews(m, "_" #MjModelGroupedViews); \ FIELD_XMACROS \ groupedViews.def("__repr__", MjModelStructRepr); \ - groupedViews.def_property_readonly("id", [](GroupedViews& views) { \ - return views.index(); \ - }); \ - groupedViews.def_property_readonly("name", [](GroupedViews& views) { \ - return views.name(); \ - }); \ + groupedViews.def_property_readonly( \ + "id", [](GroupedViews& views) { return views.index(); }); \ + groupedViews.def_property_readonly( \ + "name", [](GroupedViews& views) { return views.name(); }); \ } #define X(type, prefix, var, dim0, dim1) \ groupedViews.def_property( \ @@ -1807,8 +519,8 @@ This is useful for example when the MJB is not available as a file on disk.)")); { py::handle builtins(PyEval_GetBuiltins()); builtins[MjModelWrapper::kFromRawPointer] = - reinterpret_cast(reinterpret_cast( - &MjModelWrapper::FromRawPointer)); + reinterpret_cast( + reinterpret_cast(&MjModelWrapper::FromRawPointer)); } // ==================== MJWARNINGSTAT ======================================== @@ -1817,10 +529,10 @@ This is useful for example when the MJB is not available as a file on disk.)")); mjWarningStat.def("__copy__", [](const MjWarningStatWrapper& other) { return MjWarningStatWrapper(other); }); - mjWarningStat.def( - "__deepcopy__", [](const MjWarningStatWrapper& other, py::dict) { - return MjWarningStatWrapper(other); - }); + mjWarningStat.def("__deepcopy__", + [](const MjWarningStatWrapper& other, py::dict) { + return MjWarningStatWrapper(other); + }); DefineStructFunctions(mjWarningStat); #define X(var) \ mjWarningStat.def_property( \ @@ -1833,10 +545,8 @@ This is useful for example when the MJB is not available as a file on disk.)")); #undef X py::class_ mjWarningStatList(m, "_MjWarningStatList"); - mjWarningStatList.def( - "__getitem__", - &MjWarningStatList::operator[], - py::return_value_policy::reference); + mjWarningStatList.def("__getitem__", &MjWarningStatList::operator[], + py::return_value_policy::reference); mjWarningStatList.def( "__getitem__", [](MjWarningStatList& list, ::mjtWarning idx) { return list[idx]; }, @@ -1857,10 +567,10 @@ This is useful for example when the MJB is not available as a file on disk.)")); mjTimerStat.def("__copy__", [](const MjTimerStatWrapper& other) { return MjTimerStatWrapper(other); }); - mjTimerStat.def( - "__deepcopy__", [](const MjTimerStatWrapper& other, py::dict) { - return MjTimerStatWrapper(other); - }); + mjTimerStat.def("__deepcopy__", + [](const MjTimerStatWrapper& other, py::dict) { + return MjTimerStatWrapper(other); + }); DefineStructFunctions(mjTimerStat); #define X(var) \ mjTimerStat.def_property( \ @@ -1873,10 +583,8 @@ This is useful for example when the MJB is not available as a file on disk.)")); #undef X py::class_ mjTimerStatList(m, "_MjTimerStatList"); - mjTimerStatList.def( - "__getitem__", - &MjTimerStatList::operator[], - py::return_value_policy::reference); + mjTimerStatList.def("__getitem__", &MjTimerStatList::operator[], + py::return_value_policy::reference); mjTimerStatList.def( "__getitem__", [](MjTimerStatList& list, ::mjtTimer idx) { return list[idx]; }, @@ -1885,8 +593,7 @@ This is useful for example when the MJB is not available as a file on disk.)")); mjTimerStatList.def("__len__", &MjTimerStatList::size); DefineStructFunctions(mjTimerStatList); -#define X(type, var) \ - mjTimerStatList.def_readonly(#var, &MjTimerStatList::var) +#define X(type, var) mjTimerStatList.def_readonly(#var, &MjTimerStatList::var) X(mjtNum, duration); X(int, number); #undef X @@ -1897,10 +604,10 @@ This is useful for example when the MJB is not available as a file on disk.)")); mjSolverStat.def("__copy__", [](const MjSolverStatWrapper& other) { return MjSolverStatWrapper(other); }); - mjSolverStat.def( - "__deepcopy__", [](const MjSolverStatWrapper& other, py::dict) { - return MjSolverStatWrapper(other); - }); + mjSolverStat.def("__deepcopy__", + [](const MjSolverStatWrapper& other, py::dict) { + return MjSolverStatWrapper(other); + }); DefineStructFunctions(mjSolverStat); #define X(var) \ mjSolverStat.def_property( \ @@ -1919,13 +626,12 @@ This is useful for example when the MJB is not available as a file on disk.)")); py::class_ mjSolverStatList(m, "_MjSolverStatList"); mjSolverStatList.def("__getitem__", &MjSolverStatList::operator[], - py::return_value_policy::reference); + py::return_value_policy::reference); mjSolverStatList.def("__getitem__", &MjSolverStatList::Slice); mjSolverStatList.def("__len__", &MjSolverStatList::size); DefineStructFunctions(mjSolverStatList); -#define X(type, var) \ - mjSolverStatList.def_readonly(#var, &MjSolverStatList::var) +#define X(type, var) mjSolverStatList.def_readonly(#var, &MjSolverStatList::var) X(mjtNum, improvement); X(mjtNum, gradient); X(mjtNum, lineslope); @@ -2025,12 +731,10 @@ This is useful for example when the MJB is not available as a file on disk.)")); mjData.def_property_readonly("_address", [](const MjDataWrapper& d) { return reinterpret_cast(d.get()); }); - mjData.def_property_readonly("model", [](const MjDataWrapper& d) { - return &d.model(); - }); - mjData.def("__copy__", [](const MjDataWrapper& other) { - return MjDataWrapper(other); - }); + mjData.def_property_readonly( + "model", [](const MjDataWrapper& d) { return &d.model(); }); + mjData.def("__copy__", + [](const MjDataWrapper& other) { return MjDataWrapper(other); }); mjData.def("__deepcopy__", [](const MjDataWrapper& other, py::dict memo) { // Use copy.deepcopy(model) to make a model that Python is aware of. py::object new_model_py = @@ -2048,9 +752,8 @@ This is useful for example when the MJB is not available as a file on disk.)")); return MjDataWrapper::Deserialize(input); })); mjData.def_property_readonly( - "signature", [](const MjDataWrapper& d) -> uint64_t { - return d.get()->signature; - }); + "signature", + [](const MjDataWrapper& d) -> uint64_t { return d.get()->signature; }); #define X(type, var) \ mjData.def_property( \ @@ -2097,7 +800,8 @@ This is useful for example when the MJB is not available as a file on disk.)")); [](MjDataWrapper& d, int i) -> auto& { return d.indexer().field(i); }, \ py::return_value_policy::reference_internal); \ mjData.def( \ - #field, [](MjDataWrapper& d, std::string_view name) -> auto& { \ + #field, \ + [](MjDataWrapper& d, std::string_view name) -> auto& { \ return d.indexer().field##_by_name(name); \ }, \ py::return_value_policy::reference_internal, py::arg_v("name", "")); @@ -2105,19 +809,19 @@ This is useful for example when the MJB is not available as a file on disk.)")); MJDATA_VIEW_GROUPS #undef XGROUP -#define XGROUP(spectype, field) \ - mjData.def( \ - "bind_scalar", \ - [](MjDataWrapper& d, spectype& spec) -> auto& { \ - if (mjs_getSpec(spec.element)->element->signature != \ - d.get()->signature) { \ - throw py::value_error( \ - "The mjSpec does not match mjData. Please recompile "\ - "the mjSpec."); \ - } \ - return d.indexer().field(mjs_getId(spec.element)); \ - }, \ - py::return_value_policy::reference_internal, \ +#define XGROUP(spectype, field) \ + mjData.def( \ + "bind_scalar", \ + [](MjDataWrapper& d, spectype& spec) -> auto& { \ + if (mjs_getSpec(spec.element)->element->signature != \ + d.get()->signature) { \ + throw py::value_error( \ + "The mjSpec does not match mjData. Please recompile " \ + "the mjSpec."); \ + } \ + return d.indexer().field(mjs_getId(spec.element)); \ + }, \ + py::return_value_policy::reference_internal, \ py::arg_v("spec", py::none())); MJDATA_BIND_GROUPS @@ -2129,7 +833,8 @@ This is useful for example when the MJB is not available as a file on disk.)")); [](MjDataWrapper& d, int i) -> auto& { return d.indexer().field(i); }, \ py::return_value_policy::reference_internal); \ mjData.def( \ - #altname, [](MjDataWrapper& d, std::string_view name) -> auto& { \ + #altname, \ + [](MjDataWrapper& d, std::string_view name) -> auto& { \ return d.indexer().field##_by_name(name); \ }, \ py::return_value_policy::reference_internal, py::arg_v("name", "")); @@ -2143,12 +848,10 @@ This is useful for example when the MJB is not available as a file on disk.)")); py::class_ groupedViews(m, "_" #MjDataGroupedViews); \ FIELD_XMACROS \ groupedViews.def("__repr__", MjDataStructRepr); \ - groupedViews.def_property_readonly("id", [](GroupedViews& views) { \ - return views.index(); \ - }); \ - groupedViews.def_property_readonly("name", [](GroupedViews& views) { \ - return views.name(); \ - }); \ + groupedViews.def_property_readonly( \ + "id", [](GroupedViews& views) { return views.index(); }); \ + groupedViews.def_property_readonly( \ + "name", [](GroupedViews& views) { return views.name(); }); \ } #define X(type, prefix, var, dim0, dim1) \ groupedViews.def_property( \ @@ -2162,9 +865,8 @@ This is useful for example when the MJB is not available as a file on disk.)")); { py::handle builtins(PyEval_GetBuiltins()); - builtins[MjDataWrapper::kFromRawPointer] = - reinterpret_cast(reinterpret_cast( - &MjDataWrapper::FromRawPointer)); + builtins[MjDataWrapper::kFromRawPointer] = reinterpret_cast( + reinterpret_cast(&MjDataWrapper::FromRawPointer)); } // ==================== MJSTATISTIC ========================================== @@ -2173,10 +875,10 @@ This is useful for example when the MJB is not available as a file on disk.)")); mjStatistic.def("__copy__", [](const MjStatisticWrapper& other) { return MjStatisticWrapper(other); }); - mjStatistic.def( - "__deepcopy__", [](const MjStatisticWrapper& other, py::dict) { - return MjStatisticWrapper(other); - }); + mjStatistic.def("__deepcopy__", + [](const MjStatisticWrapper& other, py::dict) { + return MjStatisticWrapper(other); + }); DefineStructFunctions(mjStatistic); #define X(var) \ @@ -2198,9 +900,8 @@ This is useful for example when the MJB is not available as a file on disk.)")); // ==================== MJLROPT ============================================== py::class_ mjLROpt(m, "MjLROpt"); mjLROpt.def(py::init<>()); - mjLROpt.def("__copy__", [](const raw::MjLROpt& other) { - return raw::MjLROpt(other); - }); + mjLROpt.def("__copy__", + [](const raw::MjLROpt& other) { return raw::MjLROpt(other); }); mjLROpt.def("__deepcopy__", [](const raw::MjLROpt& other, py::dict) { return raw::MjLROpt(other); }); @@ -2285,11 +986,10 @@ This is useful for example when the MJB is not available as a file on disk.)")); mjvGLCamera.def("__copy__", [](const MjvGLCameraWrapper& other) { return MjvGLCameraWrapper(other); }); - mjvGLCamera.def( - "__deepcopy__", - [](const MjvGLCameraWrapper& other, py::dict) { - return MjvGLCameraWrapper(other); - }); + mjvGLCamera.def("__deepcopy__", + [](const MjvGLCameraWrapper& other, py::dict) { + return MjvGLCameraWrapper(other); + }); DefineStructFunctions(mjvGLCamera); #define X(var) \ mjvGLCamera.def_property( \ @@ -2400,7 +1100,7 @@ This is useful for example when the MJB is not available as a file on disk.)")); #define X(var) \ mjvOption.def_property( \ #var, [](const MjvOptionWrapper& c) { return c.get()->var; }, \ - [](MjvOptionWrapper& c, decltype(raw::MjvOption::var) rhs) { \ + [](MjvOptionWrapper& c, decltype(raw::MjvOption::var) rhs) { \ c.get()->var = rhs; \ }) X(label); @@ -2423,8 +1123,8 @@ This is useful for example when the MJB is not available as a file on disk.)")); // ==================== MJVSCENE ============================================= py::class_ mjvScene(m, "MjvScene"); mjvScene.def(py::init<>()); - mjvScene.def(py::init(), - py::arg("model"), py::arg("maxgeom")); + mjvScene.def(py::init(), py::arg("model"), + py::arg("maxgeom")); mjvScene.def("__copy__", [](const MjvSceneWrapper& other) { return MjvSceneWrapper(other); }); @@ -2551,11 +1251,9 @@ This is useful for example when the MJB is not available as a file on disk.)")); py::arg("cam1"), py::arg("cam2"), py::doc(python_traits::mjv_averageCamera::doc)); - m.def( - "_recompile_spec_addr", - [](uintptr_t spec_addr, const MjModelWrapper& m, const MjDataWrapper& d) { - return RecompileSpec(reinterpret_cast(spec_addr), m, d); - } - ); + m.def("_recompile_spec_addr", [](uintptr_t spec_addr, const MjModelWrapper& m, + const MjDataWrapper& d) { + return RecompileSpec(reinterpret_cast(spec_addr), m, d); + }); } // PYBIND11_MODULE NOLINT(readability/fn_size) } // namespace mujoco::python::_impl diff --git a/python/mujoco/structs_wrappers.cc b/python/mujoco/structs_wrappers.cc new file mode 100644 index 00000000..7c1ec9b0 --- /dev/null +++ b/python/mujoco/structs_wrappers.cc @@ -0,0 +1,1328 @@ +// Copyright 2022 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. + +#include + +#include +#include +#include +#include // NOLINT(build/c++11) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include "errors.h" +#include "private.h" +#include "raw.h" +#include "serialization.h" +#include "structs.h" +#include +#include +#include +#include +#include +#include +#include + +namespace mujoco::python::_impl { + +namespace py = ::pybind11; + +namespace { +#define PTRDIFF(x, y) \ + reinterpret_cast(x) - reinterpret_cast(y) + +// Returns the shape of a NumPy array given the dimensions from an X Macro. +// If dim1 is a _literal_ constant 1, the resulting array is 1-dimensional of +// length dim0, otherwise the resulting array is 2-dimensional of shape +// (dim0, dim1). +#define X_ARRAY_SHAPE(dim0, dim1) XArrayShapeImpl(#dim1)((dim0), (dim1)) + +std::vector XArrayShapeImpl1D(int dim0, int dim1) { return {dim0}; } + +std::vector XArrayShapeImpl2D(int dim0, int dim1) { return {dim0, dim1}; } + +constexpr auto XArrayShapeImpl(const std::string_view dim1_str) { + if (dim1_str == "1") { + return XArrayShapeImpl1D; + } else { + return XArrayShapeImpl2D; + } +} + +inline std::size_t NConMax(const mjData* d) { + return d->narena / sizeof(mjContact); +} + +} // namespace + +// ==================== MJOPTION =============================================== +#define X(var, dim) , var(InitPyArray(std::array{dim}, ptr_->var, owner_)) +MjOptionWrapper::MjWrapper() + : WrapperBase([]() { + raw::MjOption* const opt = new raw::MjOption; + mj_defaultOption(opt); + return opt; + }()) MJOPTION_VECTORS {} + +MjOptionWrapper::MjWrapper(raw::MjOption* ptr, py::handle owner) + : WrapperBase(ptr, owner) MJOPTION_VECTORS {} +#undef X + +MjOptionWrapper::MjWrapper(const MjOptionWrapper& other) : MjOptionWrapper() { + *this->ptr_ = *other.ptr_; +} + +// ==================== MJVISUAL =============================================== +#define X(var) var(InitPyArray(ptr_->var, owner_)) +MjVisualHeadlightWrapper::MjWrapper() + : WrapperBase(new raw::MjVisualHeadlight{}), + X(ambient), + X(diffuse), + X(specular) {} + +MjVisualHeadlightWrapper::MjWrapper(raw::MjVisualHeadlight* ptr, + py::handle owner) + : WrapperBase(ptr, owner), X(ambient), X(diffuse), X(specular) {} +#undef X + +MjVisualHeadlightWrapper::MjWrapper(const MjVisualHeadlightWrapper& other) + : MjVisualHeadlightWrapper() { + *this->ptr_ = *other.ptr_; +} + +#define X(var) var(InitPyArray(ptr_->var, owner_)) +MjVisualRgbaWrapper::MjWrapper() + : WrapperBase(new raw::MjVisualRgba{}), + X(fog), + X(haze), + X(force), + X(inertia), + X(joint), + X(actuator), + X(actuatornegative), + X(actuatorpositive), + X(com), + X(camera), + X(light), + X(selectpoint), + X(connect), + X(contactpoint), + X(contactforce), + X(contactfriction), + X(contacttorque), + X(contactgap), + X(rangefinder), + X(constraint), + X(slidercrank), + X(crankbroken), + X(frustum) {} + +MjVisualRgbaWrapper::MjWrapper(raw::MjVisualRgba* ptr, py::handle owner) + : WrapperBase(ptr, owner), + X(fog), + X(haze), + X(force), + X(inertia), + X(joint), + X(actuator), + X(actuatornegative), + X(actuatorpositive), + X(com), + X(camera), + X(light), + X(selectpoint), + X(connect), + X(contactpoint), + X(contactforce), + X(contactfriction), + X(contacttorque), + X(contactgap), + X(rangefinder), + X(constraint), + X(slidercrank), + X(crankbroken), + X(frustum) {} +#undef X + +MjVisualRgbaWrapper::MjWrapper(const MjVisualRgbaWrapper& other) + : MjVisualRgbaWrapper() { + *this->ptr_ = *other.ptr_; +} + +MjVisualWrapper::MjWrapper() + : WrapperBase(new raw::MjVisual{}), + headlight(&ptr_->headlight, owner_), + rgba(&ptr_->rgba, owner_) {} + +MjVisualWrapper::MjWrapper(raw::MjVisual* ptr, py::handle owner) + : WrapperBase(ptr, owner), + headlight(&ptr_->headlight, owner_), + rgba(&ptr_->rgba, owner_) {} + +MjVisualWrapper::MjWrapper(const MjVisualWrapper& other) : MjVisualWrapper() { + *this->ptr_ = *other.ptr_; +} + +// ==================== MJMODEL ================================================ +static void MjModelCapsuleDestructor(PyObject* pyobj) { + mj_deleteModel( + static_cast(PyCapsule_GetPointer(pyobj, nullptr))); +} + +static absl::flat_hash_map& +MjModelRawPointerMap() { + static auto* hash_map = + new absl::flat_hash_map(); + return *hash_map; +} + +MjModelWrapper* MjModelWrapper::FromRawPointer(raw::MjModel* m) noexcept { + try { + auto& map = MjModelRawPointerMap(); + { + py::gil_scoped_acquire gil; + auto found = map.find(m); + return found != map.end() ? found->second : nullptr; + } + } catch (...) { + return nullptr; + } +} + +#undef MJ_M +#define MJ_M(x) ptr_->x +#define X(dtype, var, dim0, dim1) \ + , var(InitPyArray(X_ARRAY_SHAPE(ptr_->dim0, dim1), ptr_->var, owner_)) +MjModelWrapper::MjWrapper(raw::MjModel* ptr) + : WrapperBase(ptr, &MjModelCapsuleDestructor), + opt(&ptr->opt, owner_), + vis(&ptr->vis, owner_), + stat(&ptr->stat, owner_) MJMODEL_POINTERS, + text_data_bytes(ptr->text_data, ptr->ntextdata), + names_bytes(ptr->names, ptr->nnames), + paths_bytes(ptr->paths, ptr->npaths), + indexer_(ptr, owner_) { + bool is_newly_inserted = false; + { + py::gil_scoped_acquire gil; + is_newly_inserted = MjModelRawPointerMap().insert({ptr_, this}).second; + } + if (!is_newly_inserted) { + throw UnexpectedError( + "MjModelWrapper(mjModel*): MjModelRawPointerMap already contains this " + "raw mjModel*"); + } +} + +MjModelWrapper::MjWrapper(MjModelWrapper&& other) + : WrapperBase(other.ptr_, other.owner_), + opt(&ptr_->opt, owner_), + vis(&ptr_->vis, owner_), + stat(&ptr_->stat, owner_) MJMODEL_POINTERS, + text_data_bytes(ptr_->text_data, ptr_->ntextdata), + names_bytes(ptr_->names, ptr_->nnames), + paths_bytes(ptr_->paths, ptr_->npaths), + indexer_(ptr_, owner_) { + bool is_newly_inserted = false; + { + py::gil_scoped_acquire gil; + is_newly_inserted = + MjModelRawPointerMap().insert_or_assign(ptr_, this).second; + } + if (is_newly_inserted) { + throw UnexpectedError( + "MjModelRawPointerMap does not contains the moved-from mjModel*"); + } + other.ptr_ = nullptr; +} +#undef X +#undef MJ_M +#define MJ_M(x) x + +// Delegating to the MjModelWrapper::MjWrapper(raw::MjModel*) constructor, +// no need to modify MjModelRawPointerMap here. +MjModelWrapper::MjWrapper(const MjModelWrapper& other) + : MjModelWrapper(InterceptMjErrors(mj_copyModel)(NULL, other.get())) {} + +MjModelWrapper::~MjWrapper() { + if (ptr_) { + bool erased = false; + { + py::gil_scoped_acquire gil; + erased = MjModelRawPointerMap().erase(ptr_); + } + if (!erased) { + std::cerr << "MjModelRawPointerMap does not contain this raw mjModel*" + << std::endl; + std::terminate(); + } + } +} + +// Helper function for both LoadXMLFile and LoadBinaryFile. +// Creates a temporary MJB from the assets dictionary if one is supplied. +template +static raw::MjModel* LoadModelFileImpl(const std::string& filename, + const std::vector& assets, + LoadFunc&& loadfunc) { + mjVFS vfs; + mjVFS* vfs_ptr = nullptr; + if (!assets.empty()) { + mj_defaultVFS(&vfs); + vfs_ptr = &vfs; + for (const auto& asset : assets) { + std::string buffer_name = StripPath(asset.name); + const int vfs_error = InterceptMjErrors(mj_addBufferVFS)( + vfs_ptr, buffer_name.c_str(), asset.content, asset.content_size); + if (vfs_error) { + mj_deleteVFS(vfs_ptr); + if (vfs_error == 2) { + throw py::value_error("Repeated file name in assets dict: " + + buffer_name); + } else { + throw py::value_error("Asset failed to load: " + buffer_name); + } + } + } + } + + raw::MjModel* model = loadfunc(filename.c_str(), vfs_ptr); + mj_deleteVFS(vfs_ptr); + if (model && !model->buffer) { + mj_deleteModel(model); + model = nullptr; + } + return model; +} + +MjModelWrapper MjModelWrapper::LoadXMLFile( + const std::string& filename, + const std::optional>& assets) { + const auto converted_assets = ConvertAssetsDict(assets); + raw::MjModel* model; + { + py::gil_scoped_release no_gil; + char error[1024]; + model = LoadModelFileImpl(filename, converted_assets, + [&error](const char* filename, const mjVFS* vfs) { + return InterceptMjErrors(mj_loadXML)( + filename, vfs, error, sizeof(error)); + }); + if (!model) { + throw py::value_error(error); + } + } + return MjModelWrapper(model); +} + +MjModelWrapper MjModelWrapper::LoadBinaryFile( + const std::string& filename, + const std::optional>& assets) { + const auto converted_assets = ConvertAssetsDict(assets); + raw::MjModel* model; + { + py::gil_scoped_release no_gil; + model = LoadModelFileImpl(filename, converted_assets, + InterceptMjErrors(mj_loadModel)); + if (!model) { + throw py::value_error("mj_loadModel: failed to load from mjb"); + } + } + return MjModelWrapper(model); +} + +MjModelWrapper MjModelWrapper::LoadXML( + const std::string& xml, + const std::optional>& assets) { + auto converted_assets = ConvertAssetsDict(assets); + raw::MjModel* model; + { + py::gil_scoped_release no_gil; + std::string model_filename = "model_.xml"; + if (assets.has_value()) { + while (assets->find(model_filename) != assets->end()) { + model_filename = + model_filename.substr(0, model_filename.size() - 4) + "_.xml"; + } + } + converted_assets.emplace_back(model_filename.c_str(), xml.c_str(), + xml.length()); + char error[1024]; + model = LoadModelFileImpl(model_filename, converted_assets, + [&error](const char* filename, const mjVFS* vfs) { + return InterceptMjErrors(mj_loadXML)( + filename, vfs, error, sizeof(error)); + }); + if (!model) { + throw py::value_error(error); + } + } + return MjModelWrapper(model); +} + +MjModelWrapper MjModelWrapper::WrapRawModel(raw::MjModel* m) { + return MjModelWrapper(m); +} + +py::tuple RecompileSpec(raw::MjSpec* spec, const MjModelWrapper& old_m, + const MjDataWrapper& old_d) { + raw::MjModel* m = static_cast(mju_malloc(sizeof(mjModel))); + m->buffer = nullptr; + raw::MjData* d = mj_copyData(nullptr, old_m.get(), old_d.get()); + if (mj_recompile(spec, nullptr, m, d)) { + throw py::value_error(mjs_getError(spec)); + } + + py::object m_pyobj = py::cast((MjModelWrapper(m))); + py::object d_pyobj = + py::cast((MjDataWrapper(py::cast(m_pyobj), d))); + return py::make_tuple(m_pyobj, d_pyobj); +} + +namespace { +// A byte at the start of serialized mjModel structs, which can be incremented +// when we change the serialization logic to reject pickles from an unsupported +// future version. +constexpr static char kSerializationVersion = 1; + +void CheckInput(const std::istream& input, std::string class_name) { + if (input.fail()) { + throw py::value_error("Invalid serialized " + class_name + "."); + } +} + +} // namespace + +void MjModelWrapper::Serialize(std::ostream& output) const { + WriteChar(output, kSerializationVersion); + + int model_size = mj_sizeModel(get()); + WriteInt(output, model_size); + std::string buffer(model_size, 0); + mj_saveModel(get(), nullptr, buffer.data(), model_size); + WriteBytes(output, buffer.data(), model_size); +} + +std::unique_ptr MjModelWrapper::Deserialize( + std::istream& input) { + CheckInput(input, "mjModel"); + + char serializationVersion = ReadChar(input); + CheckInput(input, "mjModel"); + + if (serializationVersion != kSerializationVersion) { + throw py::value_error("Incompatible serialization version."); + } + + std::size_t model_size = ReadInt(input); + CheckInput(input, "mjModel"); + if (model_size < 0) { + throw py::value_error("Invalid serialized mjModel."); + } + std::string model_bytes(model_size, 0); + ReadBytes(input, model_bytes.data(), model_size); + CheckInput(input, "mjModel"); + + raw::MjModel* model = LoadModelFileImpl( + "model.mjb", + {{"model.mjb", model_bytes.data(), static_cast(model_size)}}, + InterceptMjErrors(mj_loadModel)); + if (!model) { + throw py::value_error("Invalid serialized mjModel."); + } + return std::unique_ptr(new MjModelWrapper(model)); +} + +// ==================== MJCONTACT ============================================== +#define X(var) var(InitPyArray(ptr_->var, owner_)) +MjContactWrapper::MjWrapper() + : WrapperBase(new raw::MjContact{}), + X(pos), + X(frame), + X(friction), + X(solref), + X(solreffriction), + X(solimp), + X(H), + X(geom), + X(flex), + X(elem), + X(vert) {} + +MjContactWrapper::MjWrapper(raw::MjContact* ptr, py::handle owner) + : WrapperBase(ptr, owner), + X(pos), + X(frame), + X(friction), + X(solref), + X(solreffriction), + X(solimp), + X(H), + X(geom), + X(flex), + X(elem), + X(vert) {} +#undef X + +MjContactWrapper::MjWrapper(const MjContactWrapper& other) + : MjContactWrapper() { + *this->ptr_ = *other.ptr_; +} + +MjContactList::MjStructList(raw::MjContact* ptr, int nconmax, int* ncon, + py::handle owner) + : StructListBase(ptr, nconmax, owner, /* lazy = */ true), ncon_(ncon) {} + +// Slicing +MjContactList::MjStructList(MjContactList& other, py::slice slice) + : StructListBase(other, slice), ncon_(other.ncon_) {} + +// ==================== MJDATA ================================================= +static void MjDataCapsuleDestructor(PyObject* pyobj) { + mj_deleteData( + static_cast(PyCapsule_GetPointer(pyobj, nullptr))); +} + +absl::flat_hash_map& MjDataRawPointerMap() { + static auto* hash_map = + new absl::flat_hash_map(); + return *hash_map; +} + +MjDataWrapper* MjDataWrapper::FromRawPointer(raw::MjData* m) noexcept { + try { + auto& map = MjDataRawPointerMap(); + { + py::gil_scoped_acquire gil; + auto found = map.find(m); + return found != map.end() ? found->second : nullptr; + } + } catch (...) { + return nullptr; + } +} + +namespace { +// default timer callback (seconds) +mjtNum GetTime() { + using Clock = std::chrono::steady_clock; + using Seconds = std::chrono::duration; + static const Clock::time_point tm_start = Clock::now(); + return Seconds(Clock::now() - tm_start).count(); +} +} // namespace + +MjDataWrapper::MjWrapper(MjModelWrapper* model) + : WrapperBase(InterceptMjErrors(mj_makeData)(model->get()), + &MjDataCapsuleDestructor), +#undef MJ_M +#define MJ_M(x) model->get()->x +#define X(dtype, var, dim0, dim1) \ + var(InitPyArray(X_ARRAY_SHAPE(model->get()->dim0, dim1), ptr_->var, owner_)), + MJDATA_POINTERS +#undef MJ_M +#define MJ_M(x) (x) +#undef X + + contact(MjContactList(ptr_->contact, NConMax(ptr_), &ptr_->ncon, owner_)), + +#define X(dtype, var, dim0, dim1) var(InitPyArray(ptr_->var, owner_)), + MJDATA_VECTOR +#undef X + model_(model), + model_ref_(py::cast(model_)), + indexer_(ptr_, model_->get(), owner_) { + bool is_newly_inserted = false; + { + py::gil_scoped_acquire gil; + is_newly_inserted = MjDataRawPointerMap().insert({ptr_, this}).second; + } + if (!is_newly_inserted) { + throw UnexpectedError( + "MjDataRawPointerMap already contains this raw mjData*"); + } + + // install default timer if not already installed + { + py::gil_scoped_acquire gil; + if (!mjcb_time) { + mjcb_time = GetTime; + } + } +} + +MjDataWrapper::MjWrapper(const MjDataWrapper& other) + : WrapperBase(other.Copy(), &MjDataCapsuleDestructor), +#undef MJ_M +#define MJ_M(x) other.model_->get()->x +#define X(dtype, var, dim0, dim1) \ + var(InitPyArray(X_ARRAY_SHAPE(other.model_->get()->dim0, dim1), ptr_->var, \ + owner_)), + MJDATA_POINTERS +#undef MJ_M +#define MJ_M(x) (x) +#undef X + + contact(MjContactList(ptr_->contact, NConMax(ptr_), &ptr_->ncon, owner_)), + +#define X(dtype, var, dim0, dim1) var(InitPyArray(ptr_->var, owner_)), + MJDATA_VECTOR +#undef X + model_(other.model_), + model_ref_(other.model_ref_), + indexer_(ptr_, model_->get(), owner_) { + bool is_newly_inserted = false; + { + py::gil_scoped_acquire gil; + is_newly_inserted = MjDataRawPointerMap().insert({ptr_, this}).second; + } + if (!is_newly_inserted) { + throw UnexpectedError( + "MjDataRawPointerMap already contains this raw mjData*"); + } +} + +MjDataWrapper::MjWrapper(MjDataWrapper&& other) + : WrapperBase(other.ptr_, other.owner_), +#undef MJ_M +#define MJ_M(x) other.model_->get()->x +#define X(dtype, var, dim0, dim1) \ + var(InitPyArray(X_ARRAY_SHAPE(other.model_->get()->dim0, dim1), ptr_->var, \ + owner_)), + MJDATA_POINTERS +#undef MJ_M +#define MJ_M(x) (x) +#undef X + + contact(MjContactList(ptr_->contact, NConMax(ptr_), &ptr_->ncon, owner_)), + +#define X(dtype, var, dim0, dim1) var(InitPyArray(ptr_->var, owner_)), + MJDATA_VECTOR +#undef X + model_(other.model_), + model_ref_(std::move(other.model_ref_)), + indexer_(ptr_, model_->get(), owner_) { + bool is_newly_inserted = false; + { + py::gil_scoped_acquire gil; + is_newly_inserted = + MjDataRawPointerMap().insert_or_assign(ptr_, this).second; + } + if (is_newly_inserted) { + throw UnexpectedError( + "MjDataRawPointerMap does not contains the moved-from mjData*"); + } + other.ptr_ = nullptr; +} + +MjDataWrapper::MjWrapper(const MjDataWrapper& other, MjModelWrapper* model) + : WrapperBase(other.Copy(), &MjDataCapsuleDestructor), +#undef MJ_M +#define MJ_M(x) other.model_->get()->x +#define X(dtype, var, dim0, dim1) \ + var(InitPyArray(X_ARRAY_SHAPE(other.model_->get()->dim0, dim1), ptr_->var, \ + owner_)), + MJDATA_POINTERS +#undef MJ_M +#define MJ_M(x) (x) +#undef X + + contact(MjContactList(ptr_->contact, NConMax(ptr_), &ptr_->ncon, owner_)), + +#define X(dtype, var, dim0, dim1) var(InitPyArray(ptr_->var, owner_)), + MJDATA_VECTOR +#undef X + model_(model), + model_ref_(py::cast(model_)), + indexer_(ptr_, model_->get(), owner_) { + bool is_newly_inserted = false; + { + py::gil_scoped_acquire gil; + is_newly_inserted = MjDataRawPointerMap().insert({ptr_, this}).second; + } + if (!is_newly_inserted) { + throw UnexpectedError( + "MjDataRawPointerMap already contains this raw mjData*"); + } +} + +MjDataWrapper::MjWrapper(MjModelWrapper* model, raw::MjData* d) + : WrapperBase(d, &MjDataCapsuleDestructor), +#undef MJ_M +#define MJ_M(x) model->get()->x +#define X(dtype, var, dim0, dim1) \ + var(InitPyArray(X_ARRAY_SHAPE(model->get()->dim0, dim1), ptr_->var, owner_)), + MJDATA_POINTERS +#undef MJ_M +#define MJ_M(x) (x) +#undef X + + contact(MjContactList(ptr_->contact, NConMax(ptr_), &ptr_->ncon, owner_)), + +#define X(dtype, var, dim0, dim1) var(InitPyArray(ptr_->var, owner_)), + MJDATA_VECTOR +#undef X + model_(model), + model_ref_(py::cast(model_)), + indexer_(ptr_, model_->get(), owner_) { + bool is_newly_inserted = false; + { + py::gil_scoped_acquire gil; + is_newly_inserted = MjDataRawPointerMap().insert({ptr_, this}).second; + } + if (!is_newly_inserted) { + throw UnexpectedError( + "MjDataRawPointerMap already contains this raw mjData*"); + } +} + +MjDataWrapper::~MjWrapper() { + if (ptr_) { + bool erased = false; + { + py::gil_scoped_acquire gil; + erased = MjDataRawPointerMap().erase(ptr_); + } + if (!erased) { + std::cerr << "MjDataRawPointerMap does not contain this raw mjData*" + << std::endl; + std::terminate(); + } + } +} + +void MjDataWrapper::Serialize(std::ostream& output) const { + // TODO: Replace this custom serialization with a protobuf + WriteChar(output, kSerializationVersion); + + model_->Serialize(output); + + // Write struct and scalar fields +#define X(var) WriteBytes(output, &ptr_->var, sizeof(ptr_->var)) + X(maxuse_stack); + X(maxuse_arena); + X(maxuse_con); + X(maxuse_efc); + X(solver); + X(timer); + X(warning); + X(ncon); + X(ne); + X(nf); + X(nJ); + X(nA); + X(nefc); + X(nisland); + X(time); + X(energy); +#undef X + + // Write buffer and arena contents + { + MJDATA_POINTERS_PREAMBLE((this->model_->get())) + +#define X(type, name, nr, nc) \ + WriteBytes(output, ptr_->name, \ + sizeof(type) * (this->model_->get()->nr) * (nc)); + MJDATA_POINTERS +#undef X + +#undef MJ_M +#define MJ_M(x) this->model_->get()->x +#undef MJ_D +#define MJ_D(x) this->ptr_->x +#define X(type, name, nr, nc) \ + if ((nr) * (nc)) { \ + WriteBytes(output, ptr_->name, \ + ptr_->name ? sizeof(type) * (nr) * (nc) : 0); \ + } + + MJDATA_ARENA_POINTERS_CONTACT + MJDATA_ARENA_POINTERS_SOLVER + if (mj_isDual(this->model_->get())) { + MJDATA_ARENA_POINTERS_DUAL + } + if (this->ptr_->nisland) { + MJDATA_ARENA_POINTERS_ISLAND + } +#undef MJ_M +#define MJ_M(x) x +#undef MJ_D +#define MJ_D(x) x +#undef X + } +} + +MjDataWrapper MjDataWrapper::Deserialize(std::istream& input) { + char serializationVersion = ReadChar(input); + CheckInput(input, "mjData"); + + if (serializationVersion != kSerializationVersion) { + throw py::value_error("Incompatible serialization version."); + } + + // Read the model that was used to create the mjData. + std::unique_ptr m_wrapper = + MjModelWrapper::Deserialize(input); + raw::MjModel& m = *m_wrapper->get(); + + bool is_dual = mj_isDual(&m); + + raw::MjData* d = mj_makeData(&m); + if (!d) { + throw py::value_error("Failed to create mjData."); + } + + // Read structs and scalar fields +#define X(var) \ + ReadBytes(input, (void*)&d->var, sizeof(d->var)); \ + CheckInput(input, "mjData"); + + X(maxuse_stack); + X(maxuse_arena); + X(maxuse_con); + X(maxuse_efc); + X(solver); + X(timer); + X(warning); + X(ncon); + X(ne); + X(nf); + X(nJ); + X(nA); + X(nefc); + X(nisland); + X(time); + X(energy); +#undef X + + // Read buffer and arena contents + { + MJDATA_POINTERS_PREAMBLE((&m)) + +#define X(type, name, nr, nc) \ + ReadBytes(input, d->name, sizeof(type) * (m.nr) * (nc)); + MJDATA_POINTERS +#undef X + +#undef MJ_M +#define MJ_M(x) m.x +#undef MJ_D +#define MJ_D(x) d->x +// arena pointers might be null, so we need to check the size before allocating. +#define X(type, name, nr, nc) \ + if ((nr) * (nc)) { \ + std::size_t actual_nbytes = ReadInt(input); \ + if (actual_nbytes) { \ + if (actual_nbytes != sizeof(type) * (nr) * (nc)) { \ + input.setstate(input.rdstate() | std::ios_base::failbit); \ + } else { \ + d->name = static_castname)>( \ + mj_arenaAllocByte(d, sizeof(type) * (nr) * (nc), alignof(type))); \ + input.read(reinterpret_cast(d->name), actual_nbytes); \ + } \ + } else { \ + d->name = nullptr; \ + } \ + } + + MJDATA_ARENA_POINTERS_CONTACT + MJDATA_ARENA_POINTERS_SOLVER + if (is_dual) { + MJDATA_ARENA_POINTERS_DUAL + } + if (d->nisland) { + MJDATA_ARENA_POINTERS_ISLAND + } +#undef MJ_M +#define MJ_M(x) x +#undef MJ_D +#define MJ_D(x) x +#undef X + } + CheckInput(input, "mjData"); + + // All bytes should have been used. + input.ignore(1); + if (!input.eof()) { + throw py::value_error("Invalid serialized mjData."); + } + + return MjDataWrapper(m_wrapper.release(), d); +} + +raw::MjData* MjDataWrapper::Copy() const { + const raw::MjModel* m = model_->get(); + return InterceptMjErrors(mj_copyData)(NULL, m, this->ptr_); +} + +// ==================== MJSTATISTIC ============================================ +#define X(var) var(InitPyArray(ptr_->var, owner_)) +MjStatisticWrapper::MjWrapper() + : WrapperBase(new raw::MjStatistic{}), X(center) {} + +MjStatisticWrapper::MjWrapper(raw::MjStatistic* ptr, py::handle owner) + : WrapperBase(ptr, owner), X(center) {} +#undef X + +MjStatisticWrapper::MjWrapper(const MjStatisticWrapper& other) + : MjStatisticWrapper() { + *this->ptr_ = *other.ptr_; +} + +// ==================== MJWARNINGSTAT ========================================== +MjWarningStatWrapper::MjWrapper() : WrapperBase(new raw::MjWarningStat{}) {} + +MjWarningStatWrapper::MjWrapper(raw::MjWarningStat* ptr, py::handle owner) + : WrapperBase(ptr, owner) {} + +MjWarningStatWrapper::MjWrapper(const MjWarningStatWrapper& other) + : MjWarningStatWrapper() { + *this->ptr_ = *other.ptr_; +} + +#define X(type, var) \ + var(std::vector{num}, std::vector{sizeof(raw::MjWarningStat)}, \ + &ptr->var, owner) +MjWarningStatList::MjStructList(raw::MjWarningStat* ptr, int num, + py::handle owner) + : StructListBase(ptr, num, owner), X(int, lastinfo), X(int, number) {} +#undef X + +// Slicing +#define X(type, var) var(other.var[slice]) +MjWarningStatList::MjStructList(MjWarningStatList& other, py::slice slice) + : StructListBase(other, slice), X(int, lastinfo), X(int, number) {} +#undef X + +// ==================== MJTIMERSTAT ============================================ +MjTimerStatWrapper::MjWrapper() : WrapperBase(new raw::MjTimerStat{}) {} + +MjTimerStatWrapper::MjWrapper(raw::MjTimerStat* ptr, py::handle owner) + : WrapperBase(ptr, owner) {} + +MjTimerStatWrapper::MjWrapper(const MjTimerStatWrapper& other) + : MjTimerStatWrapper() { + *this->ptr_ = *other.ptr_; +} + +#define X(type, var) \ + var(std::vector{num}, std::vector{sizeof(raw::MjTimerStat)}, \ + &ptr->var, owner) +MjTimerStatList::MjStructList(raw::MjTimerStat* ptr, int num, py::handle owner) + : StructListBase(ptr, num, owner), X(mjtNum, duration), X(int, number) {} +#undef X + +// Slicing +#define X(type, var) var(other.var[slice]) +MjTimerStatList::MjStructList(MjTimerStatList& other, py::slice slice) + : StructListBase(other, slice), X(mjtNum, duration), X(int, number) {} +#undef X + +// ==================== MJSOLVERSTAT =========================================== +MjSolverStatWrapper::MjWrapper() : WrapperBase(new raw::MjSolverStat{}) {} + +MjSolverStatWrapper::MjWrapper(raw::MjSolverStat* ptr, py::handle owner) + : WrapperBase(ptr, owner) {} + +MjSolverStatWrapper::MjWrapper(const MjSolverStatWrapper& other) + : MjSolverStatWrapper() { + *this->ptr_ = *other.ptr_; +} + +#define X(type, var) \ + var(std::vector{num}, std::vector{sizeof(raw::MjSolverStat)}, \ + &ptr->var, owner) +MjSolverStatList::MjStructList(raw::MjSolverStat* ptr, int num, + py::handle owner) + : StructListBase(ptr, num, owner), + X(mjtNum, improvement), + X(mjtNum, gradient), + X(mjtNum, lineslope), + X(int, nactive), + X(int, nchange), + X(int, neval), + X(int, nupdate) {} +#undef X +#undef XN + +// Slicing +#define X(type, var) var(other.var[slice]) +MjSolverStatList::MjStructList(MjSolverStatList& other, py::slice slice) + : StructListBase(other, slice), + X(mjtNum, improvement), + X(mjtNum, gradient), + X(mjtNum, lineslope), + X(int, nactive), + X(int, nchange), + X(int, neval), + X(int, nupdate) {} +#undef X + +// ==================== MJVPERTURB ============================================= +#define X(var) var(InitPyArray(ptr_->var, owner_)) +MjvPerturbWrapper::MjWrapper() + : WrapperBase([]() { + raw::MjvPerturb* const pert = new raw::MjvPerturb; + mjv_defaultPerturb(pert); + return pert; + }()), + X(refpos), + X(refquat), + X(refselpos), + X(localpos) {} +#undef X + +MjvPerturbWrapper::MjWrapper(const MjvPerturbWrapper& other) + : MjvPerturbWrapper() { + *this->ptr_ = *other.ptr_; +} + +// ==================== MJVCAMERA ============================================== +#define X(var) var(InitPyArray(ptr_->var, owner_)) +MjvCameraWrapper::MjWrapper() + : WrapperBase([]() { + raw::MjvCamera* const cam = new raw::MjvCamera; + mjv_defaultCamera(cam); + return cam; + }()), + X(lookat) {} +#undef X + +MjvCameraWrapper::MjWrapper(const MjvCameraWrapper& other) + : MjvCameraWrapper() { + *this->ptr_ = *other.ptr_; +} + +// ==================== MJVGLCAMERA ============================================ +#define X(var) var(InitPyArray(ptr_->var, owner_)) +MjvGLCameraWrapper::MjWrapper() + : WrapperBase(new raw::MjvGLCamera{}), X(pos), X(forward), X(up) {} + +MjvGLCameraWrapper::MjWrapper(raw::MjvGLCamera* ptr, py::handle owner) + : WrapperBase(ptr, owner), X(pos), X(forward), X(up) {} +#undef X + +MjvGLCameraWrapper::MjWrapper(raw::MjvGLCamera&& other) : MjvGLCameraWrapper() { + *this->ptr_ = other; +} + +MjvGLCameraWrapper::MjWrapper(const MjvGLCameraWrapper& other) + : MjvGLCameraWrapper() { + *this->ptr_ = *other.ptr_; +} + +// ==================== MJVGEOM ================================================ +#define X(var) var(InitPyArray(ptr_->var, owner_)) +MjvGeomWrapper::MjWrapper() + : WrapperBase(new raw::MjvGeom{}), + X(size), + X(pos), + mat([this]() { + static_assert(sizeof(ptr_->mat) == sizeof(ptr_->mat[0]) * 9); + return InitPyArray(std::array{3, 3}, ptr_->mat, owner_); + }()), + X(rgba) { + mjv_initGeom(ptr_, mjGEOM_NONE, nullptr, nullptr, nullptr, nullptr); +} + +MjvGeomWrapper::MjWrapper(raw::MjvGeom* ptr, py::handle owner) + : WrapperBase(ptr, owner), + X(size), + X(pos), + mat([this]() { + static_assert(sizeof(ptr_->mat) == sizeof(ptr_->mat[0]) * 9); + return InitPyArray(std::array{3, 3}, ptr_->mat, owner_); + }()), + X(rgba) {} +#undef X + +MjvGeomWrapper::MjWrapper(const MjvGeomWrapper& other) : MjvGeomWrapper() { + *this->ptr_ = *other.ptr_; +} + +// ==================== MJVLIGHT =============================================== +#define X(var) var(InitPyArray(ptr_->var, owner_)) +MjvLightWrapper::MjWrapper() + : WrapperBase(new raw::MjvLight{}), + X(pos), + X(dir), + X(attenuation), + X(ambient), + X(diffuse), + X(specular) {} + +MjvLightWrapper::MjWrapper(raw::MjvLight* ptr, py::handle owner) + : WrapperBase(ptr, owner), + X(pos), + X(dir), + X(attenuation), + X(ambient), + X(diffuse), + X(specular) {} +#undef X + +MjvLightWrapper::MjWrapper(const MjvLightWrapper& other) : MjvLightWrapper() { + *this->ptr_ = *other.ptr_; +} + +// ==================== MJVOPTION ============================================== +#define X(var) var(InitPyArray(ptr_->var, owner_)) +MjvOptionWrapper::MjWrapper() + : WrapperBase([]() { + raw::MjvOption* const opt = new raw::MjvOption; + mjv_defaultOption(opt); + return opt; + }()), + X(geomgroup), + X(sitegroup), + X(jointgroup), + X(tendongroup), + X(actuatorgroup), + X(flexgroup), + X(skingroup), + X(flags) {} +#undef X + +MjvOptionWrapper::MjWrapper(const MjvOptionWrapper& other) + : MjvOptionWrapper() { + *this->ptr_ = *other.ptr_; +} + +// ==================== MJVSCENE =============================================== +static void MjvSceneCapsuleDestructor(PyObject* pyobj) { + py::gil_scoped_acquire gil; + auto* scn = static_cast(PyCapsule_GetPointer(pyobj, nullptr)); + if (scn) { + mjv_freeScene(scn); + delete scn; + } +} + +#define X(var) var(InitPyArray(ptr_->var, owner_)) +#define XN(var, n) var(InitPyArray(std::array{n}, ptr_->var, owner_)) +MjvSceneWrapper::MjWrapper() + : WrapperBase( + []() { + raw::MjvScene* const scn = new raw::MjvScene; + mjv_defaultScene(scn); + InterceptMjErrors(mjv_makeScene)(nullptr, scn, 0); + return scn; + }(), + &MjvSceneCapsuleDestructor), + nskinvert(0), + XN(geoms, 0), + XN(geomorder, 0), + XN(flexedgeadr, 0), + XN(flexedgenum, 0), + XN(flexvertadr, 0), + XN(flexvertnum, 0), + XN(flexfaceadr, 0), + XN(flexfacenum, 0), + XN(flexfaceused, 0), + XN(flexedge, 0), + XN(flexvert, 0), + XN(flexface, 0), + XN(flexnormal, 0), + XN(flextexcoord, 0), + XN(skinfacenum, 0), + XN(skinvertadr, 0), + XN(skinvertnum, 0), + XN(skinvert, 0), + XN(skinnormal, 0), + X(lights), + X(camera), + X(translate), + X(rotate), + X(flags), + X(framergb) {} + +#define XN(var, n) var(InitPyArray(std::array{n}, ptr_->var, owner_)) +MjvSceneWrapper::MjWrapper(const MjModelWrapper& model, int maxgeom) + : WrapperBase( + [maxgeom](const raw::MjModel* m) { + raw::MjvScene* const scn = new raw::MjvScene; + mjv_defaultScene(scn); + InterceptMjErrors(mjv_makeScene)(m, scn, maxgeom); + return scn; + }(model.get()), + &MjvSceneCapsuleDestructor), + nskinvert([](const raw::MjModel* m) { + int nskinvert = 0; + for (int i = 0; i < m->nskin; ++i) { + nskinvert += m->skin_vertnum[i]; + } + return nskinvert; + }(model.get())), + nflexface([](const raw::MjModel* m) { + int nflexface = 0; + int flexfacenum = 0; + for (int f = 0; f < m->nflex; f++) { + if (m->flex_dim[f] == 0) { + // 1D : 0 + flexfacenum = 0; + } else if (m->flex_dim[f] == 2) { + // 2D: 2*fragments + 2*elements + flexfacenum = 2 * m->flex_shellnum[f] + 2 * m->flex_elemnum[f]; + } else { + // 3D: max(fragments, 4*maxlayer) + // find number of elements in biggest layer + int maxlayer = 0, layer = 0, nlayer = 1; + while (nlayer) { + nlayer = 0; + for (int e = 0; e < m->flex_elemnum[f]; e++) { + if (m->flex_elemlayer[m->flex_elemadr[f] + e] == layer) { + nlayer++; + } + } + maxlayer = mjMAX(maxlayer, nlayer); + layer++; + } + flexfacenum = mjMAX(m->flex_shellnum[f], 4 * maxlayer); + } + + // accumulate over flexes + nflexface += flexfacenum; + } + return nflexface; + }(model.get())), + nflexedge(model.get()->nflexedge), + nflexvert(model.get()->nflexvert), + XN(geoms, ptr_->maxgeom), + XN(geomorder, ptr_->maxgeom), + XN(flexedgeadr, ptr_->nflex), + XN(flexedgenum, ptr_->nflex), + XN(flexvertadr, ptr_->nflex), + XN(flexvertnum, ptr_->nflex), + XN(flexfaceadr, ptr_->nflex), + XN(flexfacenum, ptr_->nflex), + XN(flexfaceused, ptr_->nflex), + XN(flexedge, 2 * nflexedge), + XN(flexvert, 3 * nflexvert), + XN(flexface, 9 * nflexface), + XN(flexnormal, 9 * nflexface), + XN(flextexcoord, 6 * nflexface), + XN(skinfacenum, ptr_->nskin), + XN(skinvertadr, ptr_->nskin), + XN(skinvertnum, ptr_->nskin), + XN(skinvert, 3 * nskinvert), + XN(skinnormal, 3 * nskinvert), + X(lights), + X(camera), + X(translate), + X(rotate), + X(flags), + X(framergb) {} +#undef X +#undef XN + +template +static T* MallocAndCopy(const T* src, int count) { + if (src) { + T* out = static_cast(mju_malloc(count * sizeof(T))); + std::memcpy(out, src, count * sizeof(T)); + return out; + } else { + return nullptr; + } +} + +MjvSceneWrapper::MjWrapper(const MjvSceneWrapper& other) : MjvSceneWrapper() { + mjv_freeScene(ptr_); + *ptr_ = *other.ptr_; + +#define XN(var, n) \ + ptr_->var = MallocAndCopy(other.ptr_->var, n); \ + var = InitPyArray(std::array{n}, ptr_->var, owner_); + + XN(geoms, ptr_->ngeom); + XN(geomorder, ptr_->ngeom); + XN(flexedgeadr, ptr_->nflex); + XN(flexedgenum, ptr_->nflex); + XN(flexvertadr, ptr_->nflex); + XN(flexvertnum, ptr_->nflex); + XN(flexfaceadr, ptr_->nflex); + XN(flexfacenum, ptr_->nflex); + XN(flexfaceused, ptr_->nflex); + XN(flexedge, 2 * nflexedge); + XN(flexvert, 3 * nflexvert); + XN(flexface, 9 * nflexface); + XN(flexnormal, 9 * nflexface); + XN(flextexcoord, 6 * nflexface); + XN(skinfacenum, ptr_->nskin); + XN(skinvertadr, ptr_->nskin); + XN(skinvertnum, ptr_->nskin); + XN(skinvert, 3 * nskinvert); + XN(skinnormal, 3 * nskinvert); + +#undef XN +} + +// ==================== MJVFIGURE ============================================== +#define X(var) var(InitPyArray(ptr_->var, owner_)) +MjvFigureWrapper::MjWrapper() + : WrapperBase([]() { + raw::MjvFigure* const fig = new raw::MjvFigure; + mjv_defaultFigure(fig); + return fig; + }()), + X(flg_ticklabel), + X(gridsize), + X(gridrgb), + X(figurergba), + X(panergba), + X(legendrgba), + X(textrgb), + X(linergb), + X(range), + X(highlight), + X(linepnt), + X(linedata), + X(xaxispixel), + X(yaxispixel), + X(xaxisdata), + X(yaxisdata), +#undef X + + linename([](raw::MjvFigure* ptr, py::handle owner) { +// Use a macro to help us static_assert that the array extents here are kept +// in sync with mjVisualize.h. +#define MAKE_STR_ARRAY(N1, N2) \ + static_assert( \ + std::is_same_v); \ + return py::array(py::dtype("|S" #N2), N1, ptr->linename, owner); + MAKE_STR_ARRAY(mjMAXLINE, 100); + +#undef MAKE_STR_ARRAY + }(ptr_, owner_)) { +} + +MjvFigureWrapper::MjWrapper(const MjvFigureWrapper& other) + : MjvFigureWrapper() { + *this->ptr_ = *other.ptr_; +} + +} // namespace mujoco::python::_impl From 3429a4881a3062d16a97e097c9f4650cde5ea1c0 Mon Sep 17 00:00:00 2001 From: Mohammad Hamid Date: Thu, 10 Apr 2025 04:14:34 -0700 Subject: [PATCH 055/191] Detect mismatch between maxNodes and Nodes in a gmsh block. Fixes #2342 PiperOrigin-RevId: 745967745 Change-Id: If1b5fcd71079a0e6a6def9b442758678e31a5828 --- src/user/user_flexcomp.cc | 5 ++ ...h_between_max_nodes_and_nodes_in_block.msh | 67 +++++++++++++++++++ ...h_between_max_nodes_and_nodes_in_block.xml | 27 ++++++++ test/user/user_flex_test.cc | 12 ++++ 4 files changed, 111 insertions(+) create mode 100644 test/user/testdata/malformed_cube_41_ascii_mismatch_between_max_nodes_and_nodes_in_block.msh create mode 100644 test/user/testdata/malformed_cube_41_ascii_mismatch_between_max_nodes_and_nodes_in_block.xml diff --git a/src/user/user_flexcomp.cc b/src/user/user_flexcomp.cc index 50c12d68..1d0dbece 100644 --- a/src/user/user_flexcomp.cc +++ b/src/user/user_flexcomp.cc @@ -1167,6 +1167,11 @@ void mjCFlexcomp::LoadGMSH41(char* buffer, int binary, int nodeend, throw mjCError(NULL, "All nodes must be in single block"); } + // require maximum number of nodes be equal to maximum number of nodes in a block + if (maxNodeTag != numNodesInBlock){ + throw mjCError(NULL, "Maximum number of nodes must be equal to number of nodes in a block"); + } + // check dimensionality and save if (entityDim < 1 || entityDim > 3) { throw mjCError(NULL, "Entity must be 1D, 2D or 3D"); diff --git a/test/user/testdata/malformed_cube_41_ascii_mismatch_between_max_nodes_and_nodes_in_block.msh b/test/user/testdata/malformed_cube_41_ascii_mismatch_between_max_nodes_and_nodes_in_block.msh new file mode 100644 index 00000000..7f88379f --- /dev/null +++ b/test/user/testdata/malformed_cube_41_ascii_mismatch_between_max_nodes_and_nodes_in_block.msh @@ -0,0 +1,67 @@ +$MeshFormat +4.1 0 8 +$EndMeshFormat +$Entities +0 0 0 1 +1 -0.5 -0.5 0 0.5 0.5 1 0 0 +$EndEntities +$Nodes +1 14 1 100 +3 1 0 14 +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +-0.5 -0.5 0 +0.5 -0.5 0 +0.5 0.5 0 +-0.5 0.5 0 +-0.5 -0.5 1 +0.5 -0.5 1 +0.5 0.5 1 +-0.5 0.5 1 +0 0 0 +0 -0.5 0.5 +0.5 0 0.5 +0 0.5 0.5 +-0.5 0 0.5 +0 0 1 +$EndNodes +$Elements +1 24 1 24 +3 1 4 24 +1 10 9 12 11 +2 10 12 13 14 +3 9 10 12 13 +4 10 12 14 11 +5 9 10 1 2 +6 7 11 12 3 +7 4 1 9 13 +8 13 12 4 8 +9 1 10 13 5 +10 13 8 5 14 +11 12 8 14 7 +12 14 5 10 6 +13 14 6 11 7 +14 9 4 12 3 +15 3 11 9 2 +16 11 6 10 2 +17 6 10 14 11 +18 1 10 9 13 +19 12 4 9 13 +20 5 10 13 14 +21 8 13 12 14 +22 12 14 11 7 +23 9 12 11 3 +24 10 9 11 2 +$EndElements diff --git a/test/user/testdata/malformed_cube_41_ascii_mismatch_between_max_nodes_and_nodes_in_block.xml b/test/user/testdata/malformed_cube_41_ascii_mismatch_between_max_nodes_and_nodes_in_block.xml new file mode 100644 index 00000000..1e179a81 --- /dev/null +++ b/test/user/testdata/malformed_cube_41_ascii_mismatch_between_max_nodes_and_nodes_in_block.xml @@ -0,0 +1,27 @@ + + diff --git a/test/user/user_flex_test.cc b/test/user/user_flex_test.cc index 67dfb8f6..a43b93f0 100644 --- a/test/user/user_flex_test.cc +++ b/test/user/user_flex_test.cc @@ -697,6 +697,18 @@ TEST_F(UserFlexTest, LoadMSHASCII_41_MissingElement_Fail) { mj_deleteModel(m); } +TEST_F(UserFlexTest, + LoadMSHASCII_41_MismatchBetweenMaxNodesAndNodesInBlock_Fail) { + const std::string xml_path = + GetTestDataFilePath( + "user/testdata/malformed_cube_41_ascii_mismatch_between_max_nodes_and_nodes_in_block.xml"); + std::array error; + mjModel* m = mj_loadXML(xml_path.c_str(), 0, error.data(), error.size()); + EXPECT_THAT(error.data(), HasSubstr( + "XML Error: Error: Maximum number of nodes must be equal to number of nodes in a block\nElement 'flexcomp', line 22\n")); + mj_deleteModel(m); +} + TEST_F(UserFlexTest, LoadMSHASCII_22_MissingNumNodes_Fail) { const std::string xml_path = GetTestDataFilePath( From d3664b5d0192ed81b8a62c458a6e29c63b9a0c72 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Thu, 10 Apr 2025 04:40:21 -0700 Subject: [PATCH 056/191] Avoid square root in mju_makeFrame PiperOrigin-RevId: 745974380 Change-Id: I90879fac05e62daf60acd2223f814e833634b242 --- src/engine/engine_util_spatial.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/engine_util_spatial.c b/src/engine/engine_util_spatial.c index c170d923..dda47cf6 100644 --- a/src/engine/engine_util_spatial.c +++ b/src/engine/engine_util_spatial.c @@ -532,7 +532,7 @@ void mju_makeFrame(mjtNum frame[9]) { } // if yaxis undefined, set yaxis to (0,1,0) if possible, otherwise (0,0,1) - if (mju_norm3(frame+3) < 0.5) { + if (mju_dot3(frame+3, frame+3) < 0.25) { mju_zero3(frame+3); if (frame[1] < 0.5 && frame[1] > -0.5) { From 51c489fc3025661f33d5e2e25b13e42a872cb53a Mon Sep 17 00:00:00 2001 From: Taylor Howell Date: Thu, 10 Apr 2025 06:07:23 -0700 Subject: [PATCH 057/191] Add inverse dynamics to MJX. PiperOrigin-RevId: 745998273 Change-Id: I203af89332ace5d7a60fff3ec6ff4cc88c02340d --- doc/changelog.rst | 8 ++ doc/mjx.rst | 4 +- mjx/mujoco/mjx/__init__.py | 2 + mjx/mujoco/mjx/_src/derivative.py | 59 +++++++++++++ mjx/mujoco/mjx/_src/forward.py | 25 +----- mjx/mujoco/mjx/_src/inverse.py | 106 ++++++++++++++++++++++ mjx/mujoco/mjx/_src/inverse_test.py | 131 ++++++++++++++++++++++++++++ mjx/mujoco/mjx/_src/io.py | 5 +- mjx/mujoco/mjx/_src/smooth.py | 17 +++- mjx/mujoco/mjx/_src/smooth_test.py | 7 ++ mjx/mujoco/mjx/_src/solver.py | 24 ++--- mjx/mujoco/mjx/_src/solver_test.py | 2 +- mjx/mujoco/mjx/_src/types.py | 11 +++ 13 files changed, 357 insertions(+), 44 deletions(-) create mode 100644 mjx/mujoco/mjx/_src/derivative.py create mode 100644 mjx/mujoco/mjx/_src/inverse.py create mode 100644 mjx/mujoco/mjx/_src/inverse_test.py diff --git a/doc/changelog.rst b/doc/changelog.rst index fa30391f..d094094e 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -1,6 +1,14 @@ ========= Changelog ========= + +Upcoming version (not yet release) +---------------------------------- + +MJX +^^^ +- Added inverse dynamics. + Version 3.3.1 (Apr 9, 2025) ---------------------------- diff --git a/doc/mjx.rst b/doc/mjx.rst index 98fb36b3..0c00525f 100644 --- a/doc/mjx.rst +++ b/doc/mjx.rst @@ -235,6 +235,8 @@ The following features are **fully supported** in MJX: - 1, 3, 4, 6 (1 is not supported with ``ELLIPTIC``) * - :ref:`Solver ` - ``CG``, ``NEWTON`` + * - Dynamics + - :ref:`Inverse ` * - Fluid Model - :ref:`flInertia` * - :ref:`Tendons ` @@ -262,8 +264,6 @@ The following features are **in development** and coming soon: (``BOX``, ``MESH``, ``HFIELD``) and ``ELLIPSOID``. * - :ref:`Integrator ` - ``IMPLICIT`` - * - Dynamics - - :ref:`Inverse ` * - Fluid Model - :ref:`flEllipsoid` * - :ref:`Sensors ` diff --git a/mjx/mujoco/mjx/__init__.py b/mjx/mujoco/mjx/__init__.py index 6a031b57..c60c785c 100644 --- a/mjx/mujoco/mjx/__init__.py +++ b/mjx/mujoco/mjx/__init__.py @@ -17,6 +17,7 @@ # pylint:disable=g-importing-member from mujoco.mjx._src.collision_driver import collision from mujoco.mjx._src.constraint import make_constraint +from mujoco.mjx._src.derivative import deriv_smooth_vel from mujoco.mjx._src.forward import euler from mujoco.mjx._src.forward import forward from mujoco.mjx._src.forward import fwd_acceleration @@ -26,6 +27,7 @@ from mujoco.mjx._src.forward import fwd_velocity from mujoco.mjx._src.forward import implicit from mujoco.mjx._src.forward import rungekutta4 from mujoco.mjx._src.forward import step +from mujoco.mjx._src.inverse import inverse from mujoco.mjx._src.io import get_data from mujoco.mjx._src.io import get_data_into from mujoco.mjx._src.io import make_data diff --git a/mjx/mujoco/mjx/_src/derivative.py b/mjx/mujoco/mjx/_src/derivative.py new file mode 100644 index 00000000..b0fc65f7 --- /dev/null +++ b/mjx/mujoco/mjx/_src/derivative.py @@ -0,0 +1,59 @@ +# Copyright 2025 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. +# ============================================================================== +"""Derivative functions.""" + +from typing import Optional + +import jax +from jax import numpy as jp +# pylint: disable=g-importing-member +from mujoco.mjx._src.types import BiasType +from mujoco.mjx._src.types import Data +from mujoco.mjx._src.types import DisableBit +from mujoco.mjx._src.types import DynType +from mujoco.mjx._src.types import GainType +from mujoco.mjx._src.types import Model + + +def deriv_smooth_vel(m: Model, d: Data) -> Optional[jax.Array]: + """Analytical derivative of smooth forces w.r.t velocities.""" + + qderiv = None + + # qDeriv += d qfrc_actuator / d qvel + if not m.opt.disableflags & DisableBit.ACTUATION: + affine_bias = m.actuator_biastype == BiasType.AFFINE + bias_vel = m.actuator_biasprm[:, 2] * affine_bias + affine_gain = m.actuator_gaintype == GainType.AFFINE + gain_vel = m.actuator_gainprm[:, 2] * affine_gain + ctrl = d.ctrl.at[m.actuator_dyntype != DynType.NONE].set(d.act) + vel = bias_vel + gain_vel * ctrl + qderiv = d.actuator_moment.T @ jax.vmap(jp.multiply)(d.actuator_moment, vel) + + # qDeriv += d qfrc_passive / d qvel + if not m.opt.disableflags & DisableBit.PASSIVE: + if qderiv is None: + qderiv = -jp.diag(m.dof_damping) + else: + qderiv -= jp.diag(m.dof_damping) + if m.ntendon: + qderiv -= d.ten_J.T @ jp.diag(m.tendon_damping) @ d.ten_J + # TODO(robotics-simulation): fluid drag model + if m.opt.has_fluid_params: + raise NotImplementedError('fluid drag not supported for implicitfast') + + # TODO(team): rne derivative + + return qderiv diff --git a/mjx/mujoco/mjx/_src/forward.py b/mjx/mujoco/mjx/_src/forward.py index 6ad1ae71..967aa639 100644 --- a/mjx/mujoco/mjx/_src/forward.py +++ b/mjx/mujoco/mjx/_src/forward.py @@ -22,6 +22,7 @@ from jax import numpy as jp import mujoco from mujoco.mjx._src import collision_driver from mujoco.mjx._src import constraint +from mujoco.mjx._src import derivative from mujoco.mjx._src import math from mujoco.mjx._src import passive from mujoco.mjx._src import scan @@ -392,29 +393,7 @@ def rungekutta4(m: Model, d: Data) -> Data: def implicit(m: Model, d: Data) -> Data: """Integrates fully implicit in velocity.""" - qderiv = None - - # qDeriv += d qfrc_actuator / d qvel - if not m.opt.disableflags & DisableBit.ACTUATION: - affine_bias = m.actuator_biastype == BiasType.AFFINE - bias_vel = m.actuator_biasprm[:, 2] * affine_bias - affine_gain = m.actuator_gaintype == GainType.AFFINE - gain_vel = m.actuator_gainprm[:, 2] * affine_gain - ctrl = d.ctrl.at[m.actuator_dyntype != DynType.NONE].set(d.act) - vel = bias_vel + gain_vel * ctrl - qderiv = d.actuator_moment.T @ jp.diag(vel) @ d.actuator_moment - - # qDeriv += d qfrc_passive / d qvel - if not m.opt.disableflags & DisableBit.PASSIVE: - if qderiv is None: - qderiv = -jp.diag(m.dof_damping) - else: - qderiv -= jp.diag(m.dof_damping) - if m.ntendon: - qderiv -= d.ten_J.T @ jp.diag(m.tendon_damping) @ d.ten_J - # TODO(robotics-simulation): fluid drag model - if m.opt.has_fluid_params: - raise NotImplementedError('fluid drag not supported for implicitfast') + qderiv = derivative.deriv_smooth_vel(m, d) qacc = d.qacc if qderiv is not None: diff --git a/mjx/mujoco/mjx/_src/inverse.py b/mjx/mujoco/mjx/_src/inverse.py new file mode 100644 index 00000000..5ad3e1ad --- /dev/null +++ b/mjx/mujoco/mjx/_src/inverse.py @@ -0,0 +1,106 @@ +# Copyright 2025 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. +# ============================================================================== +"""Inverse dynamics functions.""" + +from jax import numpy as jp +from mujoco.mjx._src import derivative +from mujoco.mjx._src import forward +from mujoco.mjx._src import sensor +from mujoco.mjx._src import smooth +from mujoco.mjx._src import solver +from mujoco.mjx._src import support +# pylint: disable=g-importing-member +from mujoco.mjx._src.types import Data +from mujoco.mjx._src.types import DisableBit +from mujoco.mjx._src.types import EnableBit +from mujoco.mjx._src.types import IntegratorType +from mujoco.mjx._src.types import Model + + +def discrete_acc(m: Model, d: Data) -> Data: + """Convert discrete-time qacc to continuous-time qacc.""" + + if m.opt.integrator == IntegratorType.RK4: + raise RuntimeError( + 'discrete inverse dynamics is not supported by RK4 integrator' + ) + elif m.opt.integrator == IntegratorType.EULER: + dsbl_eulerdamp = m.opt.disableflags & DisableBit.EULERDAMP + no_dof_damping = (m.dof_damping == 0).all() + if dsbl_eulerdamp or no_dof_damping: + return d + + # set qfrc = (M + h*diag(B)) * qacc + qfrc = support.mul_m(m, d, d.qacc) + qfrc += m.opt.timestep * m.dof_damping * d.qacc + elif m.opt.integrator == IntegratorType.IMPLICITFAST: + qm = support.full_m(m, d) + + # compute analytical derivative qDeriv; skip rne derivative + qderiv = derivative.deriv_smooth_vel(m, d) + if qderiv is not None: + # M = M - dt*qDeriv + qm -= m.opt.timestep * qderiv + + # set qfrc = (M - dt*qDeriv) * qacc + qfrc = qm @ d.qacc + else: + raise NotImplementedError(f'integrator {m.opt.integrator} not implemented.') + + # solve for qacc: qfrc = M * qacc + qacc = smooth.solve_m(m, d, qfrc) + + return d.replace(qacc=qacc) + + +def inv_constraint(m: Model, d: Data) -> Data: + """Inverse constraint solver.""" + + # no constraints + if d.efc_J.size == 0: + return d.replace(qfrc_constraint=jp.zeros(m.nv)) + + # update + ctx = solver.Context.create(m, d, grad=False) + + return d.replace( + qfrc_constraint=ctx.qfrc_constraint, + efc_force=ctx.efc_force, + ) + + +def inverse(m: Model, d: Data) -> Data: + """Inverse dynamics.""" + d = forward.fwd_position(m, d) + d = sensor.sensor_pos(m, d) + d = forward.fwd_velocity(m, d) + d = sensor.sensor_vel(m, d) + + qacc = d.qacc + if m.opt.enableflags & EnableBit.INVDISCRETE: + d = discrete_acc(m, d) + + d = inv_constraint(m, d) + d = smooth.rne(m, d, flg_acc=True) + d = sensor.sensor_acc(m, d) + + qfrc_inverse = ( + d.qfrc_bias + m.dof_armature * d.qacc - d.qfrc_passive - d.qfrc_constraint + ) + + if m.opt.enableflags & EnableBit.INVDISCRETE: + return d.replace(qfrc_inverse=qfrc_inverse, qacc=qacc) + else: + return d.replace(qfrc_inverse=qfrc_inverse) diff --git a/mjx/mujoco/mjx/_src/inverse_test.py b/mjx/mujoco/mjx/_src/inverse_test.py new file mode 100644 index 00000000..19a5deef --- /dev/null +++ b/mjx/mujoco/mjx/_src/inverse_test.py @@ -0,0 +1,131 @@ +# Copyright 2023 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 inverse dynamics functions.""" +from absl.testing import absltest +from absl.testing import parameterized +import jax +from jax import numpy as jp +import mujoco +from mujoco import mjx +from mujoco.mjx._src import support +from mujoco.mjx._src import test_util +import numpy as np + +# tolerance for difference between MuJoCo and MJX calculations - mostly +# due to float precision +_TOLERANCE = 1e-5 + + +def _assert_eq(a, b, name, tol=_TOLERANCE): + tol = tol * 10 # avoid test noise + err_msg = f'mismatch: {name}' + np.testing.assert_allclose(a, b, err_msg=err_msg, atol=tol, rtol=tol) + + +class InverseTest(parameterized.TestCase): + + @parameterized.parameters( + (mujoco.mjtIntegrator.mjINT_EULER, False, False), + (mujoco.mjtIntegrator.mjINT_EULER, False, True), + (mujoco.mjtIntegrator.mjINT_EULER, True, False), + (mujoco.mjtIntegrator.mjINT_EULER, True, True), + (mujoco.mjtIntegrator.mjINT_IMPLICITFAST, False, False), + (mujoco.mjtIntegrator.mjINT_IMPLICITFAST, True, False), + ) + def test_forward_inverse_match(self, integrator, invdiscrete, eulerdamp): + m = mujoco.MjModel.from_xml_string(""" + + + """) + m.opt.integrator = integrator + if invdiscrete: + m.opt.enableflags |= mujoco.mjtEnableBit.mjENBL_INVDISCRETE + if not eulerdamp: + m.opt.disableflags |= mujoco.mjtDisableBit.mjDSBL_EULERDAMP + + d = mujoco.MjData(m) + d.qvel = np.random.uniform(low=-0.01, high=0.01, size=d.qvel.shape) + d.ctrl = np.random.uniform(low=-0.01, high=0.01, size=d.ctrl.shape) + d.qfrc_applied = np.random.uniform( + low=-0.01, high=0.01, size=d.qfrc_applied.shape + ) + d.xfrc_applied = np.random.uniform( + low=-0.01, high=0.01, size=d.xfrc_applied.shape + ) + mujoco.mj_step(m, d, 100) + + mx = mjx.put_model(m) + dx = mjx.put_data(m, d) + dx_next = mjx.step(mx, dx) + qacc_fd = (dx_next.qvel - dx.qvel) / mx.opt.timestep + + dx = mjx.forward(mx, dx) + + if invdiscrete: + dx = dx.replace(qacc=qacc_fd) + + dxinv = mjx.inverse(mx, dx) + + fwdinv0 = jp.linalg.norm( + dxinv.qfrc_constraint - dx.qfrc_constraint, ord=np.inf + ) + fwdinv1 = jp.linalg.norm( + dxinv.qfrc_inverse + - ( + dx.qfrc_applied + dx.qfrc_actuator + support.xfrc_accumulate(mx, dx) + ), + ord=np.inf, + ) + + self.assertLess(fwdinv0, 1.0e-3) + self.assertLess(fwdinv1, 1.0e-3) + _assert_eq(dxinv.qacc, dx.qacc, 'qacc') + + def test_tendon_force_clamp(self): + m = test_util.load_test_file('actuator/tendon_force_clamp.xml') + d = mujoco.MjData(m) + mx = mjx.put_model(m) + dx = mjx.put_data(m, d) + + dx = dx.replace(ctrl=jp.array([1.0, 1.0, 1.0, -1.0, 1.0, -20.0, 5.0, -5.0])) + dx = mjx.forward(mx, dx) + + _assert_eq( + dx.actuator_force, + jp.array([1.0, 1.0, 1.0, -1.0, 1.0, -10.0, 5.0, -5.0]), + 'actuator_force', + ) + + +if __name__ == '__main__': + absltest.main() diff --git a/mjx/mujoco/mjx/_src/io.py b/mjx/mujoco/mjx/_src/io.py index aac18731..54ac3dac 100644 --- a/mjx/mujoco/mjx/_src/io.py +++ b/mjx/mujoco/mjx/_src/io.py @@ -56,8 +56,8 @@ def _make_option( raise NotImplementedError(f'{mujoco.mjtSolver(o.solver)}') for i in range(mujoco.mjtEnableBit.mjNENABLE): - if o.enableflags & 2**i: - raise NotImplementedError(f'{mujoco.mjtEnableBit(2 ** i)}') + if o.enableflags & 2**i and 2**i not in set(types.EnableBit): + raise NotImplementedError(f'{mujoco.mjtEnableBit(2**i)}') has_fluid_params = o.density > 0 or o.viscosity > 0 or o.wind.any() implicitfast = o.integrator == mujoco.mjtIntegrator.mjINT_IMPLICITFAST @@ -72,6 +72,7 @@ def _make_option( fields['solver'] = types.SolverType(o.solver) fields['disableflags'] = types.DisableBit(o.disableflags) fields['has_fluid_params'] = has_fluid_params + fields['enableflags'] = types.EnableBit(o.enableflags) return types.Option(**fields) diff --git a/mjx/mujoco/mjx/_src/smooth.py b/mjx/mujoco/mjx/_src/smooth.py index 87750c04..92e46702 100644 --- a/mjx/mujoco/mjx/_src/smooth.py +++ b/mjx/mujoco/mjx/_src/smooth.py @@ -532,11 +532,14 @@ def subtree_vel(m: Model, d: Data) -> Data: return d.replace(subtree_linvel=subtree_linvel, subtree_angmom=subtree_angmom) -def rne(m: Model, d: Data) -> Data: - """Computes inverse dynamics using the recursive Newton-Euler algorithm.""" +def rne(m: Model, d: Data, flg_acc: bool = False) -> Data: + """Computes inverse dynamics using the recursive Newton-Euler algorithm. + + flg_acc=False removes inertial term. + """ # forward scan over tree: accumulate link center of mass acceleration - def cacc_fn(cacc, cdof_dot, qvel): + def cacc_fn(cacc, cdof_dot, qvel, cdof, qacc): if cacc is None: if m.opt.disableflags & DisableBit.GRAVITY: cacc = jp.zeros((6,)) @@ -545,9 +548,15 @@ def rne(m: Model, d: Data) -> Data: cacc += jp.sum(jax.vmap(jp.multiply)(cdof_dot, qvel), axis=0) + # cacc += cdof * qacc + if flg_acc: + cacc += jp.sum(jax.vmap(jp.multiply)(cdof, qacc), axis=0) + return cacc - cacc = scan.body_tree(m, cacc_fn, 'vv', 'b', d.cdof_dot, d.qvel) + cacc = scan.body_tree( + m, cacc_fn, 'vvvv', 'b', d.cdof_dot, d.qvel, d.cdof, d.qacc + ) def frc(cinert, cacc, cvel): frc = math.inert_mul(cinert, cacc) diff --git a/mjx/mujoco/mjx/_src/smooth_test.py b/mjx/mujoco/mjx/_src/smooth_test.py index 01ce8abb..eca05068 100644 --- a/mjx/mujoco/mjx/_src/smooth_test.py +++ b/mjx/mujoco/mjx/_src/smooth_test.py @@ -105,6 +105,13 @@ class SmoothTest(absltest.TestCase): # rne dx = jax.jit(mjx.rne)(mx, mjx.put_data(m, d)) _assert_attr_eq(d, dx, 'qfrc_bias') + # rne (flg_acc=True) + qfrc_bias = np.zeros(m.nv) + mujoco.mj_rne(m, d, 1, qfrc_bias) + dx = jax.jit(mjx.rne, static_argnums=(2,))( + mx, mjx.put_data(m, d), flg_acc=True + ) + _assert_eq(dx.qfrc_bias, qfrc_bias, 'qfrc_bias') # set dense jacobian for tendon: m.opt.jacobian = mujoco.mjtJacobian.mjJAC_DENSE diff --git a/mjx/mujoco/mjx/_src/solver.py b/mjx/mujoco/mjx/_src/solver.py index 6d3d4f21..efb58c41 100644 --- a/mjx/mujoco/mjx/_src/solver.py +++ b/mjx/mujoco/mjx/_src/solver.py @@ -30,7 +30,7 @@ from mujoco.mjx._src.types import SolverType # pylint: enable=g-importing-member -class _Context(PyTreeNode): +class Context(PyTreeNode): """Data updated during each solver iteration. Attributes: @@ -72,7 +72,7 @@ class _Context(PyTreeNode): h: jax.Array @classmethod - def create(cls, m: Model, d: Data, grad: bool = True) -> '_Context': + def create(cls, m: Model, d: Data, grad: bool = True) -> 'Context': jaref = d.efc_J @ d.qacc - d.efc_aref # TODO(robotics-team): determine nv at which sparse mul is faster ma = support.mul_m(m, d, d.qacc) @@ -86,7 +86,7 @@ class _Context(PyTreeNode): for condim in (3, 4, 6): fri = fri.at[dim == condim, condim:].set(0) - ctx = _Context( + ctx = Context( qacc=d.qacc, qfrc_constraint=d.qfrc_constraint, Jaref=jaref, @@ -133,7 +133,7 @@ class _LSPoint(PyTreeNode): cls, m: Model, d: Data, - ctx: _Context, + ctx: Context, alpha: jax.Array, jv: jax.Array, quad: jax.Array, @@ -241,7 +241,7 @@ def _while_loop_scan(cond_fun, body_fun, init_val, max_iter): return jax.lax.scan(_fun, init, None, length=max_iter)[0][0] -def _update_constraint(m: Model, d: Data, ctx: _Context) -> _Context: +def _update_constraint(m: Model, d: Data, ctx: Context) -> Context: """Updates constraint force and resulting cost given last solver iteration. Corresponds to CGupdateConstraint in mujoco/src/engine/engine_solver.c @@ -356,7 +356,7 @@ def _update_constraint(m: Model, d: Data, ctx: _Context) -> _Context: return ctx -def _update_gradient(m: Model, d: Data, ctx: _Context) -> _Context: +def _update_gradient(m: Model, d: Data, ctx: Context) -> Context: """Updates grad and M / grad given latest solver iteration. Corresponds to CGupdateGradient in mujoco/src/engine/engine_solver.c @@ -403,7 +403,7 @@ def _rescale(m: Model, value: jax.Array) -> jax.Array: return value / (m.stat.meaninertia * max(1, m.nv)) -def _linesearch(m: Model, d: Data, ctx: _Context) -> _Context: +def _linesearch(m: Model, d: Data, ctx: Context) -> Context: """Performs a zoom linesearch to find optimal search step size. Args: @@ -529,7 +529,7 @@ def _linesearch(m: Model, d: Data, ctx: _Context) -> _Context: def solve(m: Model, d: Data) -> Data: """Finds forces that satisfy constraints using conjugate gradient descent.""" - def cond(ctx: _Context) -> jax.Array: + def cond(ctx: Context) -> jax.Array: improvement = _rescale(m, ctx.prev_cost - ctx.cost) gradient = _rescale(m, math.norm(ctx.grad)) @@ -539,7 +539,7 @@ def solve(m: Model, d: Data) -> Data: return ~done - def body(ctx: _Context) -> _Context: + def body(ctx: Context) -> Context: ctx = _linesearch(m, d, ctx) prev_grad, prev_Mgrad = ctx.grad, ctx.Mgrad # pylint: disable=invalid-name ctx = _update_constraint(m, d, ctx) @@ -560,12 +560,12 @@ def solve(m: Model, d: Data) -> Data: # warmstart: qacc = d.qacc_smooth if not m.opt.disableflags & DisableBit.WARMSTART: - warm = _Context.create(m, d.replace(qacc=d.qacc_warmstart), grad=False) - smth = _Context.create(m, d.replace(qacc=d.qacc_smooth), grad=False) + warm = Context.create(m, d.replace(qacc=d.qacc_warmstart), grad=False) + smth = Context.create(m, d.replace(qacc=d.qacc_smooth), grad=False) qacc = jp.where(warm.cost < smth.cost, d.qacc_warmstart, d.qacc_smooth) d = d.replace(qacc=qacc) - ctx = _Context.create(m, d) + ctx = Context.create(m, d) if m.opt.iterations == 1: ctx = body(ctx) else: diff --git a/mjx/mujoco/mjx/_src/solver_test.py b/mjx/mujoco/mjx/_src/solver_test.py index 57d4a116..6da06526 100644 --- a/mjx/mujoco/mjx/_src/solver_test.py +++ b/mjx/mujoco/mjx/_src/solver_test.py @@ -74,7 +74,7 @@ class SolverTest(parameterized.TestCase): # compare costs mj_cost = cost(d.qacc) - ctx = solver._Context.create(mjx.put_model(m), mjx.put_data(m, d)) + ctx = solver.Context.create(mjx.put_model(m), mjx.put_data(m, d)) mjx_cost = ctx.cost - ctx.gauss _assert_eq(mj_cost, mjx_cost, 'cost') diff --git a/mjx/mujoco/mjx/_src/types.py b/mjx/mujoco/mjx/_src/types.py index 3ee10499..94587478 100644 --- a/mjx/mujoco/mjx/_src/types.py +++ b/mjx/mujoco/mjx/_src/types.py @@ -65,6 +65,17 @@ class DisableBit(enum.IntFlag): # unsupported: MIDPHASE +class EnableBit(enum.IntFlag): + """Enable optional feature bitflags. + + Members: + INVDISCRETE: discrete-time inverse dynamics + """ + + INVDISCRETE = mujoco.mjtEnableBit.mjENBL_INVDISCRETE + # unsupported: OVERRIDE, ENERGY, FWDINV, MULTICCD, ISLAND + + class JointType(enum.IntEnum): """Type of degree of freedom. From 3c530976e3dbf461c8b43f3b0ee3467682a4a3ec Mon Sep 17 00:00:00 2001 From: Taylor Howell Date: Thu, 10 Apr 2025 06:47:39 -0700 Subject: [PATCH 058/191] Add `rne_postconstraint` dependence for `FRAMELINACC` and `FRAMEANGACC` sensors to MJX. PiperOrigin-RevId: 746009774 Change-Id: Ide45298b463f1ad04f1ffa7f60a7a8aeaf18f063 --- mjx/mujoco/mjx/_src/io.py | 2 ++ mjx/mujoco/mjx/_src/sensor.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/mjx/mujoco/mjx/_src/io.py b/mjx/mujoco/mjx/_src/io.py index 54ac3dac..01815012 100644 --- a/mjx/mujoco/mjx/_src/io.py +++ b/mjx/mujoco/mjx/_src/io.py @@ -129,6 +129,8 @@ def put_model( np.any(m.sensor_type == types.SensorType.ACCELEROMETER) | np.any(m.sensor_type == types.SensorType.FORCE) | np.any(m.sensor_type == types.SensorType.TORQUE) + | np.any(m.sensor_type == types.SensorType.FRAMELINACC) + | np.any(m.sensor_type == types.SensorType.FRAMEANGACC) ) eq_connect_weld = np.any(m.eq_type == types.EqType.CONNECT) | np.any( m.eq_type == types.EqType.WELD diff --git a/mjx/mujoco/mjx/_src/sensor.py b/mjx/mujoco/mjx/_src/sensor.py index 5d040052..7c3a5c87 100644 --- a/mjx/mujoco/mjx/_src/sensor.py +++ b/mjx/mujoco/mjx/_src/sensor.py @@ -441,6 +441,8 @@ def sensor_acc(m: Model, d: Data) -> Data: SensorType.ACCELEROMETER, SensorType.FORCE, SensorType.TORQUE, + SensorType.FRAMELINACC, + SensorType.FRAMEANGACC, }: d = smooth.rne_postconstraint(m, d) From 25126e88c7a0b1660c88cb826a948ec259cc0c1a Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Thu, 10 Apr 2025 06:55:37 -0700 Subject: [PATCH 059/191] Move mju_cholFactorCount to engine_util_solve. PiperOrigin-RevId: 746011856 Change-Id: If9812251420053644f4eca81adb5116d370ee524 --- src/engine/engine_util_solve.c | 54 +++++++++++++ src/engine/engine_util_solve.h | 4 + src/engine/engine_util_sparse.c | 52 ------------ src/engine/engine_util_sparse.h | 3 - test/engine/engine_util_solve_test.cc | 76 ++++++++++++++++- test/engine/engine_util_sparse_test.cc | 108 ++++--------------------- test/fixture.h | 5 +- 7 files changed, 150 insertions(+), 152 deletions(-) diff --git a/src/engine/engine_util_solve.c b/src/engine/engine_util_solve.c index 83fdb634..11889667 100644 --- a/src/engine/engine_util_solve.c +++ b/src/engine/engine_util_solve.c @@ -191,6 +191,60 @@ int mju_cholFactorSparse(mjtNum* mat, int n, mjtNum mindiag, +// precount row non-zeros of reverse-Cholesky factor L, return total non-zeros +// based on ldl_symbolic from 'Algorithm 8xx: a concise sparse Cholesky factorization package' +// reads pattern from upper triangle +int mju_cholFactorCount(int* L_rownnz, const int* rownnz, const int* rowadr, const int* colind, + int n, mjData* d) { + mj_markStack(d); + int* parent = mjSTACKALLOC(d, n, int); + int* flag = mjSTACKALLOC(d, n, int); + + // loop over rows in reverse order + for (int r = n - 1; r >= 0; r--) { + parent[r] = -1; + flag[r] = r; + L_rownnz[r] = 1; // start with 1 for diagonal + + // loop over non-zero columns of upper triangle + int start = rowadr[r]; + int end = start + rownnz[r]; + for (int c = start; c < end; c++) { + int i = colind[c]; + + // skip lower triangle + if (i <= r) { + continue; + } + + // traverse from i to ancestor, stop when row is flagged + while (flag[i] != r) { + // if not yet set, set parent to current row + if (parent[i] == -1) { + parent[i] = r; + } + + // increment non-zeros, flag row i, advance to parent + L_rownnz[i]++; + flag[i] = r; + i = parent[i]; + } + } + } + + mj_freeStack(d); + + // sum up all row non-zeros + int nnz = 0; + for (int r = 0; r < n; r++) { + nnz += L_rownnz[r]; + } + + return nnz; +} + + + // sparse reverse-order Cholesky solve void mju_cholSolveSparse(mjtNum* res, const mjtNum* mat, const mjtNum* vec, int n, const int* rownnz, const int* rowadr, const int* colind) { diff --git a/src/engine/engine_util_solve.h b/src/engine/engine_util_solve.h index 6bbdc6bb..6a772e2c 100644 --- a/src/engine/engine_util_solve.h +++ b/src/engine/engine_util_solve.h @@ -38,6 +38,10 @@ int mju_cholFactorSparse(mjtNum* mat, int n, mjtNum mindiag, int* rownnz, const int* rowadr, int* colind, mjData* d); +// precount row non-zeros of reverse-Cholesky factor L, return total +MJAPI int mju_cholFactorCount(int* L_rownnz, const int* rownnz, const int* rowadr, + const int* colind, int n, mjData* d); + // sparse reverse-order Cholesky solve void mju_cholSolveSparse(mjtNum* res, const mjtNum* mat, const mjtNum* vec, int n, const int* rownnz, const int* rowadr, const int* colind); diff --git a/src/engine/engine_util_sparse.c b/src/engine/engine_util_sparse.c index abb95be0..d72a9fca 100644 --- a/src/engine/engine_util_sparse.c +++ b/src/engine/engine_util_sparse.c @@ -795,55 +795,3 @@ void mju_sqrMatTDSparse(mjtNum* res, const mjtNum* mat, const mjtNum* matT, mj_freeStack(d); } - -// precount row non-zeros of reverse-Cholesky factor L, return total non-zeros -// based on ldl_symbolic from 'Algorithm 8xx: a concise sparse Cholesky factorization package' -// reads pattern from upper triangle -int mju_cholFactorCount(int* L_rownnz, const int* rownnz, const int* rowadr, const int* colind, - int n, mjData* d) { - mj_markStack(d); - int* parent = mjSTACKALLOC(d, n, int); - int* flag = mjSTACKALLOC(d, n, int); - - // loop over rows in reverse order - for (int r = n - 1; r >= 0; r--) { - parent[r] = -1; - flag[r] = r; - L_rownnz[r] = 1; // start with 1 for diagonal - - // loop over non-zero columns of upper triangle - int start = rowadr[r]; - int end = start + rownnz[r]; - for (int c = start; c < end; c++) { - int i = colind[c]; - - // skip lower triangle - if (i <= r) { - continue; - } - - // traverse from i to ancestor, stop when row is flagged - while (flag[i] != r) { - // if not yet set, set parent to current row - if (parent[i] == -1) { - parent[i] = r; - } - - // increment non-zeros, flag row i, advance to parent - L_rownnz[i]++; - flag[i] = r; - i = parent[i]; - } - } - } - - mj_freeStack(d); - - // sum up all row non-zeros - int nnz = 0; - for (int r = 0; r < n; r++) { - nnz += L_rownnz[r]; - } - - return nnz; -} diff --git a/src/engine/engine_util_sparse.h b/src/engine/engine_util_sparse.h index 92c2dad0..08a663b2 100644 --- a/src/engine/engine_util_sparse.h +++ b/src/engine/engine_util_sparse.h @@ -103,9 +103,6 @@ MJAPI void mju_sqrMatTDSparseCount(int* res_rownnz, int* res_rowadr, int nr, // precompute res_rowadr for mju_sqrMatTDSparse using uncompressed memory MJAPI void mju_sqrMatTDUncompressedInit(int* res_rowadr, int nc); -// precount row non-zeros of reverse-Cholesky factor L, return total -MJAPI int mju_cholFactorCount(int* L_rownnz, const int* rownnz, const int* rowadr, - const int* colind, int n, mjData* d); // ------------------------------ inlined functions ------------------------------------------------ diff --git a/test/engine/engine_util_solve_test.cc b/test/engine/engine_util_solve_test.cc index d4bea9a1..aad5b421 100644 --- a/test/engine/engine_util_solve_test.cc +++ b/test/engine/engine_util_solve_test.cc @@ -21,7 +21,6 @@ #include #include #include -#include #include #include @@ -36,6 +35,7 @@ namespace { using ::testing::DoubleEq; using ::testing::Pointwise; using ::testing::DoubleNear; +using ::testing::ElementsAre; using ::std::string; using ::std::setw; using QCQP2Test = MujocoTest; @@ -649,5 +649,79 @@ TEST_F(BandMatrixTest, Solve) { } } +using EngineUtilSolveTest = MujocoTest; + +TEST_F(EngineUtilSolveTest, MjuCholFactorNNZ) { + mjModel* model = LoadModelFromString(""); + mjData* d = mj_makeData(model); + + int nA = 2; + mjtNum matA[4] = {1, 0, + 0, 1}; + mjtNum sparseA[4]; + int rownnzA[2]; + int rowadrA[2]; + int colindA[4]; + int rownnzA_factor[2]; + mju_dense2sparse(sparseA, matA, nA, nA, rownnzA, rowadrA, colindA, 4); + int nnzA = mju_cholFactorCount(rownnzA_factor, + rownnzA, rowadrA, colindA, nA, d); + + EXPECT_EQ(nnzA, 2); + EXPECT_THAT(AsVector(rownnzA_factor, 2), ElementsAre(1, 1)); + + int nB = 3; + mjtNum matB[9] = {10, 1, 0, + 0, 10, 1, + 0, 0, 10}; + mjtNum sparseB[9]; + int rownnzB[3]; + int rowadrB[3]; + int colindB[9]; + int rownnzB_factor[3]; + mju_dense2sparse(sparseB, matB, nB, nB, rownnzB, rowadrB, colindB, 9); + int nnzB = mju_cholFactorCount(rownnzB_factor, + rownnzB, rowadrB, colindB, nB, d); + + EXPECT_EQ(nnzB, 5); + EXPECT_THAT(AsVector(rownnzB_factor, 3), ElementsAre(1, 2, 2)); + + int nC = 3; + mjtNum matC[9] = {10, 1, 0, + 0, 10, 0, + 0, 0, 10}; + mjtNum sparseC[9]; + int rownnzC[3]; + int rowadrC[3]; + int colindC[9]; + int rownnzC_factor[3]; + mju_dense2sparse(sparseC, matC, nC, nC, rownnzC, rowadrC, colindC, 9); + int nnzC = mju_cholFactorCount(rownnzC_factor, + rownnzC, rowadrC, colindC, nC, d); + + EXPECT_EQ(nnzC, 4); + EXPECT_THAT(AsVector(rownnzC_factor, 3), ElementsAre(1, 2, 1)); + + int nD = 4; + mjtNum matD[16] = {10, 1, 2, 3, + 0, 10, 0, 0, + 0, 0, 10, 1, + 0, 0, 0, 10}; + mjtNum sparseD[16]; + int rownnzD[4]; + int rowadrD[4]; + int colindD[16]; + int rownnzD_factor[4]; + mju_dense2sparse(sparseD, matD, nD, nD, rownnzD, rowadrD, colindD, 16); + int nnzD = mju_cholFactorCount(rownnzD_factor, + rownnzD, rowadrD, colindD, nD, d); + + EXPECT_EQ(nnzD, 8); + EXPECT_THAT(AsVector(rownnzD_factor, 4), ElementsAre(1, 2, 2, 3)); + + mj_deleteData(d); + mj_deleteModel(model); +} + } // namespace } // namespace mujoco diff --git a/test/engine/engine_util_sparse_test.cc b/test/engine/engine_util_sparse_test.cc index c1b7cb65..94da0d5d 100644 --- a/test/engine/engine_util_sparse_test.cc +++ b/test/engine/engine_util_sparse_test.cc @@ -15,7 +15,6 @@ // Tests for engine/engine_util_sparse.c #include -#include #include "src/engine/engine_util_sparse.h" @@ -30,11 +29,6 @@ namespace { using ::testing::ElementsAre; using EngineUtilSparseTest = MujocoTest; -template -std::vector AsVector(const T* array, int n) { - return std::vector(array, array + n); -} - TEST_F(EngineUtilSparseTest, MjuDot) { mjtNum a[] = {2, 3, 4, 5, 6, 7, 8}; mjtNum u[] = {2, 1, 3, 1, 1, 4, 1, 1, 1, 5, 1, 1, 1, 6, 1, 1, 7, 1, 8}; @@ -335,14 +329,12 @@ TEST_F(EngineUtilSparseTest, MjuCompressSparse) { EXPECT_EQ(AsVector(dense, 6), AsVector(dense_expected_minval1, 6)); } -static constexpr char modelStr[] = R"()"; - TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse1) { // 0 0 0 // M = 0 0 0 // 0 0 0 - mjModel* model = LoadModelFromString(modelStr); + mjModel* model = LoadModelFromString(""); mjData* data = mj_makeData(model); mjtNum mat[] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; @@ -388,7 +380,7 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse2) { // M = 1 2 -1 // 2 2 3 - mjModel* model = LoadModelFromString(modelStr); + mjModel* model = LoadModelFromString(""); mjData* data = mj_makeData(model); mjtNum mat[] = {2, -1, 1, 2, -1, 2, 2, 2, 3}; @@ -435,7 +427,7 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse3) { // M = 0 3 0 // 4 0 0 - mjModel* model = LoadModelFromString(modelStr); + mjModel* model = LoadModelFromString(""); mjData* data = mj_makeData(model); mjtNum mat[] = {1, 2, 3, 4}; @@ -483,7 +475,7 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse4) { // M = 0 0 3 // 4 0 0 - mjModel* model = LoadModelFromString(modelStr); + mjModel* model = LoadModelFromString(""); mjData* data = mj_makeData(model); mjtNum mat[] = {1, 2, 3, 4}; @@ -532,7 +524,7 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse5) { // M = 0 0 0 // 2 3 0 - mjModel* model = LoadModelFromString(modelStr); + mjModel* model = LoadModelFromString(""); mjData* data = mj_makeData(model); mjtNum mat[] = {1, 4, 2, 3}; @@ -579,7 +571,7 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse6) { // M = 0 2 0 // 0 0 3 - mjModel* model = LoadModelFromString(modelStr); + mjModel* model = LoadModelFromString(""); mjData* data = mj_makeData(model); mjtNum mat[] = {1, 2, 2, 3}; @@ -626,7 +618,7 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse7) { // M = 0 3 // 4 0 - mjModel* model = LoadModelFromString(modelStr); + mjModel* model = LoadModelFromString(""); mjData* data = mj_makeData(model); mjtNum mat[] = {1, 2, 3, 4}; @@ -673,7 +665,7 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse8) { // M = 1 0 4 // 2 3 0 - mjModel* model = LoadModelFromString(modelStr); + mjModel* model = LoadModelFromString(""); mjData* data = mj_makeData(model); mjtNum mat[] = {1, 4, 2, 3}; @@ -721,7 +713,7 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse9) { // M = 1 3 4 // 4 4 4 - mjModel* model = LoadModelFromString(modelStr); + mjModel* model = LoadModelFromString(""); mjData* data = mj_makeData(model); mjtNum mat[] = {1, 2, 2, 1, 3, 4, 4, 4, 4}; @@ -769,7 +761,7 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse10) { // M = 2 2 2 // 3 3 3 - mjModel* model = LoadModelFromString(modelStr); + mjModel* model = LoadModelFromString(""); mjData* data = mj_makeData(model); mjtNum mat[] = {1, 1, 1, 2, 2, 2, 3, 3, 3}; @@ -818,7 +810,7 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse11) { // M = 0 0 0 // 0 3 3 - mjModel* model = LoadModelFromString(modelStr); + mjModel* model = LoadModelFromString(""); mjData* data = mj_makeData(model); mjtNum mat[] = {1, 1, 1, 3, 3}; @@ -867,7 +859,7 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse12) { // M = 0 0 0 0 // 0 0 3 3 - mjModel* model = LoadModelFromString(modelStr); + mjModel* model = LoadModelFromString(""); mjData* data = mj_makeData(model); mjtNum mat[] = {1, 1, 1, 1, 3, 3}; @@ -918,7 +910,7 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse13) { // M = 1 1 0 0 0 // 1 1 0 0 0 - mjModel* model = LoadModelFromString(modelStr); + mjModel* model = LoadModelFromString(""); mjData* data = mj_makeData(model); mjtNum mat[] = {1, 1, 1, 1, 1, 1}; @@ -969,7 +961,7 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse13) { TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse14) { // M = 1 1 1 1 2 2 2 - mjModel* model = LoadModelFromString(modelStr); + mjModel* model = LoadModelFromString(""); mjData* data = mj_makeData(model); mjtNum mat[] = {1, 1, 1, 1, 2, 2, 2}; @@ -1022,78 +1014,6 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse14) { mj_deleteModel(model); } -TEST_F(EngineUtilSparseTest, MjuCholFactorNNZ) { - mjModel* model = LoadModelFromString(modelStr); - mjData* d = mj_makeData(model); - - int nA = 2; - mjtNum matA[4] = {1, 0, - 0, 1}; - mjtNum sparseA[4]; - int rownnzA[2]; - int rowadrA[2]; - int colindA[4]; - int rownnzA_factor[2]; - mju_dense2sparse(sparseA, matA, nA, nA, rownnzA, rowadrA, colindA, 4); - int nnzA = mju_cholFactorCount(rownnzA_factor, - rownnzA, rowadrA, colindA, nA, d); - - EXPECT_EQ(nnzA, 2); - EXPECT_THAT(AsVector(rownnzA_factor, 2), ElementsAre(1, 1)); - - int nB = 3; - mjtNum matB[9] = {10, 1, 0, - 0, 10, 1, - 0, 0, 10}; - mjtNum sparseB[9]; - int rownnzB[3]; - int rowadrB[3]; - int colindB[9]; - int rownnzB_factor[3]; - mju_dense2sparse(sparseB, matB, nB, nB, rownnzB, rowadrB, colindB, 9); - int nnzB = mju_cholFactorCount(rownnzB_factor, - rownnzB, rowadrB, colindB, nB, d); - - EXPECT_EQ(nnzB, 5); - EXPECT_THAT(AsVector(rownnzB_factor, 3), ElementsAre(1, 2, 2)); - - int nC = 3; - mjtNum matC[9] = {10, 1, 0, - 0, 10, 0, - 0, 0, 10}; - mjtNum sparseC[9]; - int rownnzC[3]; - int rowadrC[3]; - int colindC[9]; - int rownnzC_factor[3]; - mju_dense2sparse(sparseC, matC, nC, nC, rownnzC, rowadrC, colindC, 9); - int nnzC = mju_cholFactorCount(rownnzC_factor, - rownnzC, rowadrC, colindC, nC, d); - - EXPECT_EQ(nnzC, 4); - EXPECT_THAT(AsVector(rownnzC_factor, 3), ElementsAre(1, 2, 1)); - - int nD = 4; - mjtNum matD[16] = {10, 1, 2, 3, - 0, 10, 0, 0, - 0, 0, 10, 1, - 0, 0, 0, 10}; - mjtNum sparseD[16]; - int rownnzD[4]; - int rowadrD[4]; - int colindD[16]; - int rownnzD_factor[4]; - mju_dense2sparse(sparseD, matD, nD, nD, rownnzD, rowadrD, colindD, 16); - int nnzD = mju_cholFactorCount(rownnzD_factor, - rownnzD, rowadrD, colindD, nD, d); - - EXPECT_EQ(nnzD, 8); - EXPECT_THAT(AsVector(rownnzD_factor, 4), ElementsAre(1, 2, 2, 3)); - - mj_deleteData(d); - mj_deleteModel(model); -} - TEST_F(EngineUtilSparseTest, MjuMulMatTVec) { int nr = 2; int nc = 3; diff --git a/test/fixture.h b/test/fixture.h index df568943..cbb71d5f 100644 --- a/test/fixture.h +++ b/test/fixture.h @@ -108,8 +108,9 @@ std::vector GetCtrlNoise(const mjModel* m, int nsteps, mjtNum CompareModel(const mjModel* m1, const mjModel* m2, std::string& field); // Returns a vector containing the elements of the array. -inline std::vector AsVector(const mjtNum* array, int n) { - return std::vector(array, array + n); +template +std::vector AsVector(const T* array, int n) { + return std::vector(array, array + n); } // Prints a matrix to stderr, useful for debugging. From b1f8d444f2c660ca7d88218f6e4f306d1b24b131 Mon Sep 17 00:00:00 2001 From: Kyle Bayes Date: Thu, 10 Apr 2025 07:03:23 -0700 Subject: [PATCH 060/191] Fix bug in replaceSimplex3 in nativeccd. PiperOrigin-RevId: 746014344 Change-Id: I93b32ab5f67ee48ab72238de53f7cda07638cb79 --- src/engine/engine_collision_gjk.c | 18 ++--- test/engine/engine_collision_gjk_test.cc | 68 +++++++++++++++++++ .../engine/testdata/collision_convex/dice.xml | 4 +- .../collision_convex/dice_boxmesh.xml | 47 +++++++++++++ 4 files changed, 123 insertions(+), 14 deletions(-) create mode 100644 test/engine/testdata/collision_convex/dice_boxmesh.xml diff --git a/src/engine/engine_collision_gjk.c b/src/engine/engine_collision_gjk.c index cb58530d..3c2df6c3 100644 --- a/src/engine/engine_collision_gjk.c +++ b/src/engine/engine_collision_gjk.c @@ -834,22 +834,16 @@ static void S1D(mjtNum lambda[2], const mjtNum s1[3], const mjtNum s2[3]) { // replace a 3-simplex with one of its faces static inline void replaceSimplex3(Polytope* pt, mjCCDStatus* status, int v1, int v2, int v3) { + // reset status simplex status->nsimplex = 3; - Vertex* v = pt->verts; - copy3(status->simplex[0].vert1, v[v1].vert1); - copy3(status->simplex[1].vert1, v[v2].vert1); - copy3(status->simplex[2].vert1, v[v3].vert1); - - copy3(status->simplex[0].vert2, v[v1].vert2); - copy3(status->simplex[1].vert2, v[v2].vert2); - copy3(status->simplex[2].vert2, v[v3].vert2); - - copy3(status->simplex[0].vert, v[v1].vert); - copy3(status->simplex[1].vert, v[v2].vert); - copy3(status->simplex[2].vert, v[v3].vert); + status->simplex[0] = pt->verts[v1]; + status->simplex[1] = pt->verts[v2]; + status->simplex[2] = pt->verts[v3]; + // reset polytope pt->nfaces = 0; pt->nverts = 0; + pt->nmap = 0; } diff --git a/test/engine/engine_collision_gjk_test.cc b/test/engine/engine_collision_gjk_test.cc index 934a034e..5eb5f6df 100644 --- a/test/engine/engine_collision_gjk_test.cc +++ b/test/engine/engine_collision_gjk_test.cc @@ -1152,6 +1152,74 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD12) { mj_deleteModel(model); } +TEST_F(MjGjkTest, BoxBoxMultiCCD13) { + static constexpr char xml[] = R"( + + + + + + )"; + + std::array error; + mjModel* model = LoadModelFromString(xml, error.data(), error.size()); + ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error.data(); + + mjData* data = mj_makeData(model); + mj_forward(model, data); + + mjtNum* xpos = data->geom_xpos; + mjtNum* xmat = data->geom_xmat; + + xmat[0] = 1.0000000000000000000000000000000000000000; + xmat[1] = -0.0000000000000000000000000000001366192847; + xmat[2] = -0.0000000000000002451235402041944571528177; + xmat[3] = 0.0000000000000000000000000000001366707863; + xmat[4] = 1.0000000000000000000000000000000000000000; + xmat[5] = 0.0000000000000000002101047324941978855126; + xmat[6] = 0.0000000000000002451235402041944571528177; + xmat[7] = -0.0000000000000000002101047324941978855126; + xmat[8] = 1.0000000000000000000000000000000000000000; + + xpos[0] = -0.1000000000000000055511151231257827021182; + xpos[1] = -0.2000000000000000111022302462515654042363; + xpos[2] = -0.0809921810760001470441693527391180396080; + + xpos = data->geom_xpos + 3; + xmat = data->geom_xmat + 9; + + xmat[0] = 1.0000000000000000000000000000000000000000; + xmat[1] = -0.0000000000000000000000000000000740327228; + xmat[2] = -0.0000000000000002557259745463766308177658; + xmat[3] = 0.0000000000000000000000000000000775428823; + xmat[4] = 1.0000000000000000000000000000000000000000; + xmat[5] = 0.0000000000000000137262533997081760161613; + xmat[6] = 0.0000000000000002557259745463766308177658; + xmat[7] = -0.0000000000000000137262533997081760161613; + xmat[8] = 1.0000000000000000000000000000000000000000; + + xpos[0] = -0.1000000000000000055511151231257827021182; + xpos[1] = -0.2000000000000000111022302462515654042363; + xpos[2] = -0.0418396695286432432348000531874276930466; + + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; + std::vector dir, pos; + mjtNum dist; + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 8); + + EXPECT_EQ(ncons, 4); + + EXPECT_NEAR(dir[0], 0, kTolerance); + EXPECT_NEAR(dir[1], 0, kTolerance); + EXPECT_NEAR(dir[2], 1, kTolerance); + + mj_deleteData(data); + mj_deleteModel(model); +} + TEST_F(MjGjkTest, SmallBoxMesh) { static constexpr char xml[] = R"( diff --git a/test/engine/testdata/collision_convex/dice.xml b/test/engine/testdata/collision_convex/dice.xml index 2a3b6880..5f077f4c 100644 --- a/test/engine/testdata/collision_convex/dice.xml +++ b/test/engine/testdata/collision_convex/dice.xml @@ -25,7 +25,7 @@ - > + @@ -37,7 +37,7 @@ - + diff --git a/test/engine/testdata/collision_convex/dice_boxmesh.xml b/test/engine/testdata/collision_convex/dice_boxmesh.xml new file mode 100644 index 00000000..cfd3ae86 --- /dev/null +++ b/test/engine/testdata/collision_convex/dice_boxmesh.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From a209134315dda126a714529bfa80f7478902644d Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Thu, 10 Apr 2025 09:12:24 -0700 Subject: [PATCH 061/191] Enable procedural plugin activation in Python bindings. PiperOrigin-RevId: 746055614 Change-Id: Idcc2f62fdfeccdca356e3d1acadf168b46a98881 --- .../mujoco/codegen/generate_spec_bindings.py | 3 ++- python/mujoco/specs.cc | 7 +++++++ python/mujoco/specs_test.py | 21 ++++++++----------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/python/mujoco/codegen/generate_spec_bindings.py b/python/mujoco/codegen/generate_spec_bindings.py index bb52643d..1db57b3c 100644 --- a/python/mujoco/codegen/generate_spec_bindings.py +++ b/python/mujoco/codegen/generate_spec_bindings.py @@ -84,6 +84,7 @@ def _value_binding_code( fulltype = fulltype.replace('mjOption', 'raw::MjOption') fulltype = fulltype.replace('mjVisual', 'raw::MjVisual') fulltype = fulltype.replace('mjStatistic', 'raw::MjStatistic') + element = '.element' if fullvarname == 'plugin' else '' def_property_args = ( f'"{varname}"', @@ -91,7 +92,7 @@ def _value_binding_code( return self.{fullvarname}; }}""", f"""[]({rawclassname}& self, {fulltype} {varname}) {{ - self.{fullvarname} = {varname}; + self.{fullvarname}{element} = {varname}{element}; }}""", ) diff --git a/python/mujoco/specs.cc b/python/mujoco/specs.cc index ec444f52..313de159 100644 --- a/python/mujoco/specs.cc +++ b/python/mujoco/specs.cc @@ -483,6 +483,13 @@ PYBIND11_MODULE(_specs, m) { py::arg("suffix") = py::none(), py::arg("site") = py::none(), py::arg("frame") = py::none(), py::return_value_policy::reference_internal); + mjSpec.def( + "activate_plugin", + [](MjSpec& self, std::string& name) { + mjs_activatePlugin(self.ptr, name.c_str()); + }, + py::arg("name"), + py::return_value_policy::reference_internal); // ============================= MJSBODY ===================================== mjsBody.def( diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py index bfefd71f..1b46bd4b 100644 --- a/python/mujoco/specs_test.py +++ b/python/mujoco/specs_test.py @@ -835,22 +835,19 @@ class SpecsTest(absltest.TestCase): self.assertEqual(model.nsensor, 9) def test_plugin(self): - xml = """ - - - - - - """ - - spec = mujoco.MjSpec.from_string(xml) - self.assertIsNotNone(spec.worldbody) + spec = mujoco.MjSpec() + spec.activate_plugin('mujoco.elasticity.cable') + plugin = spec.add_plugin( + name='instance_name', + plugin_name='mujoco.elasticity.cable', + active=True, + info='info', + ) body = spec.worldbody.add_body() + body.plugin = plugin body.plugin.plugin_name = 'mujoco.elasticity.cable' - body.plugin.id = spec.add_plugin() body.plugin.active = True - self.assertEqual(body.plugin.id, 0) geom = body.add_geom() geom.type = mujoco.mjtGeom.mjGEOM_BOX From 440ce5eb7a12d401cfd4f0a06f35fdcd5a45ae35 Mon Sep 17 00:00:00 2001 From: Gabe Oppenheimer Date: Thu, 10 Apr 2025 13:16:35 -0700 Subject: [PATCH 062/191] Update the version number to 3.3.2 following the 3.3.1 release. PiperOrigin-RevId: 746147577 Change-Id: Idb7b4e7e06a4730b4f2eea8f6600b898e998f8db --- CMakeLists.txt | 2 +- dist/mujoco.rc | 8 ++++---- dist/simulate.rc | 8 ++++---- doc/APIreference/APIglobals.rst | 2 +- doc/unity.rst | 4 ++-- include/mujoco/mujoco.h | 2 +- mjx/pyproject.toml | 8 ++++---- python/mujoco/CMakeLists.txt | 4 ++-- python/mujoco/mjpython/Info.plist | 8 ++++---- python/pyproject.toml | 6 +++--- sample/CMakeLists.txt | 2 +- simulate/CMakeLists.txt | 2 +- src/engine/engine_support.c | 4 ++-- unity/Editor/Bindings/MujocoBinaryRetriever.cs | 4 ++-- unity/Runtime/Bindings/MjBindings.cs | 2 +- unity/package.json | 2 +- 16 files changed, 34 insertions(+), 34 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 62a396ea..bbea8706 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,7 +28,7 @@ set(MSVC_INCREMENTAL_DEFAULT ON) project( mujoco - VERSION 3.3.1 + VERSION 3.3.2 DESCRIPTION "MuJoCo Physics Simulator" HOMEPAGE_URL "https://mujoco.org" ) diff --git a/dist/mujoco.rc b/dist/mujoco.rc index 171b9810..08a3699d 100644 --- a/dist/mujoco.rc +++ b/dist/mujoco.rc @@ -1,6 +1,6 @@ 1 VERSIONINFO -FILEVERSION 3,3,1,0 -PRODUCTVERSION 3,3,1,0 +FILEVERSION 3,3,2,0 +PRODUCTVERSION 3,3,2,0 FILEOS 0x4 FILETYPE 0x1 { @@ -9,9 +9,9 @@ FILETYPE 0x1 BLOCK "040904b0" { VALUE "ProductName", "MuJoCo" - VALUE "ProductVersion", "3.3.1" + VALUE "ProductVersion", "3.3.2" VALUE "FileDescription", "MuJoCo" - VALUE "FileVersion", "3.3.1" + VALUE "FileVersion", "3.3.2" VALUE "InternalName", "mujoco.dll" VALUE "OriginalFilename", "mujoco.dll" VALUE "CompanyName", "Google DeepMind" diff --git a/dist/simulate.rc b/dist/simulate.rc index 7a2492ad..46e3c6de 100644 --- a/dist/simulate.rc +++ b/dist/simulate.rc @@ -1,8 +1,8 @@ MUJOCO ICON "mujoco.ico" 1 VERSIONINFO -FILEVERSION 3,3,1,0 -PRODUCTVERSION 3,3,1,0 +FILEVERSION 3,3,2,0 +PRODUCTVERSION 3,3,2,0 FILEOS 0x4 FILETYPE 0x1 { @@ -11,9 +11,9 @@ FILETYPE 0x1 BLOCK "040904b0" { VALUE "ProductName", "MuJoCo" - VALUE "ProductVersion", "3.3.1" + VALUE "ProductVersion", "3.3.2" VALUE "FileDescription", "MuJoCo" - VALUE "FileVersion", "3.3.1" + VALUE "FileVersion", "3.3.2" VALUE "InternalName", "simulate.exe" VALUE "OriginalFilename", "simulate.exe" VALUE "CompanyName", "Google DeepMind" diff --git a/doc/APIreference/APIglobals.rst b/doc/APIreference/APIglobals.rst index c116257a..64aa21f3 100644 --- a/doc/APIreference/APIglobals.rst +++ b/doc/APIreference/APIglobals.rst @@ -517,7 +517,7 @@ shown in the table below. Their names are in the format ``mjKEY_XXX``. They corr - Maximum number of UI rectangles. Defined in `mjui.h `_. * - ``mjVERSION_HEADER`` - - 331 + - 332 - The version of the MuJoCo headers; changes with every release. This is an integer equal to 100x the software version, so 210 corresponds to version 2.1. Defined in mujoco.h. The API function :ref:`mj_version` returns a number with the same meaning but for the compiled library. diff --git a/doc/unity.rst b/doc/unity.rst index 1f499b8f..5f73281f 100644 --- a/doc/unity.rst +++ b/doc/unity.rst @@ -37,14 +37,14 @@ _____ The MuJoCo app needs to be run at least once before the native library can be used, in order to register the library as a trusted binary. Then, copy the dynamic library file from -``/Applications/MuJoCo.app/Contents/Frameworks/mujoco.framework/Versions/Current/libmujoco.3.3.1.dylib`` (it can be +``/Applications/MuJoCo.app/Contents/Frameworks/mujoco.framework/Versions/Current/libmujoco.3.3.2.dylib`` (it can be found by browsing the contents of ``MuJoCo.app``) and rename it as ``mujoco.dylib``. Linux _____ Expand the ``tar.gz`` archive to ``~/.mujoco``. Then copy the dynamic library from -``~/.mujoco/mujoco-3.3.1/lib/libmujoco.so.3.3.1`` and rename it as ``libmujoco.so``. +``~/.mujoco/mujoco-3.3.2/lib/libmujoco.so.3.3.2`` and rename it as ``libmujoco.so``. Windows _______ diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index c17c7e47..b23f5d94 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -16,7 +16,7 @@ #define MUJOCO_MUJOCO_H_ // header version; should match the library version as returned by mj_version() -#define mjVERSION_HEADER 331 +#define mjVERSION_HEADER 332 // needed to define size_t, fabs and log10 #include diff --git a/mjx/pyproject.toml b/mjx/pyproject.toml index 1d102175..529fc068 100644 --- a/mjx/pyproject.toml +++ b/mjx/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name="mujoco-mjx" -version = "3.3.1" +version = "3.3.2" authors = [ {name = "Google DeepMind", email = "mujoco@deepmind.com"}, ] @@ -30,7 +30,7 @@ dependencies = [ "etils[epath]", "jax", "jaxlib", - "mujoco>=3.3.1.dev0", + "mujoco>=3.3.2.dev0", "scipy", "trimesh", ] @@ -41,9 +41,9 @@ mjx-viewer = "mujoco.mjx.viewer:main" [project.urls] Homepage = "https://github.com/google-deepmind/mujoco/tree/main/mjx" -Documentation = "https://mujoco.readthedocs.io/en/3.3.1" +Documentation = "https://mujoco.readthedocs.io/en/3.3.2" Repository = "https://github.com/google-deepmind/mujoco/tree/main/mjx" -Changelog = "https://mujoco.readthedocs.io/en/3.3.1/changelog.html" +Changelog = "https://mujoco.readthedocs.io/en/3.3.2/changelog.html" [tool.isort] force_single_line = true diff --git a/python/mujoco/CMakeLists.txt b/python/mujoco/CMakeLists.txt index ac15ccd5..faceca26 100644 --- a/python/mujoco/CMakeLists.txt +++ b/python/mujoco/CMakeLists.txt @@ -84,7 +84,7 @@ if(NOT TARGET mujoco) if(MUJOCO_FRAMEWORK) message("MuJoCo framework is at ${MUJOCO_FRAMEWORK}/mujoco.framework") set(MUJOCO_LIBRARY - ${MUJOCO_FRAMEWORK}/mujoco.framework/Versions/A/libmujoco.3.3.1.dylib + ${MUJOCO_FRAMEWORK}/mujoco.framework/Versions/A/libmujoco.3.3.2.dylib ) target_compile_options(mujoco INTERFACE -F${MUJOCO_FRAMEWORK}) endif() @@ -92,7 +92,7 @@ if(NOT TARGET mujoco) if(NOT MUJOCO_FRAMEWORK) find_library( - MUJOCO_LIBRARY mujoco mujoco.3.3.1 HINTS ${MUJOCO_LIBRARY_DIR} REQUIRED + MUJOCO_LIBRARY mujoco mujoco.3.3.2 HINTS ${MUJOCO_LIBRARY_DIR} REQUIRED ) find_path(MUJOCO_INCLUDE mujoco/mujoco.h HINTS ${MUJOCO_INCLUDE_DIR} REQUIRED) message("MuJoCo is at ${MUJOCO_LIBRARY}") diff --git a/python/mujoco/mjpython/Info.plist b/python/mujoco/mjpython/Info.plist index 4006fd6d..d7e0aa2f 100644 --- a/python/mujoco/mjpython/Info.plist +++ b/python/mujoco/mjpython/Info.plist @@ -7,13 +7,13 @@ CFBundleIdentifier org.mujoco.mjpython CFBundleVersion - 3.3.1 + 3.3.2 CFBundleGetInfoString - 3.3.1 + 3.3.2 CFBundleLongVersionString - 3.3.1 + 3.3.2 CFBundleShortVersionString - 3.3.1 + 3.3.2 CFBundleExecutable mjpython CFBundleIconFile diff --git a/python/pyproject.toml b/python/pyproject.toml index 98fc970e..23b973dd 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mujoco" -version = "3.3.1" +version = "3.3.2" authors = [ {name = "Google DeepMind", email = "mujoco@deepmind.com"}, ] @@ -35,9 +35,9 @@ dynamic = ["readme", "scripts"] [project.urls] Homepage = "https://github.com/google-deepmind/mujoco" -Documentation = "https://mujoco.readthedocs.io/en/3.3.1" +Documentation = "https://mujoco.readthedocs.io/en/3.3.2" Repository = "https://github.com/google-deepmind/mujoco" -Changelog = "https://mujoco.readthedocs.io/en/3.3.1/changelog.html" +Changelog = "https://mujoco.readthedocs.io/en/3.3.2/changelog.html" [tool.setuptools] include-package-data = false diff --git a/sample/CMakeLists.txt b/sample/CMakeLists.txt index 8273ed11..b1789412 100644 --- a/sample/CMakeLists.txt +++ b/sample/CMakeLists.txt @@ -24,7 +24,7 @@ set(MSVC_INCREMENTAL_DEFAULT ON) project( mujoco_samples - VERSION 3.3.1 + VERSION 3.3.2 DESCRIPTION "MuJoCo samples binaries" HOMEPAGE_URL "https://mujoco.org" ) diff --git a/simulate/CMakeLists.txt b/simulate/CMakeLists.txt index 1b9df797..ff64d734 100644 --- a/simulate/CMakeLists.txt +++ b/simulate/CMakeLists.txt @@ -29,7 +29,7 @@ set(MUJOCO_DEP_VERSION_lodepng project( mujoco_simulate - VERSION 3.3.1 + VERSION 3.3.2 DESCRIPTION "MuJoCo simulate binaries" HOMEPAGE_URL "https://mujoco.org" ) diff --git a/src/engine/engine_support.c b/src/engine/engine_support.c index 4f5d06af..b7fc4d7c 100644 --- a/src/engine/engine_support.c +++ b/src/engine/engine_support.c @@ -41,8 +41,8 @@ //-------------------------- Constants ------------------------------------------------------------- - #define mjVERSION 331 -#define mjVERSIONSTRING "3.3.1" + #define mjVERSION 332 +#define mjVERSIONSTRING "3.3.2" // names of disable flags const char* mjDISABLESTRING[mjNDISABLE] = { diff --git a/unity/Editor/Bindings/MujocoBinaryRetriever.cs b/unity/Editor/Bindings/MujocoBinaryRetriever.cs index a1721d2f..ba697540 100644 --- a/unity/Editor/Bindings/MujocoBinaryRetriever.cs +++ b/unity/Editor/Bindings/MujocoBinaryRetriever.cs @@ -37,7 +37,7 @@ public class MujocoBinaryRetriever { if (AssetDatabase.LoadMainAssetAtPath(mujocoPath + "/mujoco.dylib") == null) { File.Copy( "/Applications/MuJoCo.app/Contents/Frameworks" + - "/mujoco.framework/Versions/Current/libmujoco.3.3.1.dylib", + "/mujoco.framework/Versions/Current/libmujoco.3.3.2.dylib", mujocoPath + "/mujoco.dylib"); AssetDatabase.Refresh(); } @@ -45,7 +45,7 @@ public class MujocoBinaryRetriever { if (AssetDatabase.LoadMainAssetAtPath(mujocoPath + "/libmujoco.so") == null) { File.Copy( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + - "/.mujoco/mujoco-3.3.1/lib/libmujoco.so.3.3.1", + "/.mujoco/mujoco-3.3.2/lib/libmujoco.so.3.3.2", mujocoPath + "/libmujoco.so"); AssetDatabase.Refresh(); } diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index ca702ca0..592ca0da 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -110,7 +110,7 @@ public const int mjMAXLINEPNT = 1000; public const int mjMAXPLANEGRID = 200; public const bool THIRD_PARTY_MUJOCO_MJXMACRO_H_ = true; public const bool THIRD_PARTY_MUJOCO_MUJOCO_H_ = true; -public const int mjVERSION_HEADER = 331; +public const int mjVERSION_HEADER = 332; // ------------------------------------Enums------------------------------------ diff --git a/unity/package.json b/unity/package.json index 9c599fa3..95b769ec 100644 --- a/unity/package.json +++ b/unity/package.json @@ -1,7 +1,7 @@ { "name": "org.mujoco", "displayName": "MuJoCo", - "version": "3.3.1", + "version": "3.3.2", "description": "MuJoCo importer and runtime plug-in", "dependencies": {}, "author": { From 446a8f1742543f44f6afae7623f3393defa38d6c Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Fri, 11 Apr 2025 05:26:59 -0700 Subject: [PATCH 063/191] Add basic site support to Mjcf SdfFileFormat plugin. We will still need to tag these with a custom API schema to ease round-tripping but that will follow creation of such a schema. PiperOrigin-RevId: 746421793 Change-Id: I2df2209b4bc90d763718fb4bea8158b8598755c6 --- .../usd/plugins/mjcf/mujoco_to_usd.cc | 191 +++++++++++++----- src/experimental/usd/plugins/mjcf/utils.cc | 9 + src/experimental/usd/plugins/mjcf/utils.h | 3 + test/experimental/usd/plugins/mjcf/fixture.h | 14 ++ .../usd/plugins/mjcf/mjcf_file_format_test.cc | 52 +++++ 5 files changed, 214 insertions(+), 55 deletions(-) diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc index f1c8e5ee..44c8481f 100644 --- a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc @@ -122,6 +122,7 @@ using mujoco::usd::SetAttributeMetadata; using mujoco::usd::SetLayerMetadata; using mujoco::usd::SetPrimKind; using mujoco::usd::SetPrimMetadata; +using mujoco::usd::SetPrimPurpose; pxr::GfMatrix4d MujocoPosQuatToTransform(double *pos, double *quat) { pxr::GfQuatd quaternion = pxr::GfQuatd::GetIdentity(); @@ -598,34 +599,57 @@ class ModelWriter { return subcomponent_path; } - pxr::SdfPath WriteBoxGeom(const mjsGeom *geom, - const pxr::SdfPath &body_path) { + pxr::SdfPath WriteSiteGeom(const mjsSite *site, + const pxr::SdfPath &body_path) { auto name = - GetAvailablePrimName(*geom->name, pxr::UsdGeomTokens->Cube, body_path); + GetAvailablePrimName(*site->name, pxr::UsdGeomTokens->Cube, body_path); + + int site_idx = mjs_getId(site->element); + const mjtNum *size = &model_->site_size[site_idx * 3]; + pxr::SdfPath site_path; + switch (site->type) { + case mjGEOM_BOX: + site_path = WriteBox(name, size, body_path); + break; + case mjGEOM_SPHERE: + site_path = WriteSphere(name, size, body_path); + break; + case mjGEOM_CAPSULE: + site_path = WriteCapsule(name, size, body_path); + break; + case mjGEOM_CYLINDER: + site_path = WriteCylinder(name, size, body_path); + break; + case mjGEOM_ELLIPSOID: + site_path = WriteEllipsoid(name, size, body_path); + break; + default: + break; + } + + return site_path; + } + + pxr::SdfPath WriteBox(const pxr::TfToken &name, const mjtNum *size, + const pxr::SdfPath &body_path) { pxr::SdfPath box_path = CreatePrimSpec(data_, body_path, name, pxr::UsdGeomTokens->Cube); - - int geom_idx = mjs_getId(geom->element); - mjtNum *geom_size = &model_->geom_size[geom_idx * 3]; - // MuJoCo uses half sizes. pxr::SdfPath size_attr_path = CreateAttributeSpec(data_, box_path, pxr::UsdGeomTokens->size, pxr::SdfValueTypeNames->Float); - pxr::GfVec3f scale(static_cast(geom_size[0]), - static_cast(geom_size[1]), - static_cast(geom_size[2])); + pxr::GfVec3f scale(static_cast(size[0]), static_cast(size[1]), + static_cast(size[2])); SetAttributeDefault(data_, size_attr_path, 2.0); pxr::SdfPath extent_attr_path = CreateAttributeSpec(data_, box_path, pxr::UsdGeomTokens->extent, pxr::SdfValueTypeNames->Float3Array); - SetAttributeDefault( - data_, extent_attr_path, - pxr::VtArray({ - pxr::GfVec3f(-geom_size[0], -geom_size[1], -geom_size[2]), - pxr::GfVec3f(geom_size[0], geom_size[1], geom_size[2]), - })); + SetAttributeDefault(data_, extent_attr_path, + pxr::VtArray({ + pxr::GfVec3f(-size[0], -size[1], -size[2]), + pxr::GfVec3f(size[0], size[1], size[2]), + })); WriteScaleXformOp(box_path, scale); WriteXformOpOrder(box_path, @@ -633,65 +657,80 @@ class ModelWriter { return box_path; } - pxr::SdfPath WriteCapsuleGeom(const mjsGeom *geom, - const pxr::SdfPath &body_path) { - auto name = GetAvailablePrimName(*geom->name, pxr::UsdGeomTokens->Capsule, - body_path); - pxr::SdfPath capsule_path = - CreatePrimSpec(data_, body_path, name, pxr::UsdGeomTokens->Capsule); + pxr::SdfPath WriteBoxGeom(const mjsGeom *geom, + const pxr::SdfPath &body_path) { + auto name = + GetAvailablePrimName(*geom->name, pxr::UsdGeomTokens->Cube, body_path); int geom_idx = mjs_getId(geom->element); mjtNum *geom_size = &model_->geom_size[geom_idx * 3]; + return WriteBox(name, geom_size, body_path); + } + + pxr::SdfPath WriteCapsule(const pxr::TfToken name, const mjtNum *size, + const pxr::SdfPath &body_path) { + pxr::SdfPath capsule_path = + CreatePrimSpec(data_, body_path, name, pxr::UsdGeomTokens->Capsule); // MuJoCo uses half sizes. pxr::SdfPath radius_attr_path = CreateAttributeSpec(data_, capsule_path, pxr::UsdGeomTokens->radius, pxr::SdfValueTypeNames->Float); - SetAttributeDefault(data_, radius_attr_path, geom_size[0] * 2); + SetAttributeDefault(data_, radius_attr_path, size[0] * 2); pxr::SdfPath height_attr_path = CreateAttributeSpec(data_, capsule_path, pxr::UsdGeomTokens->height, pxr::SdfValueTypeNames->Float); - SetAttributeDefault(data_, height_attr_path, geom_size[1] * 2); + SetAttributeDefault(data_, height_attr_path, size[1] * 2); return capsule_path; } + pxr::SdfPath WriteCapsuleGeom(const mjsGeom *geom, + const pxr::SdfPath &body_path) { + auto name = GetAvailablePrimName(*geom->name, pxr::UsdGeomTokens->Capsule, + body_path); + int geom_idx = mjs_getId(geom->element); + mjtNum *geom_size = &model_->geom_size[geom_idx * 3]; + + return WriteCapsule(name, geom_size, body_path); + } + + pxr::SdfPath WriteCylinder(const pxr::TfToken name, const mjtNum *size, + const pxr::SdfPath &body_path) { + pxr::SdfPath cylinder_path = + CreatePrimSpec(data_, body_path, name, pxr::UsdGeomTokens->Cylinder); + + // MuJoCo uses half sizes. + pxr::SdfPath radius_attr_path = + CreateAttributeSpec(data_, cylinder_path, pxr::UsdGeomTokens->radius, + pxr::SdfValueTypeNames->Float); + SetAttributeDefault(data_, radius_attr_path, size[0] * 2); + + pxr::SdfPath height_attr_path = + CreateAttributeSpec(data_, cylinder_path, pxr::UsdGeomTokens->height, + pxr::SdfValueTypeNames->Float); + SetAttributeDefault(data_, height_attr_path, size[1] * 2); + return cylinder_path; + } + pxr::SdfPath WriteCylinderGeom(const mjsGeom *geom, const pxr::SdfPath &body_path) { auto name = GetAvailablePrimName(*geom->name, pxr::UsdGeomTokens->Cylinder, body_path); - pxr::SdfPath cylinder_path = - CreatePrimSpec(data_, body_path, name, pxr::UsdGeomTokens->Cylinder); int geom_idx = mjs_getId(geom->element); mjtNum *geom_size = &model_->geom_size[geom_idx * 3]; - - // MuJoCo uses half sizes. - pxr::SdfPath radius_attr_path = - CreateAttributeSpec(data_, cylinder_path, pxr::UsdGeomTokens->radius, - pxr::SdfValueTypeNames->Float); - SetAttributeDefault(data_, radius_attr_path, geom_size[0] * 2); - - pxr::SdfPath height_attr_path = - CreateAttributeSpec(data_, cylinder_path, pxr::UsdGeomTokens->height, - pxr::SdfValueTypeNames->Float); - SetAttributeDefault(data_, height_attr_path, geom_size[1] * 2); - return cylinder_path; + return WriteCylinder(name, geom_size, body_path); } - pxr::SdfPath WriteEllipsoidGeom(const mjsGeom *geom, - const pxr::SdfPath &body_path) { - auto name = GetAvailablePrimName(*geom->name, pxr::UsdGeomTokens->Sphere, - body_path); + pxr::SdfPath WriteEllipsoid(const pxr::TfToken name, const mjtNum *size, + const pxr::SdfPath &body_path) { pxr::SdfPath ellipsoid_path = CreatePrimSpec(data_, body_path, name, pxr::UsdGeomTokens->Sphere); - int geom_idx = mjs_getId(geom->element); - mjtNum *geom_size = &model_->geom_size[geom_idx * 3]; - - pxr::GfVec3f scale = {static_cast(geom_size[0] * 2), - static_cast(geom_size[1] * 2), - static_cast(geom_size[2] * 2)}; + pxr::GfVec3f scale = {static_cast(size[0] * 2), + static_cast(size[1] * 2), + static_cast(size[2] * 2)}; // MuJoCo uses half sizes. pxr::SdfPath radius_attr_path = @@ -705,28 +744,61 @@ class ModelWriter { return ellipsoid_path; } - pxr::SdfPath WriteSphereGeom(const mjsGeom *geom, - const pxr::SdfPath &body_path) { + pxr::SdfPath WriteEllipsoidGeom(const mjsGeom *geom, + const pxr::SdfPath &body_path) { auto name = GetAvailablePrimName(*geom->name, pxr::UsdGeomTokens->Sphere, body_path); - pxr::SdfPath sphere_path = - CreatePrimSpec(data_, body_path, name, pxr::UsdGeomTokens->Sphere); - int geom_idx = mjs_getId(geom->element); mjtNum *geom_size = &model_->geom_size[geom_idx * 3]; + return WriteEllipsoid(name, geom_size, body_path); + } + + pxr::SdfPath WriteSphere(const pxr::TfToken name, const mjtNum *size, + const pxr::SdfPath &body_path) { + pxr::SdfPath sphere_path = + CreatePrimSpec(data_, body_path, name, pxr::UsdGeomTokens->Sphere); + // MuJoCo uses half sizes. pxr::SdfPath radius_attr_path = CreateAttributeSpec(data_, sphere_path, pxr::UsdGeomTokens->radius, pxr::SdfValueTypeNames->Float); - SetAttributeDefault(data_, radius_attr_path, geom_size[0] * 2); + SetAttributeDefault(data_, radius_attr_path, size[0] * 2); return sphere_path; } + pxr::SdfPath WriteSphereGeom(const mjsGeom *geom, + const pxr::SdfPath &body_path) { + auto name = GetAvailablePrimName(*geom->name, pxr::UsdGeomTokens->Sphere, + body_path); + int geom_idx = mjs_getId(geom->element); + mjtNum *geom_size = &model_->geom_size[geom_idx * 3]; + return WriteSphere(name, geom_size, body_path); + } + + void WriteSite(mjsSite *site, const mjsBody *body) { + const int body_id = mjs_getId(body->element); + const auto &body_path = body_paths_[body_id]; + auto name = + GetAvailablePrimName(*site->name, pxr::UsdGeomTokens->Xform, body_path); + + // Create a geom primitive and set its purpose to guide so it won't be + // rendered. + pxr::SdfPath site_path = WriteSiteGeom(site, body_path); + SetPrimPurpose(data_, site_path, pxr::UsdGeomTokens->guide); + + int site_id = mjs_getId(site->element); + auto transform = MujocoPosQuatToTransform(&model_->site_pos[3 * site_id], + &model_->site_quat[4 * site_id]); + WriteTransformXformOp(site_path, transform); + + PrependToXformOpOrder( + site_path, pxr::VtArray{kTokens->xformOpTransform}); + } + void WriteGeom(mjsGeom *geom, const mjsBody *body) { const int body_id = mjs_getId(body->element); const auto &body_path = body_paths_[body_id]; - auto name = GetAvailablePrimName(*geom->name, kTokens->geom, body_path); pxr::SdfPath geom_path; int geom_id = mjs_getId(geom->element); @@ -798,6 +870,14 @@ class ModelWriter { geom_path, pxr::VtArray{kTokens->xformOpTransform}); } + void WriteSites(mjsBody *body) { + mjsSite *site = mjs_asSite(mjs_firstChild(body, mjOBJ_SITE, false)); + while (site) { + WriteSite(site, body); + site = mjs_asSite(mjs_nextChild(body, site->element, false)); + } + } + void WriteGeoms(mjsBody *body) { mjsGeom *geom = mjs_asGeom(mjs_firstChild(body, mjOBJ_GEOM, false)); while (geom) { @@ -957,6 +1037,7 @@ class ModelWriter { if (mjs_getId(body->element) != kWorldIndex) { WriteBody(body); } + WriteSites(body); WriteGeoms(body); WriteCameras(body); WriteLights(body); diff --git a/src/experimental/usd/plugins/mjcf/utils.cc b/src/experimental/usd/plugins/mjcf/utils.cc index 880cce55..e919a672 100644 --- a/src/experimental/usd/plugins/mjcf/utils.cc +++ b/src/experimental/usd/plugins/mjcf/utils.cc @@ -26,6 +26,7 @@ #include #include #include +#include namespace { template @@ -195,5 +196,13 @@ void SetPrimKind(pxr::SdfAbstractDataRefPtr& data, SetPrimMetadata(data, prim_path, pxr::TfToken("kind"), kind); } +void SetPrimPurpose(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& prim_path, pxr::TfToken purpose) { + const pxr::SdfPath attr_path = CreateAttributeSpec( + data, prim_path, pxr::UsdGeomTokens->purpose, + pxr::SdfValueTypeNames->Token, pxr::SdfVariabilityUniform); + SetAttributeDefault(data, attr_path, purpose); +} + } // namespace usd } // namespace mujoco diff --git a/src/experimental/usd/plugins/mjcf/utils.h b/src/experimental/usd/plugins/mjcf/utils.h index 4a2643ec..9f41a697 100644 --- a/src/experimental/usd/plugins/mjcf/utils.h +++ b/src/experimental/usd/plugins/mjcf/utils.h @@ -71,6 +71,9 @@ void ApplyApiSchema(pxr::SdfAbstractDataRefPtr& data, void SetPrimKind(pxr::SdfAbstractDataRefPtr& data, const pxr::SdfPath& prim_path, pxr::TfToken kind); +void SetPrimPurpose(pxr::SdfAbstractDataRefPtr& data, + const pxr::SdfPath& prim_path, pxr::TfToken purpose); + // Set the value specified by key on any field at field_path. template void SetField(pxr::SdfAbstractDataRefPtr& data, const pxr::SdfPath& field_path, diff --git a/test/experimental/usd/plugins/mjcf/fixture.h b/test/experimental/usd/plugins/mjcf/fixture.h index 64985fae..adfddbc4 100644 --- a/test/experimental/usd/plugins/mjcf/fixture.h +++ b/test/experimental/usd/plugins/mjcf/fixture.h @@ -29,12 +29,26 @@ #define EXPECT_PRIM_VALID(stage, path) \ EXPECT_TRUE((stage)->GetPrimAtPath(SdfPath(path)).IsValid()); +#define EXPECT_PRIM_IS_A(stage, path, type) \ + { \ + EXPECT_TRUE((stage)->GetPrimAtPath(SdfPath(path)).IsA()); \ + } + #define EXPECT_PRIM_KIND(stage, path, kind) \ { \ pxr::TfToken prim_kind; \ pxr::UsdModelAPI::Get(stage, SdfPath(path)).GetKind(&prim_kind); \ EXPECT_EQ(kind, prim_kind); \ } + +#define EXPECT_PRIM_PURPOSE(stage, path, purpose) \ + { \ + pxr::TfToken prim_purpose; \ + pxr::UsdGeomImageable::Get(stage, SdfPath(path)) \ + .GetPurposeAttr() \ + .Get(&prim_purpose); \ + EXPECT_EQ(prim_purpose, purpose); \ + } namespace mujoco { pxr::SdfLayerRefPtr LoadLayer(const std::string& xml); diff --git a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc index 8ca9bdba..df04db21 100644 --- a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc +++ b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc @@ -36,9 +36,14 @@ #include #include #include +#include +#include +#include #include #include #include +#include +#include PXR_NAMESPACE_OPEN_SCOPE // clang-format off @@ -383,5 +388,52 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestKindAuthoring) { EXPECT_PRIM_KIND(stage, "/test/root/tet", pxr::KindTokens->subcomponent); } +static constexpr char kSiteXml[] = R"( + + + + + + + + + + + + + )"; + +TEST_F(MjcfSdfFileFormatPluginTest, TestSitePrimsAuthored) { + pxr::SdfLayerRefPtr layer = LoadLayer(kSiteXml); + + auto stage = pxr::UsdStage::Open(layer); + EXPECT_PRIM_VALID(stage, "/test/box_site"); + EXPECT_PRIM_IS_A(stage, "/test/box_site", pxr::UsdGeomCube); + EXPECT_PRIM_VALID(stage, "/test/ball/ball/sphere_site"); + EXPECT_PRIM_IS_A(stage, "/test/ball/ball/sphere_site", pxr::UsdGeomSphere); + EXPECT_PRIM_VALID(stage, "/test/ball/ball/capsule_site"); + EXPECT_PRIM_IS_A(stage, "/test/ball/ball/capsule_site", pxr::UsdGeomCapsule); + EXPECT_PRIM_VALID(stage, "/test/ball/ball/cylinder_site"); + EXPECT_PRIM_IS_A(stage, "/test/ball/ball/cylinder_site", + pxr::UsdGeomCylinder); + EXPECT_PRIM_VALID(stage, "/test/ball/ball/ellipsoid_site"); + EXPECT_PRIM_IS_A(stage, "/test/ball/ball/ellipsoid_site", pxr::UsdGeomSphere); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestSitePrimsPurpose) { + pxr::SdfLayerRefPtr layer = LoadLayer(kSiteXml); + + auto stage = pxr::UsdStage::Open(layer); + EXPECT_PRIM_PURPOSE(stage, "/test/box_site", pxr::UsdGeomTokens->guide); + EXPECT_PRIM_PURPOSE(stage, "/test/ball/ball/sphere_site", + pxr::UsdGeomTokens->guide); + EXPECT_PRIM_PURPOSE(stage, "/test/ball/ball/capsule_site", + pxr::UsdGeomTokens->guide); + EXPECT_PRIM_PURPOSE(stage, "/test/ball/ball/cylinder_site", + pxr::UsdGeomTokens->guide); + EXPECT_PRIM_PURPOSE(stage, "/test/ball/ball/ellipsoid_site", + pxr::UsdGeomTokens->guide); +} + } // namespace } // namespace mujoco From 006c18b1d8822c41c1fedd2db8c349e23dd217fc Mon Sep 17 00:00:00 2001 From: Gabe Oppenheimer Date: Fri, 11 Apr 2025 07:36:20 -0700 Subject: [PATCH 064/191] Update numbering in the 3.3.1 release notes to give MJX a numbered bullet in the docs. PiperOrigin-RevId: 746455557 Change-Id: I04849ed1a5b97c0f6408d2333b13419e6cd35688 --- doc/changelog.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index d094094e..739cb30d 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -32,16 +32,16 @@ General MJX ^^^ -- Added tendon actuator force limits. +7. Added tendon actuator force limits. Bug fixes ^^^^^^^^^ -7. :ref:`mj_jacDot` was missing a term that accounts for the motion of the point with respect to +8. :ref:`mj_jacDot` was missing a term that accounts for the motion of the point with respect to which the Jacobian is computed, now fixed. -8. Fixed a bug that caused the parent frame of elements in the child worldbody to be incorrectly set when attaching an +9. Fixed a bug that caused the parent frame of elements in the child worldbody to be incorrectly set when attaching an mjSpec to a frame or a site. -9. Fixed a bug that caused shadow rendering to flicker on platforms (e.g., MacOS) that do not support ARB_clip_control. - Fixed in collaboration with :github:user:`aftersomemath`. +10. Fixed a bug that caused shadow rendering to flicker on platforms (e.g., MacOS) that do not support + ARB_clip_control. Fixed in collaboration with :github:user:`aftersomemath`. Python bindings ^^^^^^^^^^^^^^^ @@ -51,9 +51,9 @@ Python bindings :align: right :width: 240px -10. Added examples of procedural model creation to the Model Editing tutorial: |mjspec_colab| -11. Added support for nameless :ref:`mjSpec` objects in the ``bind`` method, see the corresponding :ref:`section` - in the documentation. +11. Added examples of procedural model creation to the Model Editing tutorial: |mjspec_colab| +12. Added support for nameless :ref:`mjSpec` objects in the ``bind`` method, see the corresponding + :ref:`section` in the documentation. .. |mjspec_colab| image:: https://colab.research.google.com/assets/colab-badge.svg :target: https://colab.research.google.com/github/google-deepmind/mujoco/blob/main/python/mjspec.ipynb From 8e7457d7c6c1cbda42ec39d2297d4f90b3396040 Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Fri, 11 Apr 2025 08:18:11 -0700 Subject: [PATCH 065/191] Initial support for writing UsdPhysics data starting with UsdPhysicsRigidbodyAPI support. The SDF_FORMAT_ARGS `usdMjcfToggleUsdPhysics` is introduced to toggle writing UsdPhysics or not when converting. Off by default PiperOrigin-RevId: 746467942 Change-Id: I26cb9a4c4ae2eef96163dab967a74f14940a2a6d --- .../usd/plugins/mjcf/mjcf_file_format.cc | 14 +++- .../usd/plugins/mjcf/mjcf_file_format.h | 9 ++- .../usd/plugins/mjcf/mujoco_to_usd.cc | 22 ++++-- .../usd/plugins/mjcf/mujoco_to_usd.h | 4 +- test/experimental/usd/plugins/mjcf/fixture.cc | 6 +- test/experimental/usd/plugins/mjcf/fixture.h | 15 ++++- .../usd/plugins/mjcf/mjcf_file_format_test.cc | 67 +++++++++++++++++++ 7 files changed, 122 insertions(+), 15 deletions(-) diff --git a/src/experimental/usd/plugins/mjcf/mjcf_file_format.cc b/src/experimental/usd/plugins/mjcf/mjcf_file_format.cc index 8c5858c9..904c84fd 100644 --- a/src/experimental/usd/plugins/mjcf/mjcf_file_format.cc +++ b/src/experimental/usd/plugins/mjcf/mjcf_file_format.cc @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -145,9 +146,18 @@ bool UsdMjcfFileFormat::CanRead(const std::string &filePath) const { } bool UsdMjcfFileFormat::ReadImpl(pxr::SdfLayer *layer, mjSpec *spec) const { - auto data = InitData(layer->GetFileFormatArguments()); + auto args = layer->GetFileFormatArguments(); - auto success = mujoco::usd::WriteSpecToData(spec, data); + bool toggleUsdPhysics = false; + const auto it = + args.find(UsdMjcfFileFormatTokens->ToggleUsdPhysicsArg.GetString()); + if (it != args.end()) { + toggleUsdPhysics = pxr::TfUnstringify(it->second); + } + + auto data = InitData(args); + + auto success = mujoco::usd::WriteSpecToData(spec, data, toggleUsdPhysics); mj_deleteSpec(spec); if (!success) { return false; diff --git a/src/experimental/usd/plugins/mjcf/mjcf_file_format.h b/src/experimental/usd/plugins/mjcf/mjcf_file_format.h index c5776ec4..5bc72337 100644 --- a/src/experimental/usd/plugins/mjcf/mjcf_file_format.h +++ b/src/experimental/usd/plugins/mjcf/mjcf_file_format.h @@ -27,10 +27,15 @@ PXR_NAMESPACE_OPEN_SCOPE +// clang-format off // The Id should realistically be mjcf, but the id and extension need to match. // So near term it just assumes the only .xml file we would import is MJCF. -#define USD_MJCF_FILE_FORMAT_TOKENS \ - ((Id, "xml"))((Version, "1.0"))((Target, "usd")) +#define USD_MJCF_FILE_FORMAT_TOKENS \ + ((Id, "xml")) \ + ((Version, "1.0")) \ + ((Target, "usd")) \ + ((ToggleUsdPhysicsArg, "usdMjcfToggleUsdPhysics")) +// clang-format on TF_DECLARE_PUBLIC_TOKENS(UsdMjcfFileFormatTokens, USD_MJCF_FILE_FORMAT_TOKENS); diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc index 44c8481f..effc26a3 100644 --- a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc @@ -49,6 +49,7 @@ #include #include #include +#include #include #include @@ -149,7 +150,7 @@ class ModelWriter { } ~ModelWriter() { mj_deleteModel(model_); } - void Write() { + void Write(bool write_physics) { // Create top level class holder. class_path_ = CreateClassSpec(data_, pxr::SdfPath::AbsoluteRootPath(), pxr::TfToken("__class__")); @@ -173,7 +174,7 @@ class ModelWriter { // Author mesh scope + mesh prims to be referenced. WriteMeshes(); WriteMaterials(); - WriteBodies(); + WriteBodies(write_physics); } private: @@ -977,7 +978,7 @@ class ModelWriter { } } - void WriteBody(mjsBody *body) { + void WriteBody(mjsBody *body, bool write_physics) { int body_id = mjs_getId(body->element); pxr::SdfPath parent_path = CreateParentIfNotExists(body, body_paths_[kWorldIndex], data_); @@ -990,6 +991,12 @@ class ModelWriter { // bodies subcomponents. SetPrimKind(data_, body_path, pxr::KindTokens->subcomponent); + // Apply the PhysicsRigidBodyAPI schema if we are writing physics. + if (write_physics) { + ApplyApiSchema(data_, body_path, + pxr::UsdPhysicsTokens->PhysicsRigidBodyAPI); + } + // Create classes if necessary mjsDefault *spec_default = mjs_getDefault(body->element); @@ -1028,14 +1035,14 @@ class ModelWriter { body_paths_[body_id] = body_path; } - void WriteBodies() { + void WriteBodies(bool write_physics) { mjsBody *body = mjs_asBody(mjs_firstElement(spec_, mjOBJ_BODY)); while (body) { // Only write a rigidbody if we are not the world body. // We fall through since the world body might have static // geom children. if (mjs_getId(body->element) != kWorldIndex) { - WriteBody(body); + WriteBody(body, write_physics); } WriteSites(body); WriteGeoms(body); @@ -1061,7 +1068,8 @@ class ModelWriter { namespace mujoco { namespace usd { -bool WriteSpecToData(mjSpec *spec, pxr::SdfAbstractDataRefPtr &data) { +bool WriteSpecToData(mjSpec *spec, pxr::SdfAbstractDataRefPtr &data, + bool write_physics) { // Create pseudo root first. data->CreateSpec(pxr::SdfPath::AbsoluteRootPath(), pxr::SdfSpecTypePseudoRoot); @@ -1072,7 +1080,7 @@ bool WriteSpecToData(mjSpec *spec, pxr::SdfAbstractDataRefPtr &data) { return false; } - ModelWriter(spec, model, data).Write(); + ModelWriter(spec, model, data).Write(write_physics); return true; } diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.h b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.h index 4c5934c6..9c0db6cf 100644 --- a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.h +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.h @@ -25,7 +25,9 @@ namespace usd { // Args: // spec: mjSpec built programmatically or via parsed XML. // data: SdfAbstractDataRefPtr that will be written to. -bool WriteSpecToData(mjSpec* spec, pxr::SdfAbstractDataRefPtr& data); +// write_physics: Whether to write physics data. +bool WriteSpecToData(mjSpec* spec, pxr::SdfAbstractDataRefPtr& data, + bool write_physics); } // namespace usd } // namespace mujoco diff --git a/test/experimental/usd/plugins/mjcf/fixture.cc b/test/experimental/usd/plugins/mjcf/fixture.cc index 19cd8e6e..74063da2 100644 --- a/test/experimental/usd/plugins/mjcf/fixture.cc +++ b/test/experimental/usd/plugins/mjcf/fixture.cc @@ -32,9 +32,11 @@ namespace mujoco { using pxr::SdfPath; -pxr::SdfLayerRefPtr LoadLayer(const std::string& xml) { +pxr::SdfLayerRefPtr LoadLayer( + const std::string& xml, + const pxr::SdfFileFormat::FileFormatArguments& args) { auto layer = pxr::SdfLayer::CreateAnonymous( - "test_layer", pxr::SdfFileFormat::FindByExtension("xml")); + "test_layer", pxr::SdfFileFormat::FindByExtension("xml"), args); layer->ImportFromString(xml); EXPECT_THAT(layer, testing::NotNull()); return layer; diff --git a/test/experimental/usd/plugins/mjcf/fixture.h b/test/experimental/usd/plugins/mjcf/fixture.h index adfddbc4..5c125930 100644 --- a/test/experimental/usd/plugins/mjcf/fixture.h +++ b/test/experimental/usd/plugins/mjcf/fixture.h @@ -21,6 +21,7 @@ #include "test/fixture.h" #include #include +#include #include #include #include @@ -34,6 +35,16 @@ EXPECT_TRUE((stage)->GetPrimAtPath(SdfPath(path)).IsA()); \ } +#define EXPECT_PRIM_API_APPLIED(stage, path, api) \ + { \ + EXPECT_TRUE((stage)->GetPrimAtPath(SdfPath(path)).HasAPI()); \ + } + +#define EXPECT_PRIM_API_NOT_APPLIED(stage, path, api) \ + { \ + EXPECT_FALSE((stage)->GetPrimAtPath(SdfPath(path)).HasAPI()); \ + } + #define EXPECT_PRIM_KIND(stage, path, kind) \ { \ pxr::TfToken prim_kind; \ @@ -51,7 +62,9 @@ } namespace mujoco { -pxr::SdfLayerRefPtr LoadLayer(const std::string& xml); +pxr::SdfLayerRefPtr LoadLayer( + const std::string& xml, + const pxr::SdfFileFormat::FileFormatArguments& args = {}); template void ExpectAttributeEqual(pxr::UsdStageRefPtr stage, const char* path, diff --git a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc index df04db21..0b3dca5d 100644 --- a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc +++ b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc @@ -44,6 +44,7 @@ #include #include #include +#include PXR_NAMESPACE_OPEN_SCOPE // clang-format off @@ -434,6 +435,72 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestSitePrimsPurpose) { EXPECT_PRIM_PURPOSE(stage, "/test/ball/ball/ellipsoid_site", pxr::UsdGeomTokens->guide); } +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsToggleSdfFormatArg) { + std::string xml_path = GetTestDataFilePath(kMeshObjPath); + + // Test that the default is no physics. + auto stage_no_physics = pxr::UsdStage::Open(xml_path); + EXPECT_THAT(stage_no_physics, testing::NotNull()); + EXPECT_PRIM_VALID(stage_no_physics, "/mesh_test/test_body/test_body"); + EXPECT_PRIM_API_NOT_APPLIED(stage_no_physics, + "/mesh_test/test_body/test_body", + pxr::UsdPhysicsRigidBodyAPI); + + // Then test that the physics flag enables physics. + std::string xml_path_physics_flag = + xml_path + ":SDF_FORMAT_ARGS:usdMjcfToggleUsdPhysics=true"; + auto stage_with_physics = pxr::UsdStage::Open(xml_path_physics_flag); + EXPECT_THAT(stage_with_physics, testing::NotNull()); + + EXPECT_PRIM_VALID(stage_with_physics, "/mesh_test/test_body/test_body"); + EXPECT_PRIM_API_APPLIED(stage_with_physics, "/mesh_test/test_body/test_body", + pxr::UsdPhysicsRigidBodyAPI); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsRigidBody) { + static constexpr char kXml[] = R"( + + + + + + + + + + + )"; + + pxr::SdfFileFormat::FileFormatArguments args; + args["usdMjcfToggleUsdPhysics"] = "true"; + pxr::SdfLayerRefPtr layer = LoadLayer(kXml, args); + auto stage = pxr::UsdStage::Open(layer); + + EXPECT_THAT(stage, testing::NotNull()); + EXPECT_PRIM_VALID(stage, "/physics_test"); + EXPECT_PRIM_VALID(stage, "/physics_test/test_body"); + EXPECT_PRIM_VALID(stage, "/physics_test/test_body/test_body"); + // USD does not allow nested rigidbodies so we put them as siblings to the + // first body in the hierarchy. + EXPECT_PRIM_VALID(stage, "/physics_test/test_body/test_body_2"); + + // The parent containing the body should not have the RigidBodyAPI applied. + EXPECT_PRIM_API_NOT_APPLIED(stage, "/physics_test/test_body", + pxr::UsdPhysicsRigidBodyAPI); + + EXPECT_PRIM_API_APPLIED(stage, "/physics_test/test_body/test_body", + pxr::UsdPhysicsRigidBodyAPI); + EXPECT_PRIM_API_APPLIED(stage, "/physics_test/test_body/test_body_2", + pxr::UsdPhysicsRigidBodyAPI); + + // Geoms should not have RigidBodyAPI applied either. + EXPECT_PRIM_API_NOT_APPLIED(stage, + "/physics_test/test_body/test_body/test_geom", + pxr::UsdPhysicsRigidBodyAPI); + EXPECT_PRIM_API_NOT_APPLIED(stage, + "/physics_test/test_body/test_body_2/test_geom_2", + pxr::UsdPhysicsRigidBodyAPI); +} } // namespace } // namespace mujoco From f317bd17c3494b954982de7bcf14030978d5013b Mon Sep 17 00:00:00 2001 From: Taylor Howell Date: Mon, 14 Apr 2025 03:50:16 -0700 Subject: [PATCH 066/191] Add tendon actuator force sensor to MJX. PiperOrigin-RevId: 747344916 Change-Id: Iab513ff953408c002216ffff12e3f8c825cedc10 --- doc/changelog.rst | 1 + doc/mjx.rst | 2 +- mjx/mujoco/mjx/_src/forward_test.py | 10 ++++++++-- mjx/mujoco/mjx/_src/sensor.py | 10 ++++++++++ mjx/mujoco/mjx/_src/types.py | 2 ++ .../mjx/test_data/actuator/tendon_force_clamp.xml | 6 ++++++ 6 files changed, 28 insertions(+), 3 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index 739cb30d..a71644aa 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -8,6 +8,7 @@ Upcoming version (not yet release) MJX ^^^ - Added inverse dynamics. +- Added tendon actuator force sensor. Version 3.3.1 (Apr 9, 2025) ---------------------------- diff --git a/doc/mjx.rst b/doc/mjx.rst index 0c00525f..e0106b5f 100644 --- a/doc/mjx.rst +++ b/doc/mjx.rst @@ -246,7 +246,7 @@ The following features are **fully supported** in MJX: ``FRAMEPOS``, ``FRAMEXAXIS``, ``FRAMEYAXIS``, ``FRAMEZAXIS``, ``FRAMEQUAT``, ``SUBTREECOM``, ``CLOCK``, ``VELOCIMETER``, ``GYRO``, ``JOINTVEL``, ``TENDONVEL``, ``ACTUATORVEL``, ``BALLANGVEL``, ``FRAMELINVEL``, ``FRAMEANGVEL``, ``SUBTREELINVEL``, ``SUBTREEANGMOM``, ``TOUCH``, ``ACCELEROMETER``, ``FORCE``, ``TORQUE``, - ``ACTUATORFRC``, ``JOINTACTFRC``, ``FRAMELINACC``, ``FRAMEANGACC`` + ``ACTUATORFRC``, ``JOINTACTFRC``, ``TENDONACTFRC``, ``FRAMELINACC``, ``FRAMEANGACC`` (``ACCELEROMETER``, ``FORCE``, ``TORQUE`` not supported with connect or weld equality constraints) The following features are **in development** and coming soon: diff --git a/mjx/mujoco/mjx/_src/forward_test.py b/mjx/mujoco/mjx/_src/forward_test.py index efdbcd65..f90fae1a 100644 --- a/mjx/mujoco/mjx/_src/forward_test.py +++ b/mjx/mujoco/mjx/_src/forward_test.py @@ -203,15 +203,21 @@ class ActuatorTest(parameterized.TestCase): mx = mjx.put_model(m) dx = mjx.put_data(m, d) - dx = dx.replace(ctrl=jp.array([1.0, 1.0, 1.0, -1.0, 1.0, -20.0, 5.0, -5.0])) + dx = dx.replace(ctrl=jp.array([1.0, 1.0, 1.0, -4.0, 1.0, -20.0, 5.0, -5.0])) dx = mjx.forward(mx, dx) _assert_eq( dx.actuator_force, - jp.array([1.0, 1.0, 1.0, -1.0, 1.0, -10.0, 5.0, -5.0]), + jp.array([1.0, 1.0, 1.0, -4.0 / 3.0, 1.0 / 3.0, -10.0, 5.0, -5.0]), 'actuator_force', ) + _assert_eq( + dx.sensordata, + jp.array([3.0, -1.0, -10.0, 0.0]), + 'sensordata', + ) + if __name__ == '__main__': absltest.main() diff --git a/mjx/mujoco/mjx/_src/sensor.py b/mjx/mujoco/mjx/_src/sensor.py index 7c3a5c87..24471ccc 100644 --- a/mjx/mujoco/mjx/_src/sensor.py +++ b/mjx/mujoco/mjx/_src/sensor.py @@ -27,6 +27,7 @@ from mujoco.mjx._src.types import DisableBit from mujoco.mjx._src.types import Model from mujoco.mjx._src.types import ObjType from mujoco.mjx._src.types import SensorType +from mujoco.mjx._src.types import TrnType # pylint: enable=g-importing-member import numpy as np @@ -552,6 +553,15 @@ def sensor_acc(m: Model, d: Data) -> Data: sensor = d.actuator_force[objid] elif sensor_type == SensorType.JOINTACTFRC: sensor = d.qfrc_actuator[m.jnt_dofadr[objid]] + elif sensor_type == SensorType.TENDONACTFRC: + force_mask = [ + (m.actuator_trntype == TrnType.TENDON) + & (m.actuator_trnid[:, 0] == tendon_id) + for tendon_id in objid + ] + force_ids = np.concatenate([np.nonzero(mask)[0] for mask in force_mask]) + force_mat = np.array(force_mask)[:, force_ids] + sensor = force_mat @ d.actuator_force[force_ids] elif sensor_type in (SensorType.FRAMELINACC, SensorType.FRAMEANGACC): objtype = m.sensor_objtype[idx] diff --git a/mjx/mujoco/mjx/_src/types.py b/mjx/mujoco/mjx/_src/types.py index 94587478..657cd524 100644 --- a/mjx/mujoco/mjx/_src/types.py +++ b/mjx/mujoco/mjx/_src/types.py @@ -370,6 +370,7 @@ class SensorType(enum.IntEnum): TORQUE: torque ACTUATORFRC: scalar actuator force JOINTACTFRC: scalar actuator force, measured at the joint + TENDONACTFRC: scalar actuator force, measured at the tendon FRAMELINACC: 3D linear acceleration FRAMEANGACC: 3D angular acceleration """ @@ -404,6 +405,7 @@ class SensorType(enum.IntEnum): TORQUE = mujoco.mjtSensor.mjSENS_TORQUE ACTUATORFRC = mujoco.mjtSensor.mjSENS_ACTUATORFRC JOINTACTFRC = mujoco.mjtSensor.mjSENS_JOINTACTFRC + TENDONACTFRC = mujoco.mjtSensor.mjSENS_TENDONACTFRC FRAMELINACC = mujoco.mjtSensor.mjSENS_FRAMELINACC FRAMEANGACC = mujoco.mjtSensor.mjSENS_FRAMEANGACC diff --git a/mjx/mujoco/mjx/test_data/actuator/tendon_force_clamp.xml b/mjx/mujoco/mjx/test_data/actuator/tendon_force_clamp.xml index 43d4c69b..51952b3a 100644 --- a/mjx/mujoco/mjx/test_data/actuator/tendon_force_clamp.xml +++ b/mjx/mujoco/mjx/test_data/actuator/tendon_force_clamp.xml @@ -46,4 +46,10 @@ + + + + + + From ec186430eff4086d4f18fc6143e8d10b1f512051 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Mon, 14 Apr 2025 08:05:13 -0700 Subject: [PATCH 067/191] Add setting plugin config properties via Python bindings. PiperOrigin-RevId: 747421357 Change-Id: Ie80f1a889f2f6ec457c1e16193fa0694e07d3fe9 --- python/mujoco/specs.cc | 16 ++++++++++++++++ python/mujoco/specs_test.py | 4 +++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/python/mujoco/specs.cc b/python/mujoco/specs.cc index 313de159..3adc549b 100644 --- a/python/mujoco/specs.cc +++ b/python/mujoco/specs.cc @@ -1032,6 +1032,22 @@ PYBIND11_MODULE(_specs, m) { }); mjsPlugin.def("delete", [](raw::MjsPlugin& self) { mjs_delete(self.element); }); + mjsPlugin.def_property( + "config", + [](raw::MjsPlugin& self) -> void { + throw pybind11::value_error("Reading plugin config is not supported."); + }, + [](raw::MjsPlugin& self, py::dict& config) { + std::map> config_attribs; + for (const auto& [key, value] : config) { + std::string key_str = key.cast(); + if (config_attribs.find(key_str) != config_attribs.end()) { + throw pybind11::value_error("Duplicate config key: " + key_str); + } + config_attribs[key_str] = value.cast(); + } + mjs_setPluginAttributes(&self, &config_attribs); + }); // ============================= MJVISUAL ==================================== mjVisual.def_property( "global_", diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py index 1b46bd4b..210d833f 100644 --- a/python/mujoco/specs_test.py +++ b/python/mujoco/specs_test.py @@ -841,8 +841,9 @@ class SpecsTest(absltest.TestCase): name='instance_name', plugin_name='mujoco.elasticity.cable', active=True, - info='info', + info='info' ) + plugin.config = {'twist': '10', 'bend': '1'} body = spec.worldbody.add_body() body.plugin = plugin @@ -858,6 +859,7 @@ class SpecsTest(absltest.TestCase): model = spec.compile() self.assertIsNotNone(model) self.assertEqual(model.nplugin, 1) + self.assertEqual(model.npluginattr, 7) self.assertEqual(model.body_plugin[1], 0) def test_recompile_error(self): From 71a70040570cc93d558938e66692059c8265427d Mon Sep 17 00:00:00 2001 From: Saran Tunyasuvunakool Date: Tue, 15 Apr 2025 04:02:15 -0700 Subject: [PATCH 068/191] Fix array extent parsing logic for introspect. Also fix the mjxmacro_test to correctly assert that all array extents defined in the X macros can be correctly parsed from field comments. PiperOrigin-RevId: 747808525 Change-Id: I0b08dc263f8a46118a07200ae044d6c491207245 --- doc/includes/references.h | 4 +- include/mujoco/mjdata.h | 4 +- .../introspect/codegen/generate_structs.py | 13 ++-- python/mujoco/introspect/structs.py | 65 ++++++++++++------- 4 files changed, 54 insertions(+), 32 deletions(-) diff --git a/doc/includes/references.h b/doc/includes/references.h index b97f2b93..35fd7ff1 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -347,8 +347,8 @@ struct mjData_ { mjtNum* qfrc_constraint; // constraint force (nv x 1) // computed by mj_inverse - mjtNum* qfrc_inverse; // net external force; should equal: (nv x 1) - // qfrc_applied + J'*xfrc_applied + qfrc_actuator + mjtNum* qfrc_inverse; // net external force; should equal: + // qfrc_applied + J'*xfrc_applied + qfrc_actuator (nv x 1) // computed by mj_sensorAcc/mj_rnePostConstraint if needed; rotation:translation format mjtNum* cacc; // com-based acceleration (nbody x 6) diff --git a/include/mujoco/mjdata.h b/include/mujoco/mjdata.h index 2b5eb7c6..b5b0373b 100644 --- a/include/mujoco/mjdata.h +++ b/include/mujoco/mjdata.h @@ -375,8 +375,8 @@ struct mjData_ { mjtNum* qfrc_constraint; // constraint force (nv x 1) // computed by mj_inverse - mjtNum* qfrc_inverse; // net external force; should equal: (nv x 1) - // qfrc_applied + J'*xfrc_applied + qfrc_actuator + mjtNum* qfrc_inverse; // net external force; should equal: + // qfrc_applied + J'*xfrc_applied + qfrc_actuator (nv x 1) // computed by mj_sensorAcc/mj_rnePostConstraint if needed; rotation:translation format mjtNum* cacc; // com-based acceleration (nbody x 6) diff --git a/python/mujoco/introspect/codegen/generate_structs.py b/python/mujoco/introspect/codegen/generate_structs.py index 1c50db22..24da6e2e 100644 --- a/python/mujoco/introspect/codegen/generate_structs.py +++ b/python/mujoco/introspect/codegen/generate_structs.py @@ -47,7 +47,7 @@ _EXCLUDED = ( 'mjResource_', ) -_ARRAY_COMMENT_PATTERN = re.compile(r'(.+?)\s\s+\((.+) x (.+)\)\Z') +_ARRAY_COMMENT_PATTERN = re.compile(r'(.+?)\s+\(([^\(\)]+) x ([^\(\)]+)\)\Z') def traverse(node, visitor): @@ -93,16 +93,19 @@ class MjStructVisitor: # No valid normalization, just parse the declname. return type_parsing.parse_type(declname) - def _make_comment(self, node: ClangJsonNode) -> str: + def _make_comment(self, node: ClangJsonNode, strip: bool = True) -> str: """Makes a comment string from a Clang AST FullComment node.""" kind = node.get('kind') if kind == 'TextComment': - return node['text'].replace('\N{NO-BREAK SPACE}', ' ').strip() + retval = node['text'].replace('\N{NO-BREAK SPACE}', ' ') else: strings = [] for child in node['inner']: - strings.append(self._make_comment(child)) - return ''.join(strings).strip() + strings.append(self._make_comment(child, strip=False)) + retval = ''.join(strings) + if strip: + retval = retval.strip() + return retval def _make_field( self, node: ClangJsonNode diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index 51799032..3db9e84c 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -1744,14 +1744,16 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=PointerType( inner_type=ValueType(name='mjtNum'), ), - doc='constraint solver reference:frictionloss (nv x mjNREF)', + doc='constraint solver reference:frictionloss', + array_extent=('nv', 'mjNREF'), ), StructFieldDecl( name='dof_solimp', type=PointerType( inner_type=ValueType(name='mjtNum'), ), - doc='constraint solver impedance:frictionloss (nv x mjNIMP)', + doc='constraint solver impedance:frictionloss', + array_extent=('nv', 'mjNIMP'), ), StructFieldDecl( name='dof_frictionloss', @@ -1958,7 +1960,8 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=PointerType( inner_type=ValueType(name='mjtNum'), ), - doc='detect contact if dist Date: Tue, 15 Apr 2025 05:39:40 -0700 Subject: [PATCH 069/191] Remove duplicate test_tendon_force_clamp from inverse_test.py. PiperOrigin-RevId: 747835971 Change-Id: I96ca7a2e3f71dbc05af238c269a81b41b51fa48b --- mjx/mujoco/mjx/_src/inverse_test.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/mjx/mujoco/mjx/_src/inverse_test.py b/mjx/mujoco/mjx/_src/inverse_test.py index 19a5deef..556f2a7e 100644 --- a/mjx/mujoco/mjx/_src/inverse_test.py +++ b/mjx/mujoco/mjx/_src/inverse_test.py @@ -111,21 +111,6 @@ class InverseTest(parameterized.TestCase): self.assertLess(fwdinv1, 1.0e-3) _assert_eq(dxinv.qacc, dx.qacc, 'qacc') - def test_tendon_force_clamp(self): - m = test_util.load_test_file('actuator/tendon_force_clamp.xml') - d = mujoco.MjData(m) - mx = mjx.put_model(m) - dx = mjx.put_data(m, d) - - dx = dx.replace(ctrl=jp.array([1.0, 1.0, 1.0, -1.0, 1.0, -20.0, 5.0, -5.0])) - dx = mjx.forward(mx, dx) - - _assert_eq( - dx.actuator_force, - jp.array([1.0, 1.0, 1.0, -1.0, 1.0, -10.0, 5.0, -5.0]), - 'actuator_force', - ) - if __name__ == '__main__': absltest.main() From 99490163df46f65a0cabcf8efef61b3164faa620 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Tue, 15 Apr 2025 08:33:00 -0700 Subject: [PATCH 070/191] Prevent NaN in cylinder SDF. Fixes #2573. The correction matches the one used in MJX (see _cylinder_grad in collision_sdf.py). PiperOrigin-RevId: 747887406 Change-Id: I1a4f1ac769adb96c98665eace7ce54fb09a7d3a2 --- src/engine/engine_collision_sdf.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/engine/engine_collision_sdf.c b/src/engine/engine_collision_sdf.c index c6fe8eba..f58f414a 100644 --- a/src/engine/engine_collision_sdf.c +++ b/src/engine/engine_collision_sdf.c @@ -158,7 +158,9 @@ static void geomGradient(mjtNum gradient[3], const mjModel* m, const mjData* d, e = mju_abs(x[2]); a[0] = c - size[0]; a[1] = e - size[1]; - mjtNum grada[3] = {x[0] / c, x[1] / c, x[2] / e}; + mjtNum grada[3] = {x[0] / mju_max(c, 1. / mjMAXVAL), + x[1] / mju_max(c, 1. / mjMAXVAL), + x[2] / mju_max(e, 1. / mjMAXVAL)}; int j = a[0] > a[1] ? 0 : 1; if (a[j] < 0) { gradient[0] = j == 0 ? grada[0] : 0; @@ -167,7 +169,7 @@ static void geomGradient(mjtNum gradient[3], const mjModel* m, const mjData* d, } else { b[0] = mju_max(a[0], 0); b[1] = mju_max(a[1], 0); - mjtNum bnorm = mju_norm(b, 2); + mjtNum bnorm = mju_max(mju_norm(b, 2), 1./mjMAXVAL); gradient[0] = grada[0] * b[0] / bnorm; gradient[1] = grada[1] * b[0] / bnorm; gradient[2] = grada[2] * b[1] / bnorm; From 7c8930af96f17c031c5b06d049bd10adff887841 Mon Sep 17 00:00:00 2001 From: Rahul Lashkari Date: Mon, 21 Apr 2025 15:38:50 +0530 Subject: [PATCH 071/191] fix(notebook): corrections in python notebooks --- python/mjspec.ipynb | 4 ++-- python/rollout.ipynb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/python/mjspec.ipynb b/python/mjspec.ipynb index 925da6bc..9213e2b4 100644 --- a/python/mjspec.ipynb +++ b/python/mjspec.ipynb @@ -10,7 +10,7 @@ "\n", "# \n", "\n", - "This notebook provides an introductory tutorial for model editing in MuJoCo using the `mjSpec` API. This notebook assumes that the reader is already familiar with MuJoCo basic concepts, as demostrated in the [introductory tutorial](https://github.com/google-deepmind/mujoco?tab=readme-ov-file#getting-started). Documentation for this API can be found in the [Model Editing](https://mujoco.readthedocs.io/en/latest/programming/modeledit.html) chapter in the documentation (C API) and in the [Python chapter](https://mujoco.readthedocs.io/en/latest/python.html#model-editing). Here we use the Python API.\n", + "This notebook provides an introductory tutorial for model editing in MuJoCo using the `mjSpec` API. This notebook assumes that the reader is already familiar with MuJoCo basic concepts, as demonstrated in the [introductory tutorial](https://github.com/google-deepmind/mujoco?tab=readme-ov-file#getting-started). Documentation for this API can be found in the [Model Editing](https://mujoco.readthedocs.io/en/latest/programming/modeledit.html) chapter in the documentation (C API) and in the [Python chapter](https://mujoco.readthedocs.io/en/latest/python.html#model-editing). Here we use the Python API.\n", "\n", "The goal of the API is to allow users to easily interact with and modify MuJoCo\n", "models in Python, similarly to what the JavaScript DOM does for HTML.\n", @@ -635,7 +635,7 @@ "outputs": [], "source": [ "def add_hfield(spec=None, hsize=10, vsize=4):\n", - " \"\"\" Function that adds a heighfield with countours\"\"\"\n", + " \"\"\" Function that adds a height field with contours\"\"\"\n", "\n", " # Initialize spec\n", " if spec is None:\n", diff --git a/python/rollout.ipynb b/python/rollout.ipynb index 61b21ffc..fe8ac49c 100644 --- a/python/rollout.ipynb +++ b/python/rollout.ipynb @@ -1599,7 +1599,7 @@ "\n", "Here we will produce a similar plot to compare MJX and with `rollout`. On a 5800X3D and 4090 the benchmark takes about 16.5 minutes to run.\n", "\n", - "**Note:** These results are not directly comparable since with the plot in the documentation because, in particular, the batch size was redued from 8192 to 4096 in order to fit the batch on a 4090." + "**Note:** These results are not directly comparable since with the plot in the documentation because, in particular, the batch size was reduced from 8192 to 4096 in order to fit the batch on a 4090." ] }, { From c2ac0d724e866045e89eee954cfaa22e5c52b4a5 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 22 Apr 2025 10:32:42 -0700 Subject: [PATCH 072/191] Add `mju_gather` and `mju_scatter`, private engine functions. PiperOrigin-RevId: 750245294 Change-Id: I3a14ccdd55a324d3fe206e388fb2a513772f5f5e --- src/engine/engine_core_smooth.c | 8 ++++---- src/engine/engine_forward.c | 22 +++++++--------------- src/engine/engine_inverse.c | 7 +++---- src/engine/engine_solver.c | 6 ++---- src/engine/engine_support.c | 10 ++++------ src/engine/engine_util_misc.c | 18 ++++++++++++++++++ src/engine/engine_util_misc.h | 6 ++++++ test/benchmark/factorI_benchmark_test.cc | 5 ++--- test/benchmark/inertia_benchmark_test.cc | 5 ++--- test/benchmark/solveLD_benchmark_test.cc | 7 +++---- test/engine/engine_core_smooth_test.cc | 19 +++++++------------ 11 files changed, 58 insertions(+), 55 deletions(-) diff --git a/src/engine/engine_core_smooth.c b/src/engine/engine_core_smooth.c index 0bb97dc7..09ea01c2 100644 --- a/src/engine/engine_core_smooth.c +++ b/src/engine/engine_core_smooth.c @@ -1646,11 +1646,11 @@ void mj_factorI_legacy(const mjModel* m, mjData* d, const mjtNum* M, mjtNum* qLD // sparse L'*D*L factorizaton of the inertia matrix M, assumed spd void mj_factorM(const mjModel* m, mjData* d) { TM_START; - int nM = m->nM; - for (int i=0; i < nM; i++) { - d->qLD[i] = d->qM[d->mapM2M[i]]; - } + + // gather LD <- M (legacy to CSR) and factorize in-place + mju_gather(d->qLD, d->qM, d->mapM2M, m->nM); mj_factorI(d->qLD, d->qLDiagInv, m->nv, d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + TM_ADD(mjTIMER_POS_INERTIA); } diff --git a/src/engine/engine_forward.c b/src/engine/engine_forward.c index 5771db32..42520046 100644 --- a/src/engine/engine_forward.c +++ b/src/engine/engine_forward.c @@ -840,9 +840,7 @@ void mj_EulerSkip(const mjModel* m, mjData* d, int skipfactor) { else { if (!skipfactor) { // qH = M + h*diag(B) - for (int i=0; i < nM; i++) { - d->qH[i] = d->qM[d->mapM2M[i]]; - } + mju_gather(d->qH, d->qM, d->mapM2M, nM); for (int i=0; i < nv; i++) { d->qH[d->M_rowadr[i] + d->M_rownnz[i] - 1] += m->opt.timestep * m->dof_damping[i]; } @@ -999,10 +997,8 @@ void mj_implicitSkip(const mjModel* m, mjData* d, int skipfactor) { // compute analytical derivative qDeriv mjd_smooth_vel(m, d, /* flg_bias = */ 1); - // set qLU = qM - for (int i=0; i < nD; i++) { - d->qLU[i] = d->qM[d->mapM2D[i]]; - } + // gather qLU <- qM (lower to full) + mju_gather(d->qLU, d->qM, d->mapM2D, nD); // set qLU = qM - dt*qDeriv mju_addToScl(d->qLU, d->qDeriv, -m->opt.timestep, m->nD); @@ -1022,19 +1018,15 @@ void mj_implicitSkip(const mjModel* m, mjData* d, int skipfactor) { // compute analytical derivative qDeriv; skip rne derivative mjd_smooth_vel(m, d, /* flg_bias = */ 0); - // modified mass matrix MhB = qDeriv[Lower] + // modified mass matrix: gather MhB <- qDeriv (full to lower) mjtNum* MhB = mjSTACKALLOC(d, nM, mjtNum); - for (int i=0; i < nM; i++) { - MhB[i] = d->qDeriv[d->mapD2M[i]]; - } + mju_gather(MhB, d->qDeriv, d->mapD2M, nM); // set MhB = M - dt*qDeriv mju_addScl(MhB, d->qM, MhB, -m->opt.timestep, nM); - // copy into qH - for (int i=0; i < nM; i++) { - d->qH[i] = MhB[d->mapM2M[i]]; - } + // gather qH <- MhB (legacy to CSR) + mju_gather(d->qH, MhB, d->mapM2M, nM); // factorize in-place mj_factorI(d->qH, d->qHDiagInv, nv, d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); diff --git a/src/engine/engine_inverse.c b/src/engine/engine_inverse.c index c4042401..57005f1e 100644 --- a/src/engine/engine_inverse.c +++ b/src/engine/engine_inverse.c @@ -31,6 +31,7 @@ #include "engine/engine_support.h" #include "engine/engine_util_blas.h" #include "engine/engine_util_errmem.h" +#include "engine/engine_util_misc.h" #include "engine/engine_util_sparse.h" // position-dependent computations @@ -115,10 +116,8 @@ static void mj_discreteAcc(const mjModel* m, mjData* d) { // compute qDeriv mjd_smooth_vel(m, d, /* flg_bias = */ 1); - // set qLU = qM - for (int i=0; i < nD; i++) { - d->qLU[i] = d->qM[d->mapM2D[i]]; - } + // gather qLU <- qM (lower to full) + mju_gather(d->qLU, d->qM, d->mapM2D, nD); // set qLU = qM - dt*qDeriv mju_addToScl(d->qLU, d->qDeriv, -m->opt.timestep, m->nD); diff --git a/src/engine/engine_solver.c b/src/engine/engine_solver.c index f23d8bf5..8e83ace5 100644 --- a/src/engine/engine_solver.c +++ b/src/engine/engine_solver.c @@ -1394,10 +1394,8 @@ static void MakeHessian(const mjModel* m, mjData* d, mjCGContext* ctx) { // sparse if (mj_isSparse(m)) { - // copy values of reduced sparse inertia matrix C - for (int i=0; i < m->nC; i++) { - ctx->C[i] = d->qM[d->mapM2C[i]]; - } + // gather C <- qM (legacy to CSR) + mju_gather(ctx->C, d->qM, d->mapM2C, m->nC); // initialize Hessian rowadr, rownnz mju_sqrMatTDSparseCount(ctx->H_rownnz, ctx->H_rowadr, nv, diff --git a/src/engine/engine_support.c b/src/engine/engine_support.c index b7fc4d7c..b7e2699d 100644 --- a/src/engine/engine_support.c +++ b/src/engine/engine_support.c @@ -1130,14 +1130,12 @@ void mj_addM(const mjModel* m, mjData* d, mjtNum* dst, int nC = m->nC; mj_markStack(d); - // create reduced sparse inertia matrix C + // gather C <- qM (legacy to CSR) mjtNum* C = mjSTACKALLOC(d, nC, mjtNum); - for (int i=0; i < nC; i++) { - C[i] = d->qM[d->mapM2C[i]]; - } + mju_gather(C, d->qM, d->mapM2C, nC); - mj_addMSparse(m, d, dst, rownnz, rowadr, colind, C, - d->C_rownnz, d->C_rowadr, d->C_colind); + // add to dst + mj_addMSparse(m, d, dst, rownnz, rowadr, colind, C, d->C_rownnz, d->C_rowadr, d->C_colind); mj_freeStack(d); } diff --git a/src/engine/engine_util_misc.c b/src/engine/engine_util_misc.c index 528b8f21..bec28aa3 100644 --- a/src/engine/engine_util_misc.c +++ b/src/engine/engine_util_misc.c @@ -1390,6 +1390,24 @@ void mju_n2d(double* res, const mjtNum* vec, int n) { +// gather +void mju_gather(mjtNum* restrict res, const mjtNum* restrict vec, const int* restrict ind, int n) { + for (int i=0; i < n; i++) { + res[i] = vec[ind[i]]; + } +} + + + +// scatter +void mju_scatter(mjtNum* restrict res, const mjtNum* restrict vec, const int* restrict ind, int n) { + for (int i=0; i < n; i++) { + res[ind[i]] = vec[i]; + } +} + + + // insertion sort, increasing order void mju_insertionSort(mjtNum* list, int n) { for (int i=1; i < n; i++) { diff --git a/src/engine/engine_util_misc.h b/src/engine/engine_util_misc.h index e266f814..de10f907 100644 --- a/src/engine/engine_util_misc.h +++ b/src/engine/engine_util_misc.h @@ -156,6 +156,12 @@ MJAPI void mju_d2n(mjtNum* res, const double* vec, int n); // convert from mjtNum to double MJAPI void mju_n2d(double* res, const mjtNum* vec, int n); +// gather +MJAPI void mju_gather(mjtNum* res, const mjtNum* vec, const int* ind, int n); + +// scatter +MJAPI void mju_scatter(mjtNum* res, const mjtNum* vec, const int* ind, int n); + // insertion sort, increasing order MJAPI void mju_insertionSort(mjtNum* list, int n); diff --git a/test/benchmark/factorI_benchmark_test.cc b/test/benchmark/factorI_benchmark_test.cc index dd73d3da..f2feea1e 100644 --- a/test/benchmark/factorI_benchmark_test.cc +++ b/test/benchmark/factorI_benchmark_test.cc @@ -19,6 +19,7 @@ #include #include #include "src/engine/engine_core_smooth.h" +#include "src/engine/engine_util_misc.h" #include "test/fixture.h" namespace mujoco { @@ -45,9 +46,7 @@ static void BM_factorI(benchmark::State& state, bool legacy, bool coil) { // M: mass matrix in CSR format mjtNum* M = mj_stackAllocNum(d, m->nM); - for (int i=0; i < m->nM; i++) { - M[i] = d->qM[d->mapM2M[i]]; - } + mju_gather(M, d->qM, d->mapM2M, m->nM); // LDlegacy: legacy LD matrix (size nM) mjtNum* LDlegacy = mj_stackAllocNum(d, m->nM); diff --git a/test/benchmark/inertia_benchmark_test.cc b/test/benchmark/inertia_benchmark_test.cc index e787fa24..3bdeddfd 100644 --- a/test/benchmark/inertia_benchmark_test.cc +++ b/test/benchmark/inertia_benchmark_test.cc @@ -20,6 +20,7 @@ #include #include #include "src/engine/engine_core_smooth.h" +#include "src/engine/engine_util_misc.h" #include "test/fixture.h" namespace mujoco { @@ -47,9 +48,7 @@ static void BM_solve(benchmark::State& state, SolveType type) { // M: mass matrix in CSR format mjtNum* M = mj_stackAllocNum(d, m->nM); - for (int i=0; i < m->nM; i++) { - M[i] = d->qM[d->mapM2M[i]]; - } + mju_gather(M, d->qM, d->mapM2M, m->nM); // LDlegacy: legacy LD matrix (size nM) mjtNum* LDlegacy = mj_stackAllocNum(d, m->nM); diff --git a/test/benchmark/solveLD_benchmark_test.cc b/test/benchmark/solveLD_benchmark_test.cc index 64276ba9..6204425f 100644 --- a/test/benchmark/solveLD_benchmark_test.cc +++ b/test/benchmark/solveLD_benchmark_test.cc @@ -19,6 +19,7 @@ #include #include #include "src/engine/engine_core_smooth.h" +#include "src/engine/engine_util_misc.h" #include "test/fixture.h" namespace mujoco { @@ -50,11 +51,9 @@ static void BM_solveLD(benchmark::State& state, bool featherstone, bool coil) { vec[i] = 0.2 + 0.3*i; } - // make legacy matrix + // scatter into legacy matrix mjtNum* LDlegacy = mj_stackAllocNum(d, m->nM); - for (int i=0; i < m->nM; i++) { - LDlegacy[d->mapM2M[i]] = d->qLD[i]; - } + mju_scatter(LDlegacy, d->qLD, d->mapM2M, m->nM); // benchmark while (state.KeepRunningBatch(kNumBenchmarkSteps)) { diff --git a/test/engine/engine_core_smooth_test.cc b/test/engine/engine_core_smooth_test.cc index 1d383922..1c71475a 100644 --- a/test/engine/engine_core_smooth_test.cc +++ b/test/engine/engine_core_smooth_test.cc @@ -15,6 +15,7 @@ // Tests for engine/engine_core_smooth.c. #include "src/engine/engine_core_smooth.h" +#include "src/engine/engine_util_misc.h" #include "src/engine/engine_util_sparse.h" #include @@ -754,11 +755,9 @@ TEST_F(CoreSmoothTest, SolveLDs) { int nv = m->nv; int nM = m->nM; - // copy M into LD: Legacy format + // scatter M into LD: Legacy format vector LDlegacy(nM); - for (int i=0; i < nM; i++) { - LDlegacy[d->mapM2M[i]] = d->qLD[i]; - } + mju_scatter(LDlegacy.data(), d->qLD, d->mapM2M, nM); // compare LD and LDs densified matrices vector LDdense(nv*nv); @@ -805,11 +804,9 @@ TEST_F(CoreSmoothTest, SolveLDmultipleVectors) { int nv = m->nv; int nM = m->nM; - // copy LD into LDlegacy: Legacy format + // scatter LD into LDlegacy: Legacy format vector LDlegacy(nM); - for (int i=0; i < nM; i++) { - LDlegacy[d->mapM2M[i]] = d->qLD[i]; - } + mju_scatter(LDlegacy.data(), d->qLD, d->mapM2M, nM); // compare n LD and LDs vector solve int n = 3; @@ -891,11 +888,9 @@ TEST_F(CoreSmoothTest, FactorIs) { qLDexpected[i] = qLDlegacy[d->mapM2M[i]]; } - // copy qM into qLD: CSR format + // gather qM into qLD: CSR format vector qLD(nM); - for (int i=0; i < nM; i++) { - qLD[i] = d->qM[d->mapM2M[i]]; // mj_factorI is in-place - } + mju_gather(qLD.data(), d->qM, d->mapM2M, nM); vector qLDiagInvExpected(d->qLDiagInv, d->qLDiagInv + nv); vector qLDiagInv(nv, 0); From 8f768be2dafbdd3a61bd664343362a72791e10dd Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Wed, 23 Apr 2025 03:26:40 -0700 Subject: [PATCH 073/191] Remove solver_nisland field from mjData. PiperOrigin-RevId: 750527660 Change-Id: I6b34944c92483ea2abc774df75efd868255a2bac --- doc/includes/references.h | 1 - include/mujoco/mjdata.h | 1 - include/mujoco/mjxmacro.h | 1 - python/mujoco/introspect/structs.py | 5 ----- sample/testspeed.cc | 2 +- simulate/simulate.cc | 6 +++--- src/engine/engine_forward.c | 4 ---- src/engine/engine_io.c | 1 - src/engine/engine_print.c | 3 +-- unity/Runtime/Bindings/MjBindings.cs | 1 - 10 files changed, 5 insertions(+), 20 deletions(-) diff --git a/doc/includes/references.h b/doc/includes/references.h index 35fd7ff1..89890efc 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -154,7 +154,6 @@ struct mjData_ { // solver statistics mjSolverStat solver[mjNISLAND*mjNSOLVER]; // solver statistics per island, per iteration - int solver_nisland; // number of islands processed by solver int solver_niter[mjNISLAND]; // number of solver iterations, per island int solver_nnz[mjNISLAND]; // number of nonzeros in Hessian or efc_AR, per island mjtNum solver_fwdinv[2]; // forward-inverse comparison: qfrc, efc diff --git a/include/mujoco/mjdata.h b/include/mujoco/mjdata.h index b5b0373b..d6f98775 100644 --- a/include/mujoco/mjdata.h +++ b/include/mujoco/mjdata.h @@ -182,7 +182,6 @@ struct mjData_ { // solver statistics mjSolverStat solver[mjNISLAND*mjNSOLVER]; // solver statistics per island, per iteration - int solver_nisland; // number of islands processed by solver int solver_niter[mjNISLAND]; // number of solver iterations, per island int solver_nnz[mjNISLAND]; // number of nonzeros in Hessian or efc_AR, per island mjtNum solver_fwdinv[2]; // forward-inverse comparison: qfrc, efc diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index 704ba353..7c73c598 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -777,7 +777,6 @@ X( size_t, maxuse_arena ) \ X( int, maxuse_con ) \ X( int, maxuse_efc ) \ - X( int, solver_nisland ) \ X( int, ncon ) \ X( int, ne ) \ X( int, nf ) \ diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index 3db9e84c..db237c74 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -4816,11 +4816,6 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), doc='solver statistics per island, per iteration', ), - StructFieldDecl( - name='solver_nisland', - type=ValueType(name='int'), - doc='number of islands processed by solver', - ), StructFieldDecl( name='solver_niter', type=ArrayType( diff --git a/sample/testspeed.cc b/sample/testspeed.cc index 84eb78bd..8074f68e 100644 --- a/sample/testspeed.cc +++ b/sample/testspeed.cc @@ -97,7 +97,7 @@ void simulate(int id, int nstep, mjtNum* ctrl) { // accumulate statistics contacts[id] += d[id]->ncon; constraints[id] += d[id]->nefc; - int nisland = d[id]->solver_nisland; + int nisland = mjMAX(1, mjMIN(d[id]->nisland, mjNISLAND)); if (nisland == 1 || nisland == 0) { iterations[id] += d[id]->solver_niter[0]; } else { diff --git a/simulate/simulate.cc b/simulate/simulate.cc index 8d5013a3..78a5e312 100644 --- a/simulate/simulate.cc +++ b/simulate/simulate.cc @@ -298,7 +298,7 @@ void UpdateProfiler(mj::Simulate* sim, const mjModel* m, const mjData* d) { memset(sim->figcost.linepnt, 0, mjMAXLINE*sizeof(int)); // number of islands that have diagnostics - int nisland = mjMIN(d->solver_nisland, mjNISLAND); + int nisland = mjMAX(1, mjMIN(d->nisland, mjNISLAND)); // iterate over islands for (int k=0; k < nisland; k++) { @@ -413,7 +413,7 @@ void UpdateProfiler(mj::Simulate* sim, const mjModel* m, const mjData* d) { static_cast(d->nefc), static_cast(sqrt_nnz), static_cast(d->ncon), - static_cast(solver_niter) + static_cast(solver_niter) / nisland }; // update figsize @@ -582,7 +582,7 @@ void UpdateInfoText(mj::Simulate* sim, const mjModel* m, const mjData* d, char tmp[20]; // number of islands with statistics - int nisland = mjMIN(d->solver_nisland, mjNISLAND); + int nisland = mjMAX(1, mjMIN(d->nisland, mjNISLAND)); // compute solver error (maximum over islands) mjtNum solerr = 0; diff --git a/src/engine/engine_forward.c b/src/engine/engine_forward.c index 42520046..3857bcae 100644 --- a/src/engine/engine_forward.c +++ b/src/engine/engine_forward.c @@ -730,7 +730,6 @@ void mj_fwdConstraint(const mjModel* m, mjData* d) { // solve using threads mj_solCG_island_multithreaded(m, d); } - d->solver_nisland = nisland; } // run solver over all constraints @@ -751,9 +750,6 @@ void mj_fwdConstraint(const mjModel* m, mjData* d) { default: mjERROR("unknown solver type %d", m->opt.solver); } - - // one (monolithic) island - d->solver_nisland = 1; } // save result for next step warmstart diff --git a/src/engine/engine_io.c b/src/engine/engine_io.c index 0f6878e1..78aed97d 100644 --- a/src/engine/engine_io.c +++ b/src/engine/engine_io.c @@ -1904,7 +1904,6 @@ static void _resetData(const mjModel* m, mjData* d, unsigned char debug_value) { memset(d->warning, 0, mjNWARNING*sizeof(mjWarningStat)); memset(d->timer, 0, mjNTIMER*sizeof(mjTimerStat)); memset(d->solver, 0, mjNSOLVER*mjNISLAND*sizeof(mjSolverStat)); - d->solver_nisland = 0; mju_zeroInt(d->solver_niter, mjNISLAND); mju_zeroInt(d->solver_nnz, mjNISLAND); mju_zero(d->solver_fwdinv, 2); diff --git a/src/engine/engine_print.c b/src/engine/engine_print.c index fcea96b2..49d0afa3 100644 --- a/src/engine/engine_print.c +++ b/src/engine/engine_print.c @@ -1008,9 +1008,8 @@ void mj_printFormattedData(const mjModel* m, const mjData* d, const char* filena // SOLVER STAT if (d->nefc) { fprintf(fp, "SOLVER STAT\n"); - fprintf(fp, " solver_nisland = %d\n", d->solver_nisland); printVector(" solver_fwdinv = ", d->solver_fwdinv, 2, fp, float_format); - int nisland_stat = mjMIN(d->solver_nisland, mjNISLAND); + int nisland_stat = mjMAX(1, mjMIN(d->nisland, mjNISLAND)); for (int island=0; island < nisland_stat; island++) { int niter_stat = mjMIN(mjNSOLVER, d->solver_niter[island]); if (niter_stat) { diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index 592ca0da..2af0cb51 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -4826,7 +4826,6 @@ public unsafe struct mjData_ { public mjSolverStat_ solver3997; public mjSolverStat_ solver3998; public mjSolverStat_ solver3999; - public int solver_nisland; public fixed int solver_niter[20]; public fixed int solver_nnz[20]; public fixed double solver_fwdinv[2]; From 9e72874801ac848691ef5d748fe2df681c40a035 Mon Sep 17 00:00:00 2001 From: Kyle Bayes Date: Wed, 23 Apr 2025 10:50:59 -0700 Subject: [PATCH 074/191] Remove EPA tolerance in the discrete case in nativeccd. PiperOrigin-RevId: 750649926 Change-Id: I8ab317cd6b227c9486a2916ae9c6445906c23401 --- src/engine/engine_collision_gjk.c | 125 +++++++++++------------ test/engine/engine_collision_gjk_test.cc | 64 ++++++++++++ 2 files changed, 123 insertions(+), 66 deletions(-) diff --git a/src/engine/engine_collision_gjk.c b/src/engine/engine_collision_gjk.c index 3c2df6c3..268de4c2 100644 --- a/src/engine/engine_collision_gjk.c +++ b/src/engine/engine_collision_gjk.c @@ -59,18 +59,18 @@ typedef struct { // polytope used in the Expanding Polytope Algorithm (EPA) typedef struct { - Vertex* verts; // list of vertices that make up the polytope - int nverts; // number of vertices - Face* faces; // list of faces that make up the polytope - int nfaces; // number of faces - int maxfaces; // max number of faces that can be stored in polytope - Face** map; // linear map storing faces - int nmap; // number of faces in map - struct Horizon { // polytope boundary edges that can be seen from w - int* indices; // indices of faces on horizon - int* edges; // corresponding edge of each face on the horizon - int nedges; // number of edges in horizon - mjtNum* w; // point where horizon is created + Vertex* verts; // list of vertices that make up the polytope + int nverts; // number of vertices + Face* faces; // list of faces that make up the polytope + int nfaces; // number of faces + int maxfaces; // max number of faces that can be stored in polytope + Face** map; // linear map storing faces + int nmap; // number of faces in map + struct Horizon { // polytope boundary edges that can be seen from w + int* indices; // indices of faces on horizon + int* edges; // corresponding edge of each face on the horizon + int nedges; // number of edges in horizon + const mjtNum* w; // point where horizon is created } horizon; } Polytope; @@ -291,25 +291,32 @@ static void gjk(mjCCDStatus* status, mjCCDObj* obj1, mjCCDObj* obj2) { // compute the support point in obj1 and obj2 for Minkowski difference -static inline void support(mjtNum s1[3], mjtNum s2[3], mjCCDObj* obj1, mjCCDObj* obj2, +static inline void support(Vertex* v, mjCCDObj* obj1, mjCCDObj* obj2, const mjtNum dir[3], const mjtNum dir_neg[3]) { // obj1 - obj1->support(s1, obj1, dir); + obj1->support(v->vert1, obj1, dir); if (obj1->margin > 0 && obj1->geom >= 0) { mjtNum margin = 0.5 * obj1->margin; - s1[0] += dir[0] * margin; - s1[1] += dir[1] * margin; - s1[2] += dir[2] * margin; + v->vert1[0] += dir[0] * margin; + v->vert1[1] += dir[1] * margin; + v->vert1[2] += dir[2] * margin; } // obj2 - obj2->support(s2, obj2, dir_neg); + obj2->support(v->vert2, obj2, dir_neg); if (obj2->margin > 0 && obj2->geom >= 0) { mjtNum margin = 0.5 * obj2->margin; - s2[0] += dir_neg[0] * margin; - s2[1] += dir_neg[1] * margin; - s2[2] += dir_neg[2] * margin; + v->vert2[0] += dir_neg[0] * margin; + v->vert2[1] += dir_neg[1] * margin; + v->vert2[2] += dir_neg[2] * margin; } + + // compute S_{A-B}(dir) = S_A(dir) - S_B(-dir) + sub3(v->vert, v->vert1, v->vert2); + + // copy vertex indices of discrete geoms + v->index1 = obj1->vertindex; + v->index2 = obj2->vertindex; } @@ -326,17 +333,7 @@ static void gjkSupport(Vertex* v, mjCCDObj* obj1, mjCCDObj* obj2, scl3(dir_neg, x_k, norm); scl3(dir, dir_neg, -1); } - - // compute S_{A-B}(dir) = S_A(dir) - S_B(-dir) - support(v->vert1, v->vert2, obj1, obj2, dir, dir_neg); - sub3(v->vert, v->vert1, v->vert2); - // copy mesh indices - if (obj1->vertindex >= 0) { - v->index1 = obj1->vertindex; - } - if (obj2->vertindex >= 0) { - v->index2 = obj2->vertindex; - } + support(v, obj1, obj2, dir, dir_neg); } @@ -356,16 +353,7 @@ static int epaSupport(Polytope* pt, mjCCDObj* obj1, mjCCDObj* obj2, int n = pt->nverts++; Vertex* v = pt->verts + n; - - // compute S_{A-B}(dir) = S_A(dir) - S_B(-dir) - support(v->vert1, v->vert2, obj1, obj2, dir, dir_neg); - sub3(v->vert, v->vert1, v->vert2); - if (obj1->vertindex >= 0) { - v->index1 = obj1->vertindex; - } - if (obj2->vertindex >= 0) { - v->index2 = obj2->vertindex; - } + support(v, obj1, obj2, dir, dir_neg); return n; } @@ -375,15 +363,7 @@ static int epaSupport(Polytope* pt, mjCCDObj* obj1, mjCCDObj* obj2, static void gjkIntersectSupport(Vertex* v, mjCCDObj* obj1, mjCCDObj* obj2, const mjtNum dir[3]) { mjtNum dir_neg[3] = {-dir[0], -dir[1], -dir[2]}; - // compute S_{A-B}(dir) = S_A(dir) - S_B(-dir) - support(v->vert1, v->vert2, obj1, obj2, dir, dir_neg); - sub3(v->vert, v->vert1, v->vert2); - if (obj1->vertindex >= 0) { - v->index1 = obj1->vertindex; - } - if (obj2->vertindex >= 0) { - v->index2 = obj2->vertindex; - } + support(v, obj1, obj2, dir, dir_neg); } @@ -1135,9 +1115,7 @@ static int polytope3(Polytope* pt, mjCCDStatus* status, mjCCDObj* obj1, mjCCDObj return mjEPA_P3_ORIGIN_ON_FACE; } - - // if the origin is on the affine hull of any of the faces then the origin is not in the - // hexahedron or the hexahedron is degenerate + // populate face map for (int i = 0; i < 6; i++) { pt->map[i] = pt->faces + i; pt->faces[i].index = i; @@ -1177,6 +1155,7 @@ static int polytope4(Polytope* pt, mjCCDStatus* status, mjCCDObj* obj1, mjCCDObj return mjEPA_P4_MISSING_ORIGIN; } + // populate face map for (int i = 0; i < 4; i++) { pt->map[i] = pt->faces + i; pt->faces[i].index = i; @@ -1186,16 +1165,10 @@ static int polytope4(Polytope* pt, mjCCDStatus* status, mjCCDObj* obj1, mjCCDObj } - // make a copy of vertex in polytope and return its index static inline int insertVertex(Polytope* pt, const Vertex* v) { int n = pt->nverts++; - Vertex* new_v = pt->verts + n; - copy3(new_v->vert1, v->vert1); - copy3(new_v->vert2, v->vert2); - new_v->index1 = v->index1; - new_v->index2 = v->index2; - sub3(new_v->vert, v->vert1, v->vert2); + pt->verts[n] = *v; return n; } @@ -1344,10 +1317,17 @@ static void epaWitness(const Polytope* pt, const Face* face, mjtNum x1[3], mjtNu // return a face of the expanded polytope that best approximates the pentration depth // witness points are in status->{x1, x2} static Face* epa(mjCCDStatus* status, Polytope* pt, mjCCDObj* obj1, mjCCDObj* obj2) { - mjtNum tolerance = status->tolerance, lower2, upper = mjMAX_LIMIT, upper2 = mjMAX_LIMIT; - int k, kmax = status->max_iterations; + mjtNum upper = mjMAX_LIMIT, upper2 = mjMAX_LIMIT, lower2; Face* face = NULL, *pface = NULL; // face closest to origin + mjtNum tolerance = status->tolerance; + int discrete = discreteGeoms(obj1, obj2); + // tolerance is not used for discrete geoms + if (discrete && sizeof(mjtNum) == sizeof(double)) { + tolerance = mjMINVAL; + } + + int k, kmax = status->max_iterations; for (k = 0; k < kmax; k++) { pface = face; @@ -1375,8 +1355,8 @@ static Face* epa(mjCCDStatus* status, Polytope* pt, mjCCDObj* obj1, mjCCDObj* ob // compute support point w from the closest face's normal mjtNum lower = mju_sqrt(lower2); int wi = epaSupport(pt, obj1, obj2, face->v, lower); - mjtNum* w = pt->verts[wi].vert; - mjtNum upper_k = dot3(face->v, w) / lower; // upper bound for kth iteration + const Vertex* w = pt->verts + wi; + mjtNum upper_k = dot3(face->v, w->vert) / lower; // upper bound for kth iteration if (upper_k < upper) { upper = upper_k; upper2 = upper * upper; @@ -1385,7 +1365,20 @@ static Face* epa(mjCCDStatus* status, Polytope* pt, mjCCDObj* obj1, mjCCDObj* ob break; } - pt->horizon.w = w; + // check if vertex w is a repeated support point + if (discrete) { + int i = 0, nverts = pt->nverts - 1; + for (; i < nverts; i++) { + if (w->index1 == pt->verts[i].index1 && w->index2 == pt->verts[i].index2) { + break; + } + } + if (i != nverts) { + break; + } + } + + pt->horizon.w = w->vert; horizon(pt, face); // unrecoverable numerical issue; at least one face was deleted so nedges is 3 or more diff --git a/test/engine/engine_collision_gjk_test.cc b/test/engine/engine_collision_gjk_test.cc index 5eb5f6df..9237aef4 100644 --- a/test/engine/engine_collision_gjk_test.cc +++ b/test/engine/engine_collision_gjk_test.cc @@ -1220,6 +1220,70 @@ TEST_F(MjGjkTest, BoxBoxMultiCCD13) { mj_deleteModel(model); } +TEST_F(MjGjkTest, BoxBoxMultiCCD14) { + static constexpr char xml[] = R"( + + + + + + )"; + + std::array error; + mjModel* model = LoadModelFromString(xml, error.data(), error.size()); + ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error.data(); + + mjData* data = mj_makeData(model); + mj_forward(model, data); + + mjtNum* xpos = data->geom_xpos; + mjtNum* xmat = data->geom_xmat; + + xmat[0] = 0.9999999980312528347070610834634862840176; + xmat[1] = 0.0000179109150612445166097282805983681442; + xmat[2] = -0.0000601389470252842008382576644009986921; + xmat[3] = -0.0000179108686851742081238159781664265324; + xmat[4] = 0.9999999998393023226128661917755380272865; + xmat[5] = 0.0000007716871733595438989517368948145570; + xmat[6] = 0.0000601389608372434404702858157243383630; + xmat[7] = -0.0000007706100310572527002924239115932981; + xmat[8] = 0.9999999981913554325529958077822811901569; + + xpos[0] = 0.0002051257133161473724877743585182088282; + xpos[1] = 0.0000051793157380883478958571650152542531; + xpos[2] = -0.0800031938952457943869944756443146616220; + + xpos = data->geom_xpos + 3; + xmat = data->geom_xmat + 9; + + xmat[0] = 0.9999999606378873195922096783760935068130; + xmat[1] = -0.0000186818570733572177707156047876679850; + xmat[2] = -0.0002799557310143530259108346491814245383; + xmat[3] = 0.0000186853252997592718994551708178164517; + xmat[4] = 0.9999999997487241110150080203311517834663; + xmat[5] = 0.0000123858711158191162315369768243122905; + xmat[6] = 0.0002799554995529331168427344955773605761; + xmat[7] = -0.0000123911016921886008170612322731862776; + xmat[8] = 0.9999999607356884201436741932411678135395; + + xpos[0] = 0.0002145111032389043976328218965576866140; + xpos[1] = -0.0000051338999751368759734112059978095033; + xpos[2] = -0.0400059009625639144802633495601185131818; + + int g1 = mj_name2id(model, mjOBJ_GEOM, "geom1"); + int g2 = mj_name2id(model, mjOBJ_GEOM, "geom2"); + + mjCCDStatus status; + std::vector dir, pos; + mjtNum dist; + int ncons = Penetration(status, dist, dir, pos, model, data, g1, g2, 0, 8); + + EXPECT_EQ(ncons, 4); + + mj_deleteData(data); + mj_deleteModel(model); +} + TEST_F(MjGjkTest, SmallBoxMesh) { static constexpr char xml[] = R"( From 8df3614781bb68cb31a2845c0ea360a9d0d1e2af Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Thu, 24 Apr 2025 07:53:58 -0700 Subject: [PATCH 075/191] Add support for plane geometry. PiperOrigin-RevId: 750994019 Change-Id: Ia3b085bed5b8e2fcdb00ffc69dd5551a7fa3c30d --- .../usd/plugins/mjcf/mujoco_to_usd.cc | 40 +++++++++++++ test/experimental/usd/plugins/mjcf/fixture.h | 5 +- .../usd/plugins/mjcf/mjcf_file_format_test.cc | 58 +++++++++++++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc index effc26a3..25757654 100644 --- a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc @@ -777,6 +777,43 @@ class ModelWriter { return WriteSphere(name, geom_size, body_path); } + pxr::SdfPath WritePlane(const pxr::TfToken &name, const mjtNum *size, + const pxr::SdfPath &body_path) { + pxr::SdfPath plane_path = + CreatePrimSpec(data_, body_path, name, pxr::UsdGeomTokens->Plane); + + // MuJoCo uses half sizes. + // Note that UsdGeomPlane is infinite for simulation purposes but can have + // width/length for visualization, same as MuJoCo. + double width = size[0] * 2.0; + double length = size[1] * 2.0; + + pxr::SdfPath width_attr_path = + CreateAttributeSpec(data_, plane_path, pxr::UsdGeomTokens->width, + pxr::SdfValueTypeNames->Double); + SetAttributeDefault(data_, width_attr_path, width); + + pxr::SdfPath length_attr_path = + CreateAttributeSpec(data_, plane_path, pxr::UsdGeomTokens->length, + pxr::SdfValueTypeNames->Double); + SetAttributeDefault(data_, length_attr_path, length); + + // MuJoCo plane is always a XY plane with +Z up. + // UsdGeomPlane is also a XY plane if axis is 'Z', which is default. + // So no need to set axis attribute explicitly. + + return plane_path; + } + + pxr::SdfPath WritePlaneGeom(const mjsGeom *geom, + const pxr::SdfPath &body_path) { + auto name = + GetAvailablePrimName(*geom->name, pxr::UsdGeomTokens->Plane, body_path); + int geom_idx = mjs_getId(geom->element); + mjtNum *geom_size = &model_->geom_size[geom_idx * 3]; + return WritePlane(name, geom_size, body_path); + } + void WriteSite(mjsSite *site, const mjsBody *body) { const int body_id = mjs_getId(body->element); const auto &body_path = body_paths_[body_id]; @@ -804,6 +841,9 @@ class ModelWriter { pxr::SdfPath geom_path; int geom_id = mjs_getId(geom->element); switch (geom->type) { + case mjGEOM_PLANE: + geom_path = WritePlaneGeom(geom, body_path); + break; case mjGEOM_MESH: geom_path = WriteMeshGeom(geom, body_path); break; diff --git a/test/experimental/usd/plugins/mjcf/fixture.h b/test/experimental/usd/plugins/mjcf/fixture.h index 5c125930..9ab5ffe1 100644 --- a/test/experimental/usd/plugins/mjcf/fixture.h +++ b/test/experimental/usd/plugins/mjcf/fixture.h @@ -70,10 +70,11 @@ template void ExpectAttributeEqual(pxr::UsdStageRefPtr stage, const char* path, const T& value) { auto attr = stage->GetAttributeAtPath(pxr::SdfPath(path)); - EXPECT_TRUE(attr.IsValid()); + EXPECT_TRUE(attr.IsValid()) << "Attribute " << path << " is not valid"; T attr_value; attr.Get(&attr_value); - EXPECT_EQ(attr_value, value); + EXPECT_EQ(attr_value, value) << "Attribute " << path << " has value " + << attr_value << ". Expected: " << value; } // Specialization for SdfAssetPath, so that we can compare only the asset path diff --git a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc index 0b3dca5d..f5caeb19 100644 --- a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc +++ b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -389,6 +390,62 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestKindAuthoring) { EXPECT_PRIM_KIND(stage, "/test/root/tet", pxr::KindTokens->subcomponent); } +TEST_F(MjcfSdfFileFormatPluginTest, TestGeomsPrims) { + static constexpr char kXml[] = R"( + + + + + + + + + + + )"; + + pxr::SdfLayerRefPtr layer = LoadLayer(kXml); + auto stage = pxr::UsdStage::Open(layer); + + // Note that all sizes are multiplied by 2 because Mujoco uses half sizes. + + // Plane + EXPECT_PRIM_VALID(stage, "/test/plane_geom"); + EXPECT_PRIM_IS_A(stage, "/test/plane_geom", pxr::UsdGeomPlane); + ExpectAttributeEqual(stage, "/test/plane_geom.width", 2 * 10.0); + ExpectAttributeEqual(stage, "/test/plane_geom.length", 2 * 20.0); + // Box + EXPECT_PRIM_VALID(stage, "/test/box_geom"); + EXPECT_PRIM_IS_A(stage, "/test/box_geom", pxr::UsdGeomCube); + // Box is a special case, it uses a UsdGeomCube and scales it with + // xformOp:scale. The radius is always set to 2. + ExpectAttributeEqual(stage, "/test/box_geom.size", 2.0); + ExpectAttributeEqual(stage, "/test/box_geom.xformOp:scale", + pxr::GfVec3f(10.0, 20.0, 30.0)); + // Sphere + EXPECT_PRIM_VALID(stage, "/test/sphere_geom"); + EXPECT_PRIM_IS_A(stage, "/test/sphere_geom", pxr::UsdGeomSphere); + ExpectAttributeEqual(stage, "/test/sphere_geom.radius", 2 * 10.0); + // Capsule + EXPECT_PRIM_VALID(stage, "/test/capsule_geom"); + EXPECT_PRIM_IS_A(stage, "/test/capsule_geom", pxr::UsdGeomCapsule); + ExpectAttributeEqual(stage, "/test/capsule_geom.radius", 2 * 10.0); + ExpectAttributeEqual(stage, "/test/capsule_geom.height", 2 * 20.0); + // Cylinder + EXPECT_PRIM_VALID(stage, "/test/cylinder_geom"); + EXPECT_PRIM_IS_A(stage, "/test/cylinder_geom", pxr::UsdGeomCylinder); + ExpectAttributeEqual(stage, "/test/cylinder_geom.radius", 2 * 10.0); + ExpectAttributeEqual(stage, "/test/cylinder_geom.height", 2 * 20.0); + // Ellipsoid + EXPECT_PRIM_VALID(stage, "/test/ellipsoid_geom"); + // Ellipsoid is a special case, it uses a UsdGeomSphere and scales it with + // xformOp:scale. The radius is always set to 1. + EXPECT_PRIM_IS_A(stage, "/test/ellipsoid_geom", pxr::UsdGeomSphere); + ExpectAttributeEqual(stage, "/test/ellipsoid_geom.radius", 1.0); + ExpectAttributeEqual(stage, "/test/ellipsoid_geom.xformOp:scale", + pxr::GfVec3f(2.0 * 10.0, 2.0 * 20.0, 2.0 * 30.0)); +} + static constexpr char kSiteXml[] = R"( @@ -435,6 +492,7 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestSitePrimsPurpose) { EXPECT_PRIM_PURPOSE(stage, "/test/ball/ball/ellipsoid_site", pxr::UsdGeomTokens->guide); } + TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsToggleSdfFormatArg) { std::string xml_path = GetTestDataFilePath(kMeshObjPath); From d3bc0544d39c0933067e493f4c1c7ebfd15ae9f3 Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Thu, 24 Apr 2025 08:58:05 -0700 Subject: [PATCH 076/191] Add basic support for geom rgba. Adds primvar:displayColor/primvar:displayOpacity to the geom prim if its rgba attribute differs from the default (0.5, 0.5, 0.5, 1.0). This sets the color and opacity directly on the prim with no need for an external material, making it consistent with how rgba is used in mujoco. PiperOrigin-RevId: 751013128 Change-Id: I059943ec5b266e62c3696d9a0ac4f2033db9e07f --- .../usd/plugins/mjcf/mujoco_to_usd.cc | 23 +++++++++ test/experimental/usd/plugins/mjcf/fixture.h | 7 +++ .../usd/plugins/mjcf/mjcf_file_format_test.cc | 47 +++++++++++++++++++ 3 files changed, 77 insertions(+) diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc index 25757654..e443149c 100644 --- a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc @@ -897,6 +897,29 @@ class ModelWriter { } } + // If geom rgba is not the default (0.5, 0.5, 0.5, 1), then set the + // displayColor attribute. + // No effort is made to properly handle the interaction between geom rgba + // and the material if both are specified. + if (geom->rgba[0] != 0.5f || geom->rgba[1] != 0.5f || + geom->rgba[2] != 0.5f || geom->rgba[3] != 1.0f) { + // Set the displayColor attribute. + pxr::SdfPath display_color_attr = CreateAttributeSpec( + data_, geom_path, pxr::UsdGeomTokens->primvarsDisplayColor, + pxr::SdfValueTypeNames->Color3fArray); + SetAttributeDefault(data_, display_color_attr, + pxr::VtArray{ + {geom->rgba[0], geom->rgba[1], geom->rgba[2]}}); + // Set the displayOpacity attribute, only if the opacity is not 1. + if (geom->rgba[3] != 1.0f) { + pxr::SdfPath display_opacity_attr = CreateAttributeSpec( + data_, geom_path, pxr::UsdGeomTokens->primvarsDisplayOpacity, + pxr::SdfValueTypeNames->FloatArray); + SetAttributeDefault(data_, display_opacity_attr, + pxr::VtArray{geom->rgba[3]}); + } + } + if (body_id == kWorldIndex) { SetPrimKind(data_, geom_path, pxr::KindTokens->component); } diff --git a/test/experimental/usd/plugins/mjcf/fixture.h b/test/experimental/usd/plugins/mjcf/fixture.h index 9ab5ffe1..d7b8cda1 100644 --- a/test/experimental/usd/plugins/mjcf/fixture.h +++ b/test/experimental/usd/plugins/mjcf/fixture.h @@ -60,6 +60,13 @@ .Get(&prim_purpose); \ EXPECT_EQ(prim_purpose, purpose); \ } + +#define EXPECT_ATTRIBUTE_HAS_VALUE(stage, path) \ + EXPECT_TRUE((stage)->GetAttributeAtPath(SdfPath(path)).HasValue()); + +#define EXPECT_ATTRIBUTE_HAS_NO_VALUE(stage, path) \ + EXPECT_FALSE((stage)->GetAttributeAtPath(SdfPath(path)).HasValue()); + namespace mujoco { pxr::SdfLayerRefPtr LoadLayer( diff --git a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc index f5caeb19..f144da6e 100644 --- a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc +++ b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc @@ -140,6 +140,53 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestMaterials) { pxr::SdfAssetPath("textures/cube.png")); } +TEST_F(MjcfSdfFileFormatPluginTest, TestGeomRgba) { + static constexpr char kXml[] = R"( + + + + + + + + + )"; + + pxr::SdfLayerRefPtr layer = LoadLayer(kXml); + auto stage = pxr::UsdStage::Open(layer); + + EXPECT_PRIM_VALID(stage, "/test/sphere_red"); + ExpectAttributeEqual(stage, "/test/sphere_red.primvars:displayColor", + pxr::VtArray{{1, 0, 0}}); + EXPECT_ATTRIBUTE_HAS_NO_VALUE(stage, + "/test/sphere_red.primvars:displayOpacity"); + + // There's no mechanism in Mujoco to specify whether an attribute was set + // explicitly or not. We do the same as Mujoco does, which is to compare with + // the default value. + // Which explains why not setting rgba is the same as setting it to the + // default value of (0.5, 0.5, 0.5, 1). + EXPECT_PRIM_VALID(stage, "/test/sphere_default"); + EXPECT_ATTRIBUTE_HAS_NO_VALUE(stage, + "/test/sphere_default.primvars:displayColor"); + EXPECT_ATTRIBUTE_HAS_NO_VALUE(stage, + "/test/sphere_default.primvars:displayOpacity"); + + EXPECT_PRIM_VALID(stage, "/test/sphere_also_default"); + EXPECT_ATTRIBUTE_HAS_NO_VALUE( + stage, "/test/sphere_also_default.primvars:displayColor"); + EXPECT_ATTRIBUTE_HAS_NO_VALUE( + stage, "/test/sphere_also_default.primvars:displayOpacity"); + + EXPECT_PRIM_VALID(stage, "/test/sphere_almost_default"); + ExpectAttributeEqual(stage, + "/test/sphere_almost_default.primvars:displayColor", + pxr::VtArray{{0.5, 0.5, 0.5}}); + ExpectAttributeEqual(stage, + "/test/sphere_almost_default.primvars:displayOpacity", + pxr::VtArray{0.9}); +} + TEST_F(MjcfSdfFileFormatPluginTest, TestFaceVaryingMeshSourcesSimpleMjcfMesh) { static constexpr char kXml[] = R"( From fb30e0e00c4e5a45e66e32e73f73f038a5000ee9 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Mon, 28 Apr 2025 02:57:48 -0700 Subject: [PATCH 077/191] Add docs for mjSpec serialization. PiperOrigin-RevId: 752230683 Change-Id: Id237ccc1b265e99bf15892f8fd208ba38f4c6ded --- doc/python.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/python.rst b/doc/python.rst index 85a54b73..61ebcdc8 100644 --- a/doc/python.rst +++ b/doc/python.rst @@ -628,6 +628,12 @@ Parent: The parent body of a given element -- including bodies and frames -- can be accessed via the ``parent`` property. For example, the parent of a site can be accessed via ``site.parent``. +Serialization +^^^^^^^^^^^^^ +The ``MjSpec`` object can be serialized with all of its assets using the function ``spec.to_zip(file)``, where ``file`` +can be either a path to a file or a file object. In order to load the spec from a zip file, use ``spec = +MjSpec.from_zip(file)``, where ``file`` is a path to a zip file or a zip file object. + .. _PyMJCF: Relationship to ``PyMJCF`` and ``bind`` From 1dc5bcb04a51dd1140a2b742353b3446698997f2 Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Mon, 28 Apr 2025 11:45:51 -0700 Subject: [PATCH 078/191] Update changelog for 3.3.2 release PiperOrigin-RevId: 752389725 Change-Id: I0976279bece09a099b6a34029575b24ba2d8fd1b --- doc/changelog.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index a71644aa..77726e1f 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -2,13 +2,13 @@ Changelog ========= -Upcoming version (not yet release) +Version 3.3.2 (April 28, 2025) ---------------------------------- MJX ^^^ -- Added inverse dynamics. -- Added tendon actuator force sensor. +1. Added inverse dynamics. +2. Added tendon actuator force sensor. Version 3.3.1 (Apr 9, 2025) ---------------------------- From 4137a4c774070b60f7244b7acfc0e602291fde7e Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Mon, 28 Apr 2025 16:08:25 -0700 Subject: [PATCH 079/191] Bump version to 3.3.3 following the 3.3.2 release PiperOrigin-RevId: 752482043 Change-Id: I3dae8a0b9de2be4d456e856825c18f16f62bf51a --- CMakeLists.txt | 2 +- dist/mujoco.rc | 8 ++++---- dist/simulate.rc | 8 ++++---- doc/APIreference/APIglobals.rst | 2 +- doc/unity.rst | 4 ++-- include/mujoco/mujoco.h | 2 +- mjx/pyproject.toml | 8 ++++---- python/mujoco/CMakeLists.txt | 4 ++-- python/mujoco/mjpython/Info.plist | 8 ++++---- python/pyproject.toml | 6 +++--- sample/CMakeLists.txt | 2 +- simulate/CMakeLists.txt | 2 +- src/engine/engine_support.c | 4 ++-- unity/Editor/Bindings/MujocoBinaryRetriever.cs | 4 ++-- unity/Runtime/Bindings/MjBindings.cs | 2 +- unity/package.json | 2 +- 16 files changed, 34 insertions(+), 34 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bbea8706..087a81e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,7 +28,7 @@ set(MSVC_INCREMENTAL_DEFAULT ON) project( mujoco - VERSION 3.3.2 + VERSION 3.3.3 DESCRIPTION "MuJoCo Physics Simulator" HOMEPAGE_URL "https://mujoco.org" ) diff --git a/dist/mujoco.rc b/dist/mujoco.rc index 08a3699d..6191980f 100644 --- a/dist/mujoco.rc +++ b/dist/mujoco.rc @@ -1,6 +1,6 @@ 1 VERSIONINFO -FILEVERSION 3,3,2,0 -PRODUCTVERSION 3,3,2,0 +FILEVERSION 3,3,3,0 +PRODUCTVERSION 3,3,3,0 FILEOS 0x4 FILETYPE 0x1 { @@ -9,9 +9,9 @@ FILETYPE 0x1 BLOCK "040904b0" { VALUE "ProductName", "MuJoCo" - VALUE "ProductVersion", "3.3.2" + VALUE "ProductVersion", "3.3.3" VALUE "FileDescription", "MuJoCo" - VALUE "FileVersion", "3.3.2" + VALUE "FileVersion", "3.3.3" VALUE "InternalName", "mujoco.dll" VALUE "OriginalFilename", "mujoco.dll" VALUE "CompanyName", "Google DeepMind" diff --git a/dist/simulate.rc b/dist/simulate.rc index 46e3c6de..fca19ae2 100644 --- a/dist/simulate.rc +++ b/dist/simulate.rc @@ -1,8 +1,8 @@ MUJOCO ICON "mujoco.ico" 1 VERSIONINFO -FILEVERSION 3,3,2,0 -PRODUCTVERSION 3,3,2,0 +FILEVERSION 3,3,3,0 +PRODUCTVERSION 3,3,3,0 FILEOS 0x4 FILETYPE 0x1 { @@ -11,9 +11,9 @@ FILETYPE 0x1 BLOCK "040904b0" { VALUE "ProductName", "MuJoCo" - VALUE "ProductVersion", "3.3.2" + VALUE "ProductVersion", "3.3.3" VALUE "FileDescription", "MuJoCo" - VALUE "FileVersion", "3.3.2" + VALUE "FileVersion", "3.3.3" VALUE "InternalName", "simulate.exe" VALUE "OriginalFilename", "simulate.exe" VALUE "CompanyName", "Google DeepMind" diff --git a/doc/APIreference/APIglobals.rst b/doc/APIreference/APIglobals.rst index 64aa21f3..09c01f6c 100644 --- a/doc/APIreference/APIglobals.rst +++ b/doc/APIreference/APIglobals.rst @@ -517,7 +517,7 @@ shown in the table below. Their names are in the format ``mjKEY_XXX``. They corr - Maximum number of UI rectangles. Defined in `mjui.h `_. * - ``mjVERSION_HEADER`` - - 332 + - 333 - The version of the MuJoCo headers; changes with every release. This is an integer equal to 100x the software version, so 210 corresponds to version 2.1. Defined in mujoco.h. The API function :ref:`mj_version` returns a number with the same meaning but for the compiled library. diff --git a/doc/unity.rst b/doc/unity.rst index 5f73281f..876a96a6 100644 --- a/doc/unity.rst +++ b/doc/unity.rst @@ -37,14 +37,14 @@ _____ The MuJoCo app needs to be run at least once before the native library can be used, in order to register the library as a trusted binary. Then, copy the dynamic library file from -``/Applications/MuJoCo.app/Contents/Frameworks/mujoco.framework/Versions/Current/libmujoco.3.3.2.dylib`` (it can be +``/Applications/MuJoCo.app/Contents/Frameworks/mujoco.framework/Versions/Current/libmujoco.3.3.3.dylib`` (it can be found by browsing the contents of ``MuJoCo.app``) and rename it as ``mujoco.dylib``. Linux _____ Expand the ``tar.gz`` archive to ``~/.mujoco``. Then copy the dynamic library from -``~/.mujoco/mujoco-3.3.2/lib/libmujoco.so.3.3.2`` and rename it as ``libmujoco.so``. +``~/.mujoco/mujoco-3.3.3/lib/libmujoco.so.3.3.3`` and rename it as ``libmujoco.so``. Windows _______ diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index b23f5d94..94f89a4a 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -16,7 +16,7 @@ #define MUJOCO_MUJOCO_H_ // header version; should match the library version as returned by mj_version() -#define mjVERSION_HEADER 332 +#define mjVERSION_HEADER 333 // needed to define size_t, fabs and log10 #include diff --git a/mjx/pyproject.toml b/mjx/pyproject.toml index 529fc068..ee03aa0a 100644 --- a/mjx/pyproject.toml +++ b/mjx/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name="mujoco-mjx" -version = "3.3.2" +version = "3.3.3" authors = [ {name = "Google DeepMind", email = "mujoco@deepmind.com"}, ] @@ -30,7 +30,7 @@ dependencies = [ "etils[epath]", "jax", "jaxlib", - "mujoco>=3.3.2.dev0", + "mujoco>=3.3.3.dev0", "scipy", "trimesh", ] @@ -41,9 +41,9 @@ mjx-viewer = "mujoco.mjx.viewer:main" [project.urls] Homepage = "https://github.com/google-deepmind/mujoco/tree/main/mjx" -Documentation = "https://mujoco.readthedocs.io/en/3.3.2" +Documentation = "https://mujoco.readthedocs.io/en/3.3.3" Repository = "https://github.com/google-deepmind/mujoco/tree/main/mjx" -Changelog = "https://mujoco.readthedocs.io/en/3.3.2/changelog.html" +Changelog = "https://mujoco.readthedocs.io/en/3.3.3/changelog.html" [tool.isort] force_single_line = true diff --git a/python/mujoco/CMakeLists.txt b/python/mujoco/CMakeLists.txt index faceca26..b1aec9ed 100644 --- a/python/mujoco/CMakeLists.txt +++ b/python/mujoco/CMakeLists.txt @@ -84,7 +84,7 @@ if(NOT TARGET mujoco) if(MUJOCO_FRAMEWORK) message("MuJoCo framework is at ${MUJOCO_FRAMEWORK}/mujoco.framework") set(MUJOCO_LIBRARY - ${MUJOCO_FRAMEWORK}/mujoco.framework/Versions/A/libmujoco.3.3.2.dylib + ${MUJOCO_FRAMEWORK}/mujoco.framework/Versions/A/libmujoco.3.3.3.dylib ) target_compile_options(mujoco INTERFACE -F${MUJOCO_FRAMEWORK}) endif() @@ -92,7 +92,7 @@ if(NOT TARGET mujoco) if(NOT MUJOCO_FRAMEWORK) find_library( - MUJOCO_LIBRARY mujoco mujoco.3.3.2 HINTS ${MUJOCO_LIBRARY_DIR} REQUIRED + MUJOCO_LIBRARY mujoco mujoco.3.3.3 HINTS ${MUJOCO_LIBRARY_DIR} REQUIRED ) find_path(MUJOCO_INCLUDE mujoco/mujoco.h HINTS ${MUJOCO_INCLUDE_DIR} REQUIRED) message("MuJoCo is at ${MUJOCO_LIBRARY}") diff --git a/python/mujoco/mjpython/Info.plist b/python/mujoco/mjpython/Info.plist index d7e0aa2f..475027c5 100644 --- a/python/mujoco/mjpython/Info.plist +++ b/python/mujoco/mjpython/Info.plist @@ -7,13 +7,13 @@ CFBundleIdentifier org.mujoco.mjpython CFBundleVersion - 3.3.2 + 3.3.3 CFBundleGetInfoString - 3.3.2 + 3.3.3 CFBundleLongVersionString - 3.3.2 + 3.3.3 CFBundleShortVersionString - 3.3.2 + 3.3.3 CFBundleExecutable mjpython CFBundleIconFile diff --git a/python/pyproject.toml b/python/pyproject.toml index 23b973dd..c178ad79 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mujoco" -version = "3.3.2" +version = "3.3.3" authors = [ {name = "Google DeepMind", email = "mujoco@deepmind.com"}, ] @@ -35,9 +35,9 @@ dynamic = ["readme", "scripts"] [project.urls] Homepage = "https://github.com/google-deepmind/mujoco" -Documentation = "https://mujoco.readthedocs.io/en/3.3.2" +Documentation = "https://mujoco.readthedocs.io/en/3.3.3" Repository = "https://github.com/google-deepmind/mujoco" -Changelog = "https://mujoco.readthedocs.io/en/3.3.2/changelog.html" +Changelog = "https://mujoco.readthedocs.io/en/3.3.3/changelog.html" [tool.setuptools] include-package-data = false diff --git a/sample/CMakeLists.txt b/sample/CMakeLists.txt index b1789412..f9457f2c 100644 --- a/sample/CMakeLists.txt +++ b/sample/CMakeLists.txt @@ -24,7 +24,7 @@ set(MSVC_INCREMENTAL_DEFAULT ON) project( mujoco_samples - VERSION 3.3.2 + VERSION 3.3.3 DESCRIPTION "MuJoCo samples binaries" HOMEPAGE_URL "https://mujoco.org" ) diff --git a/simulate/CMakeLists.txt b/simulate/CMakeLists.txt index ff64d734..c22ea4a7 100644 --- a/simulate/CMakeLists.txt +++ b/simulate/CMakeLists.txt @@ -29,7 +29,7 @@ set(MUJOCO_DEP_VERSION_lodepng project( mujoco_simulate - VERSION 3.3.2 + VERSION 3.3.3 DESCRIPTION "MuJoCo simulate binaries" HOMEPAGE_URL "https://mujoco.org" ) diff --git a/src/engine/engine_support.c b/src/engine/engine_support.c index b7e2699d..2b3960e8 100644 --- a/src/engine/engine_support.c +++ b/src/engine/engine_support.c @@ -41,8 +41,8 @@ //-------------------------- Constants ------------------------------------------------------------- - #define mjVERSION 332 -#define mjVERSIONSTRING "3.3.2" + #define mjVERSION 333 +#define mjVERSIONSTRING "3.3.3" // names of disable flags const char* mjDISABLESTRING[mjNDISABLE] = { diff --git a/unity/Editor/Bindings/MujocoBinaryRetriever.cs b/unity/Editor/Bindings/MujocoBinaryRetriever.cs index ba697540..3ff96ab5 100644 --- a/unity/Editor/Bindings/MujocoBinaryRetriever.cs +++ b/unity/Editor/Bindings/MujocoBinaryRetriever.cs @@ -37,7 +37,7 @@ public class MujocoBinaryRetriever { if (AssetDatabase.LoadMainAssetAtPath(mujocoPath + "/mujoco.dylib") == null) { File.Copy( "/Applications/MuJoCo.app/Contents/Frameworks" + - "/mujoco.framework/Versions/Current/libmujoco.3.3.2.dylib", + "/mujoco.framework/Versions/Current/libmujoco.3.3.3.dylib", mujocoPath + "/mujoco.dylib"); AssetDatabase.Refresh(); } @@ -45,7 +45,7 @@ public class MujocoBinaryRetriever { if (AssetDatabase.LoadMainAssetAtPath(mujocoPath + "/libmujoco.so") == null) { File.Copy( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + - "/.mujoco/mujoco-3.3.2/lib/libmujoco.so.3.3.2", + "/.mujoco/mujoco-3.3.3/lib/libmujoco.so.3.3.3", mujocoPath + "/libmujoco.so"); AssetDatabase.Refresh(); } diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index 2af0cb51..a3637d62 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -110,7 +110,7 @@ public const int mjMAXLINEPNT = 1000; public const int mjMAXPLANEGRID = 200; public const bool THIRD_PARTY_MUJOCO_MJXMACRO_H_ = true; public const bool THIRD_PARTY_MUJOCO_MUJOCO_H_ = true; -public const int mjVERSION_HEADER = 332; +public const int mjVERSION_HEADER = 333; // ------------------------------------Enums------------------------------------ diff --git a/unity/package.json b/unity/package.json index 95b769ec..c91c5d6c 100644 --- a/unity/package.json +++ b/unity/package.json @@ -1,7 +1,7 @@ { "name": "org.mujoco", "displayName": "MuJoCo", - "version": "3.3.2", + "version": "3.3.3", "description": "MuJoCo importer and runtime plug-in", "dependencies": {}, "author": { From 75f196c7efd8a2637437b35492e9ee5484ba5cda Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Mon, 28 Apr 2025 16:22:55 -0700 Subject: [PATCH 080/191] Add missing PointToLocal call in mjCPlugin copy constructor. PiperOrigin-RevId: 752486555 Change-Id: I08b14476b9098e6473a4c00918ef3f9a287f35f6 --- python/mujoco/specs_test.py | 11 +++++++++++ src/user/user_objects.cc | 11 +++++++++++ src/user/user_objects.h | 3 +++ 3 files changed, 25 insertions(+) diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py index 210d833f..e8aafa23 100644 --- a/python/mujoco/specs_test.py +++ b/python/mujoco/specs_test.py @@ -861,6 +861,17 @@ class SpecsTest(absltest.TestCase): self.assertEqual(model.nplugin, 1) self.assertEqual(model.npluginattr, 7) self.assertEqual(model.body_plugin[1], 0) + attributes = (''.join([chr(i) for i in model.plugin_attr]).split(chr(0))) + self.assertEqual(attributes[:2], ['10', '1']) + + copy = spec.copy() # before assigning the new config + wrong_config = {'wrong': '10', 'bend': '1'} + for s in [spec, copy]: + s.plugins[0].config = wrong_config + with self.assertRaisesRegex( + ValueError, "Error: unrecognized attribute 'plugin:wrong'" + ): + s.compile() def test_recompile_error(self): main_xml = """ diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index d609621c..cff97b72 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -7271,6 +7271,8 @@ mjCPlugin::mjCPlugin(mjCModel* _model) { spec.plugin_name = &plugin_name; spec.name = &name; spec.info = &info; + + PointToLocal(); } @@ -7289,11 +7291,20 @@ mjCPlugin& mjCPlugin::operator=(const mjCPlugin& other) { parent = this; plugin_slot = other.plugin_slot; } + PointToLocal(); return *this; } +void mjCPlugin::PointToLocal() { + spec.element = static_cast(this); + spec.name = &name; + spec.info = &info; +} + + + // compiler void mjCPlugin::Compile(void) { mjCPlugin* plugin_instance = this; diff --git a/src/user/user_objects.h b/src/user/user_objects.h index 16aa57af..bfec0797 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -1587,6 +1587,9 @@ class mjCPlugin : public mjCPlugin_ { mjCPlugin(mjCModel*); mjCPlugin(const mjCPlugin& other); mjCPlugin& operator=(const mjCPlugin& other); + + void PointToLocal(); + mjsPlugin spec; mjCBase* parent; // parent object (only used when generating error message) int plugin_slot; // global registered slot number of the plugin From 2545ec5383dceb9e940b2bc9477709bf25565dfc Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 29 Apr 2025 04:36:32 -0700 Subject: [PATCH 081/191] Don't call `getchar` on error. PiperOrigin-RevId: 752680097 Change-Id: Idce87a3e33ebce703bf3eedc18026f6a13d05f35 --- src/engine/engine_util_errmem.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/engine/engine_util_errmem.c b/src/engine/engine_util_errmem.c index 7c63c1ce..58be05a1 100644 --- a/src/engine/engine_util_errmem.c +++ b/src/engine/engine_util_errmem.c @@ -123,10 +123,9 @@ void mju_error_raw(const char* msg) { } else { // write to log and console mju_writeLog("ERROR", msg); - printf("ERROR: %s\n\nPress Enter to exit ...", msg); + printf("ERROR: %s\n\n", msg); - // pause, exit - getchar(); + // exit exit(EXIT_FAILURE); } } From 3e9d13009274ed8da5019c54ec7177d43c40bf9f Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Tue, 29 Apr 2025 05:18:17 -0700 Subject: [PATCH 082/191] Add mjs_getPluginAttributes. PiperOrigin-RevId: 752691116 Change-Id: Ib503a99f8d10661b8bf92410ccf471420f924986 --- doc/APIreference/functions.rst | 9 +++++++++ doc/includes/references.h | 1 + include/mujoco/mujoco.h | 3 +++ python/mujoco/introspect/functions.py | 16 ++++++++++++++++ python/mujoco/specs.cc | 14 +++++++++++--- python/mujoco/specs_test.py | 1 + src/user/user_api.cc | 8 ++++++++ src/user/user_api.h | 3 +++ 8 files changed, 52 insertions(+), 3 deletions(-) diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index 029fac76..9606e94d 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -4401,6 +4401,15 @@ Get string contents. Get double array contents and optionally its size. +.. _mjs_getPluginAttributes: + +`mjs_getPluginAttributes <#mjs_getPluginAttributes>`__ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. mujoco-include:: mjs_getPluginAttributes + +Get plugin attributes. + .. _SpecUtilities: Spec utilities diff --git a/doc/includes/references.h b/doc/includes/references.h index 89890efc..ce9bd670 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -3686,6 +3686,7 @@ void mjs_setDouble(mjDoubleVec* dest, const double* array, int size); void mjs_setPluginAttributes(mjsPlugin* plugin, void* attributes); const char* mjs_getString(const mjString* source); const double* mjs_getDouble(const mjDoubleVec* source, int* size); +const void* mjs_getPluginAttributes(const mjsPlugin* plugin); void mjs_setDefault(mjsElement* element, const mjsDefault* def); int mjs_setFrame(mjsElement* dest, mjsFrame* frame); const char* mjs_resolveOrientation(double quat[4], mjtByte degree, const char* sequence, diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index 94f89a4a..63bfafcc 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -1622,6 +1622,9 @@ MJAPI const char* mjs_getString(const mjString* source); // Get double array contents and optionally its size. MJAPI const double* mjs_getDouble(const mjDoubleVec* source, int* size); +// Get plugin attributes. +MJAPI const void* mjs_getPluginAttributes(const mjsPlugin* plugin); + //---------------------------------- Spec utilities ------------------------------------------------ diff --git a/python/mujoco/introspect/functions.py b/python/mujoco/introspect/functions.py index 9e61b828..b74e222f 100644 --- a/python/mujoco/introspect/functions.py +++ b/python/mujoco/introspect/functions.py @@ -10298,6 +10298,22 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), doc='Get double array contents and optionally its size.', )), + ('mjs_getPluginAttributes', + FunctionDecl( + name='mjs_getPluginAttributes', + return_type=PointerType( + inner_type=ValueType(name='void', is_const=True), + ), + parameters=( + FunctionParameterDecl( + name='plugin', + type=PointerType( + inner_type=ValueType(name='mjsPlugin', is_const=True), + ), + ), + ), + doc='Get plugin attributes.', + )), ('mjs_setDefault', FunctionDecl( name='mjs_setDefault', diff --git a/python/mujoco/specs.cc b/python/mujoco/specs.cc index 3adc549b..c1bbc3eb 100644 --- a/python/mujoco/specs.cc +++ b/python/mujoco/specs.cc @@ -1034,8 +1034,15 @@ PYBIND11_MODULE(_specs, m) { [](raw::MjsPlugin& self) { mjs_delete(self.element); }); mjsPlugin.def_property( "config", - [](raw::MjsPlugin& self) -> void { - throw pybind11::value_error("Reading plugin config is not supported."); + [](raw::MjsPlugin& self) -> py::dict { + const std::map>* config_attribs = + static_cast>*>( + mjs_getPluginAttributes(&self)); + py::dict config; + for (const auto& [key, value] : *config_attribs) { + config[py::str(key)] = value; + } + return config; }, [](raw::MjsPlugin& self, py::dict& config) { std::map> config_attribs; @@ -1047,7 +1054,8 @@ PYBIND11_MODULE(_specs, m) { config_attribs[key_str] = value.cast(); } mjs_setPluginAttributes(&self, &config_attribs); - }); + }, + py::return_value_policy::reference_internal); // ============================= MJVISUAL ==================================== mjVisual.def_property( "global_", diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py index e8aafa23..ac702876 100644 --- a/python/mujoco/specs_test.py +++ b/python/mujoco/specs_test.py @@ -844,6 +844,7 @@ class SpecsTest(absltest.TestCase): info='info' ) plugin.config = {'twist': '10', 'bend': '1'} + self.assertEqual(plugin.config, {'twist': '10', 'bend': '1'}) body = spec.worldbody.add_body() body.plugin = plugin diff --git a/src/user/user_api.cc b/src/user/user_api.cc index e21181dd..e103aa8e 100644 --- a/src/user/user_api.cc +++ b/src/user/user_api.cc @@ -1302,6 +1302,14 @@ void mjs_setPluginAttributes(mjsPlugin* plugin, void* attributes) { +// get plugin attributes +const void* mjs_getPluginAttributes(const mjsPlugin* plugin) { + mjCPlugin* pluginC = static_cast(plugin->element); + return &pluginC->config_attribs; +} + + + // -------------------------- GLOBAL ASSET CACHE ------------------------------- void mj_setCacheSize(mjCache cache, std::size_t size) { diff --git a/src/user/user_api.h b/src/user/user_api.h index d01b5172..e1b73d86 100644 --- a/src/user/user_api.h +++ b/src/user/user_api.h @@ -352,6 +352,9 @@ MJAPI const char* mjs_getString(const mjString* source); // Get double array contents and optionally its size. MJAPI const double* mjs_getDouble(const mjDoubleVec* source, int* size); +// Get plugin attributes. +MJAPI const void* mjs_getPluginAttributes(const mjsPlugin* plugin); + //---------------------------------- Other utilities ----------------------------------------------- From d657e628f0fc224874d83f4bbaf40b3dfe1e6b0e Mon Sep 17 00:00:00 2001 From: Robin Alazard Date: Tue, 29 Apr 2025 08:43:23 -0700 Subject: [PATCH 083/191] Fix radius sizes of geom shapes PiperOrigin-RevId: 752750452 Change-Id: I2ac08ad4014718320f014ed8ba7cc3b87586f6c0 --- .../usd/plugins/mjcf/mujoco_to_usd.cc | 18 ++++++++---------- .../usd/plugins/mjcf/mjcf_file_format_test.cc | 8 ++++---- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc index e443149c..8ddba52d 100644 --- a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc @@ -673,15 +673,15 @@ class ModelWriter { pxr::SdfPath capsule_path = CreatePrimSpec(data_, body_path, name, pxr::UsdGeomTokens->Capsule); - // MuJoCo uses half sizes. pxr::SdfPath radius_attr_path = CreateAttributeSpec(data_, capsule_path, pxr::UsdGeomTokens->radius, pxr::SdfValueTypeNames->Float); - SetAttributeDefault(data_, radius_attr_path, size[0] * 2); + SetAttributeDefault(data_, radius_attr_path, size[0]); pxr::SdfPath height_attr_path = CreateAttributeSpec(data_, capsule_path, pxr::UsdGeomTokens->height, pxr::SdfValueTypeNames->Float); + // MuJoCo uses half sizes. SetAttributeDefault(data_, height_attr_path, size[1] * 2); return capsule_path; } @@ -701,15 +701,15 @@ class ModelWriter { pxr::SdfPath cylinder_path = CreatePrimSpec(data_, body_path, name, pxr::UsdGeomTokens->Cylinder); - // MuJoCo uses half sizes. pxr::SdfPath radius_attr_path = CreateAttributeSpec(data_, cylinder_path, pxr::UsdGeomTokens->radius, pxr::SdfValueTypeNames->Float); - SetAttributeDefault(data_, radius_attr_path, size[0] * 2); + SetAttributeDefault(data_, radius_attr_path, size[0]); pxr::SdfPath height_attr_path = CreateAttributeSpec(data_, cylinder_path, pxr::UsdGeomTokens->height, pxr::SdfValueTypeNames->Float); + // MuJoCo uses half sizes. SetAttributeDefault(data_, height_attr_path, size[1] * 2); return cylinder_path; } @@ -729,11 +729,10 @@ class ModelWriter { pxr::SdfPath ellipsoid_path = CreatePrimSpec(data_, body_path, name, pxr::UsdGeomTokens->Sphere); - pxr::GfVec3f scale = {static_cast(size[0] * 2), - static_cast(size[1] * 2), - static_cast(size[2] * 2)}; + pxr::GfVec3f scale = {static_cast(size[0]), + static_cast(size[1]), + static_cast(size[2])}; - // MuJoCo uses half sizes. pxr::SdfPath radius_attr_path = CreateAttributeSpec(data_, ellipsoid_path, pxr::UsdGeomTokens->radius, pxr::SdfValueTypeNames->Float); @@ -760,11 +759,10 @@ class ModelWriter { pxr::SdfPath sphere_path = CreatePrimSpec(data_, body_path, name, pxr::UsdGeomTokens->Sphere); - // MuJoCo uses half sizes. pxr::SdfPath radius_attr_path = CreateAttributeSpec(data_, sphere_path, pxr::UsdGeomTokens->radius, pxr::SdfValueTypeNames->Float); - SetAttributeDefault(data_, radius_attr_path, size[0] * 2); + SetAttributeDefault(data_, radius_attr_path, size[0]); return sphere_path; } diff --git a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc index f144da6e..78f10f46 100644 --- a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc +++ b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc @@ -472,16 +472,16 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestGeomsPrims) { // Sphere EXPECT_PRIM_VALID(stage, "/test/sphere_geom"); EXPECT_PRIM_IS_A(stage, "/test/sphere_geom", pxr::UsdGeomSphere); - ExpectAttributeEqual(stage, "/test/sphere_geom.radius", 2 * 10.0); + ExpectAttributeEqual(stage, "/test/sphere_geom.radius", 10.0); // Capsule EXPECT_PRIM_VALID(stage, "/test/capsule_geom"); EXPECT_PRIM_IS_A(stage, "/test/capsule_geom", pxr::UsdGeomCapsule); - ExpectAttributeEqual(stage, "/test/capsule_geom.radius", 2 * 10.0); + ExpectAttributeEqual(stage, "/test/capsule_geom.radius", 10.0); ExpectAttributeEqual(stage, "/test/capsule_geom.height", 2 * 20.0); // Cylinder EXPECT_PRIM_VALID(stage, "/test/cylinder_geom"); EXPECT_PRIM_IS_A(stage, "/test/cylinder_geom", pxr::UsdGeomCylinder); - ExpectAttributeEqual(stage, "/test/cylinder_geom.radius", 2 * 10.0); + ExpectAttributeEqual(stage, "/test/cylinder_geom.radius", 10.0); ExpectAttributeEqual(stage, "/test/cylinder_geom.height", 2 * 20.0); // Ellipsoid EXPECT_PRIM_VALID(stage, "/test/ellipsoid_geom"); @@ -490,7 +490,7 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestGeomsPrims) { EXPECT_PRIM_IS_A(stage, "/test/ellipsoid_geom", pxr::UsdGeomSphere); ExpectAttributeEqual(stage, "/test/ellipsoid_geom.radius", 1.0); ExpectAttributeEqual(stage, "/test/ellipsoid_geom.xformOp:scale", - pxr::GfVec3f(2.0 * 10.0, 2.0 * 20.0, 2.0 * 30.0)); + pxr::GfVec3f(10.0, 20.0, 30.0)); } static constexpr char kSiteXml[] = R"( From 8078a45727a3898caaa485e57fd7b22122362bd1 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 29 Apr 2025 11:29:41 -0700 Subject: [PATCH 084/191] Add private functions `mju_blockDiag` and `mju_blockDiagSparse` PiperOrigin-RevId: 752815446 Change-Id: Ia7f73160315ce57b78a2c34bc781f00e1b0f05c4 --- src/engine/engine_util_sparse.c | 100 +++++++++++ src/engine/engine_util_sparse.h | 15 ++ test/engine/engine_util_sparse_test.cc | 222 +++++++++++++++++++++++++ 3 files changed, 337 insertions(+) diff --git a/src/engine/engine_util_sparse.c b/src/engine/engine_util_sparse.c index d72a9fca..23c038ec 100644 --- a/src/engine/engine_util_sparse.c +++ b/src/engine/engine_util_sparse.c @@ -795,3 +795,103 @@ void mju_sqrMatTDSparse(mjtNum* res, const mjtNum* mat, const mjtNum* matT, mj_freeStack(d); } + + + +// block-diagonalize a dense matrix +// res output matrix +// mat input matrix +// nc_mat number of columns in mat +// nc_res number of columns in res +// nb number of blocks +// perm_r reverse permutation of rows (res -> mat) +// perm_c reverse permutation of columns (res -> mat) +// block_nr number of rows in each block +// block_nc number of columns in each block +// block_r first row of each block +// block_c first column of each block +void mju_blockDiag(mjtNum* restrict res, const mjtNum* restrict mat, + int nc_mat, int nc_res, int nb, + const int* restrict perm_r, const int* restrict perm_c, + const int* restrict block_nr, const int* restrict block_nc, + const int* restrict block_r, const int* restrict block_c) { + for (int b=0; b < nb; b++) { + int bnr = block_nr[b]; + int bnc = block_nc[b]; + const int* adr_r = perm_r + block_r[b]; + const int* adr_c = perm_c + block_c[b]; + int adr = nc_res * block_r[b]; + for (int r = 0; r < bnr; r++) { + for (int c = 0; c < bnc; c++) { + res[adr++] = mat[nc_mat * adr_r[r] + adr_c[c]]; + } + } + } +} + + +// block-diagonalize a sparse matrix +// res values of the target matrix res +// res_rownnz number of non-zeros in each row of res +// res_rowadr row address of each non-zero in res +// res_colind column index of each non-zero in res +// mat values of the source matrix mat +// mat_rownnz number of non-zeros in each row of mat +// mat_rowadr row address of each non-zero in mat +// mat_colind column index of each non-zero in mat +// nr number of rows in mat/res +// nb number of blocks +// perm_r reverse permutation of rows (res -> mat) +// perm_c forward permutation of columns (mat -> res) +// block_r first row of each block in res +// block_c first column of each block in res +// mat2 optional additional source matrix (same structure as mat) +// res2 optional additional target matrix (same structure as res) +void mju_blockDiagSparse(mjtNum* restrict res, int* restrict res_rownnz, + int* restrict res_rowadr, int* restrict res_colind, + const mjtNum* restrict mat, const int* restrict rownnz, + const int* restrict rowadr, const int* restrict colind, + int nr, int nb, + const int* restrict perm_r, const int* restrict perm_c, + const int* restrict block_r, const int* restrict block_c, + mjtNum* restrict res2, const mjtNum* restrict mat2) { + int block = 0; + int col_offset = block_c[block]; + int row_next = block + 1 < nb ? block_r[block + 1] : nr; + for (int r=0; r < nr; r++) { + // row k in mat goes to row r in res + int k = perm_r[r]; + + // rownnz + int nnz = rownnz[k]; + res_rownnz[r] = nnz; + + // rowadr + int res_adr = (r == 0) ? 0 : res_rowadr[r-1] + res_rownnz[r-1]; + res_rowadr[r] = res_adr; + + // colind + int* res_colind_r = res_colind + res_adr; + mjtNum* res_r = res + res_adr; + int mat_adr = rowadr[k]; + const int* colind_k = colind + mat_adr; + const mjtNum* mat_k = mat + mat_adr; + for (int j=0; j < nnz; j++) { + res_colind_r[j] = perm_c[colind_k[j]] - col_offset; + } + + // values (dense copy: partial order within block is guaranteed) + mju_copy(res_r, mat_k, nnz); + if (mat2 && res2) { + mju_copy(res2 + res_adr, mat2 + mat_adr, nnz); + } + + // end of block reached: update block counter, column offset, next row + if (r + 1 >= row_next && block + 1 < nb ) { + block++; + col_offset = block_c[block]; + row_next = block + 1 < nb ? block_r[block + 1] : nr; + } + } +} + diff --git a/src/engine/engine_util_sparse.h b/src/engine/engine_util_sparse.h index 08a663b2..1f75739b 100644 --- a/src/engine/engine_util_sparse.h +++ b/src/engine/engine_util_sparse.h @@ -103,6 +103,21 @@ MJAPI void mju_sqrMatTDSparseCount(int* res_rownnz, int* res_rowadr, int nr, // precompute res_rowadr for mju_sqrMatTDSparse using uncompressed memory MJAPI void mju_sqrMatTDUncompressedInit(int* res_rowadr, int nc); +// block-diagonalize a dense matrix +MJAPI void mju_blockDiag(mjtNum* res, const mjtNum* mat, + int nc_mat, int nc_res, int nb, + const int* perm_r, const int* perm_c, + const int* block_nr, const int* block_nc, + const int* blockadr_r, const int* blockadr_c); + +// block-diagonalize a sparse matrix +MJAPI void mju_blockDiagSparse( + mjtNum* res, int* res_rownnz, int* res_rowadr, int* res_colind, + const mjtNum* mat, const int* rownnz, const int* rowadr, const int* colind, + int nr, int nb, + const int* perm_r, const int* perm_c, + const int* block_r, const int* block_c, + mjtNum* res2, const mjtNum* mat2); // ------------------------------ inlined functions ------------------------------------------------ diff --git a/test/engine/engine_util_sparse_test.cc b/test/engine/engine_util_sparse_test.cc index 94da0d5d..41231c2c 100644 --- a/test/engine/engine_util_sparse_test.cc +++ b/test/engine/engine_util_sparse_test.cc @@ -1088,5 +1088,227 @@ TEST_F(EngineUtilSparseTest, MergeSorted) { EXPECT_THAT(merged_b, ElementsAre(1, 2, 3, 4, 5, 6, 7, 8)); } +TEST_F(EngineUtilSparseTest, BlockDiag) { + // 4x5 matrix with 3 blocks + constexpr int nr = 4; + constexpr int nc = 5; + const mjtNum mat[nr*nc] = { + 1, 2, 0, 0, 0, + 0, 0, 3, 4, 0, + 0, 0, 5, 6, 0, + 0, 0, 0, 0, 7 + }; + + // block structure + constexpr int nb = 3; + const int block_nr[nb] = {1, 2, 1}; + const int block_nc[nb] = {2, 2, 1}; + const int block_r[nb] = {0, 1, 3}; + const int block_c[nb] = {0, 2, 4}; + + // test with identity permutations + const int perm_r[nr] = {0, 1, 2, 3}; + const int perm_c[nc] = {0, 1, 2, 3, 4}; + mjtNum res[nr*nc] = {0}; + mju_blockDiag(res, mat, nc, nc, nb, + perm_r, perm_c, + block_nr, block_nc, + block_r, block_c); + EXPECT_THAT(res, ElementsAre(1, 2, 0, 0, 0, + 3, 4, 5, 6, 0, + 0, 0, 0, 0, 0, + 7, 0, 0, 0, 0)); +} + +void PermuteMat(mjtNum* res, const mjtNum* mat, int nr, int nc, + const int* perm_r, const int* perm_c, + bool scatter_r, bool scatter_c); + + +TEST_F(EngineUtilSparseTest, BlockDiagPerm) { + // 4x5 matrix with 3 blocks + constexpr int nr = 4; + constexpr int nc = 5; + const mjtNum mat[nr*nc] = { + 1, 2, 0, 0, 0, + 0, 0, 3, 4, 0, + 0, 0, 5, 6, 0, + 0, 0, 0, 0, 7 + }; + + // block structure + constexpr int nb = 3; + const int block_nr[nb] = {1, 2, 1}; + const int block_nc[nb] = {2, 2, 1}; + const int block_r[nb] = {0, 1, 3}; + const int block_c[nb] = {0, 2, 4}; + + // scatter mat into mat_p + const int perm_r[nr] = {1, 3, 2, 0}; + const int perm_c[nc] = {2, 0, 4, 3, 1}; + mjtNum mat_p[nr*nc]; + PermuteMat(mat_p, mat, nr, nc, perm_r, perm_c, true, true); + + // test with permutation + mjtNum res[nr*nc] = {0}; + mju_blockDiag(res, mat_p, nc, nc, nb, + perm_r, perm_c, + block_nr, block_nc, + block_r, block_c); + EXPECT_THAT(res, ElementsAre(1, 2, 0, 0, 0, + 3, 4, 5, 6, 0, + 0, 0, 0, 0, 0, + 7, 0, 0, 0, 0)); +} + +TEST_F(EngineUtilSparseTest, BlockDiagLessCols) { + // 4x5 matrix with 3 blocks + constexpr int nr = 4; + constexpr int nc = 5; + const mjtNum mat[nr*nc] = { + 1, 2, 0, 0, 0, + 0, 0, 3, 4, 0, + 0, 0, 5, 6, 0, + 0, 0, 0, 0, 7 + }; + + // block structure (ignore middle block) + constexpr int nb = 2; + const int block_nr[nb] = {1, 1}; + const int block_nc[nb] = {2, 1}; + const int block_r[nb] = {0, 3}; + const int block_c[nb] = {0, 4}; + + // scatter mat into mat_p + const int perm_r[nr] = {1, 3, 2, 0}; + const int perm_c[nc] = {2, 0, 4, 3, 1}; + mjtNum mat_p[nr*nc]; + PermuteMat(mat_p, mat, nr, nc, perm_r, perm_c, true, true); + + // test with permutation and less columns (ignore middle block) + constexpr int nc_res = 3; + mjtNum res2[nr*nc_res] = {0}; + mju_blockDiag(res2, mat_p, nc, nc_res, nb, + perm_r, perm_c, + block_nr, block_nc, + block_r, block_c); + EXPECT_THAT(res2, ElementsAre(1, 2, 0, + 0, 0, 0, + 0, 0, 0, + 7, 0, 0)); +} + +TEST_F(EngineUtilSparseTest, BlockDiagSparse) { + // 4x5 matrix with 3 blocks + constexpr int nr = 4; + constexpr int nc = 5; + const mjtNum mat[nr*nc] = { + 1, 2, 0, 0, 0, + 0, 0, 3, 4, 0, + 0, 0, 5, 6, 0, + 0, 0, 0, 0, 7 + }; + constexpr int nnz = 7; + + // block structure + constexpr int nb = 3; + const int block_r[nb] = {0, 1, 3}; + const int block_c[nb] = {0, 2, 4}; + + // convert to sparse + int rownnz[nr]; + int rowadr[nr]; + int colind[nnz]; + mjtNum mat_sparse[nnz]; + mju_dense2sparse(mat_sparse, mat, nr, nc, rownnz, rowadr, colind, nnz); + + // test with identity permutations + const int perm_r[nr] = {0, 1, 2, 3}; + const int perm_c[nc] = {0, 1, 2, 3, 4}; + int res_rownnz[nr]; + int res_rowadr[nr]; + int res_colind[nnz]; + mjtNum res[nnz]; + mju_blockDiagSparse(res, res_rownnz, res_rowadr, res_colind, + mat_sparse, rownnz, rowadr, colind, nr, nb, + perm_r, perm_c, + block_r, block_c, nullptr, nullptr); + mjtNum dense_res[nr*nc]; + mju_sparse2dense(dense_res, res, nr, nc, res_rownnz, res_rowadr, res_colind); + EXPECT_THAT(dense_res, ElementsAre(1, 2, 0, 0, 0, + 3, 4, 0, 0, 0, + 5, 6, 0, 0, 0, + 7, 0, 0, 0, 0)); + + // permute mat into mat_p (scatter rows, gather columns) + const int perm_r2[nr] = {3, 1, 0, 2}; + const int perm_c2[nc] = {4, 0, 2, 1, 3}; + mjtNum mat_p[nr*nc]; + PermuteMat(mat_p, mat, nr, nc, perm_r2, perm_c2, true, false); + mju_dense2sparse(mat_sparse, mat_p, nr, nc, rownnz, rowadr, colind, nnz); + + // test with permutation + mju_blockDiagSparse(res, res_rownnz, res_rowadr, res_colind, + mat_sparse, rownnz, rowadr, colind, nr, nb, + perm_r2, perm_c2, + block_r, block_c, nullptr, nullptr); + mju_sparse2dense(dense_res, res, nr, nc, res_rownnz, res_rowadr, res_colind); + EXPECT_THAT(dense_res, ElementsAre(1, 2, 0, 0, 0, + 3, 4, 0, 0, 0, + 5, 6, 0, 0, 0, + 7, 0, 0, 0, 0)); +} + +TEST_F(EngineUtilSparseTest, PermuteMat) { + const mjtNum mat[] = {1, 2, 0, 0, + 0, 0, 3, 4, + 0, 0, 5, 6}; + const int perm_r[] = {2, 0, 1}; + const int perm_c[] = {3, 2, 0, 1}; + mjtNum gather[3*4]; + PermuteMat(gather, mat, 3, 4, perm_r, perm_c, false, false); + EXPECT_THAT(gather, ElementsAre(6, 5, 0, 0, + 0, 0, 1, 2, + 4, 3, 0, 0)); + mjtNum scatter[3*4]; + PermuteMat(scatter, gather, 3, 4, perm_r, perm_c, true, true); + EXPECT_THAT(scatter, ElementsAre(1, 2, 0, 0, + 0, 0, 3, 4, + 0, 0, 5, 6)); + mjtNum mixed[3*4]; + PermuteMat(mixed, mat, 3, 4, perm_r, perm_c, true, false); + EXPECT_THAT(mixed, ElementsAre(4, 3, 0, 0, + 6, 5, 0, 0, + 0, 0, 1, 2)); + mjtNum mixed_back[3*4]; + PermuteMat(mixed_back, mixed, 3, 4, perm_r, perm_c, false, true); + EXPECT_THAT(mixed_back, ElementsAre(1, 2, 0, 0, + 0, 0, 3, 4, + 0, 0, 5, 6)); +} + +// local function for permuting the rows and columns of a dense matrix +void PermuteMat(mjtNum* res, const mjtNum* mat, int nr, int nc, + const int* perm_r, const int* perm_c, + bool scatter_r, bool scatter_c) { + for (int r = 0; r < nr; r++) { + for (int c = 0; c < nc; c++) { + if (scatter_r && scatter_c) { + // scatter both + res[perm_r[r] * nc + perm_c[c]] = mat[r * nc + c]; + } else if (scatter_r && !scatter_c) { + // scatter rows, gather columns + res[perm_r[r] * nc + c] = mat[r * nc + perm_c[c]]; + } else if (!scatter_r && scatter_c) { + // gather rows, scatter columns + res[r * nc + perm_c[c]] = mat[perm_r[r] * nc + c]; + } else { + // gather both + res[r * nc + c] = mat[perm_r[r] * nc + perm_c[c]]; + } + } + } +} + } // namespace } // namespace mujoco From 4c92c609887f1ee911307db9f4619ff87adc0217 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 29 Apr 2025 12:55:43 -0700 Subject: [PATCH 085/191] Remove time from test model keyframe PiperOrigin-RevId: 752847956 Change-Id: Iebc28c4c73d39c3d37c5fa7abd53ea8a9e11f7f9 --- test/testdata/model.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/test/testdata/model.xml b/test/testdata/model.xml index e122f927..5a1f9c81 100644 --- a/test/testdata/model.xml +++ b/test/testdata/model.xml @@ -168,7 +168,6 @@ From 1766a388ccb5d8c13bc963f1f13758f760081849 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 29 Apr 2025 15:17:13 -0700 Subject: [PATCH 086/191] Improve warmstarting if island structure exists. PiperOrigin-RevId: 752901506 Change-Id: I062d14df7da7d9dc1a2c1c30a16d0db7215af336 --- src/engine/engine_forward.c | 9 ++ test/engine/engine_solver_test.cc | 139 +++++++++++++++++++++--------- test/pipeline_test.cc | 25 ++++-- 3 files changed, 127 insertions(+), 46 deletions(-) diff --git a/src/engine/engine_forward.c b/src/engine/engine_forward.c index 3857bcae..a720f156 100644 --- a/src/engine/engine_forward.c +++ b/src/engine/engine_forward.c @@ -629,6 +629,15 @@ static void warmstart(const mjModel* m, mjData* d) { } } + // have island structure: unconstrained qacc = qacc_smooth + if (d->nisland > 0) { + for (int i=0; i < nv; i++) { + if (d->dof_island[i] < 0) { + d->qacc[i] = d->qacc_smooth[i]; + } + } + } + mj_freeStack(d); } diff --git a/test/engine/engine_solver_test.cc b/test/engine/engine_solver_test.cc index 6343ce24..b5b2db6d 100644 --- a/test/engine/engine_solver_test.cc +++ b/test/engine/engine_solver_test.cc @@ -29,88 +29,149 @@ namespace { using ::testing::DoubleNear; using ::testing::NotNull; -using ::testing::Pointwise; using ::std::vector; using ::std::abs; using ::std::max; -// compare two vectors, relative error (reduces size of large vector elements) +// compare two vectors, relative error (increase tolerance for large elements) inline void ExpectEqRel(vector v1, vector v2, mjtNum rtol) { ASSERT_TRUE(v1.size() == v2.size()); - - // make scale vector - int n = v1.size(); - vector scale(n); - for (int i = 0; i < n; i++) { - scale[i] = max(1.0, abs(v1[i]) + abs(v2[i])); + for (int i = 0; i < v1.size(); i++) { + mjtNum scale = 0.5 * max(2.0, abs(v1[i]) + abs(v2[i])); + EXPECT_THAT(v1[i], DoubleNear(v2[i], scale*rtol)); } - - // scale and compare - for (int i = 0; i < n; i++) { - v1[i] /= scale[i]; - v2[i] /= scale[i]; - } - EXPECT_THAT(v1, Pointwise(DoubleNear(rtol), v2)); } using SolverTest = MujocoTest; -static const char* const kIlslandEfcPath = - "engine/testdata/island/island_efc.xml"; +static const char* const kModelPath = + "testdata/model.xml"; // compare accelerations produced by CG solver with and without islands TEST_F(SolverTest, IslandsEquivalent) { - const std::string xml_path = GetTestDataFilePath(kIlslandEfcPath); + const std::string xml_path = GetTestDataFilePath(kModelPath); char error[1024]; mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, error, sizeof(error)); ASSERT_THAT(model, NotNull()) << error; model->opt.solver = mjSOL_CG; // use CG solver + model->opt.jacobian = mjJAC_SPARSE; // use sparse model->opt.tolerance = 0; // set tolerance to 0 - model->opt.enableflags &= ~mjENBL_ISLAND; // disable islands + model->opt.ls_tolerance = 0; // set ls_tolerance to 0 int nv = model->nv; int state_size = mj_stateSize(model, mjSTATE_INTEGRATION); mjtNum* state = (mjtNum*) mju_malloc(sizeof(mjtNum)*state_size); - mjtNum* qacc_diff = (mjtNum*) mju_malloc(sizeof(mjtNum)*nv); mjData* data_island = mj_makeData(model); mjData* data_noisland = mj_makeData(model); - mjtNum rtol = 1e-5; + // Below are 3 tolerances associated with 3 different iteration counts, + // they are only moderately tight, 2x higher than x86-64 failure on Linux, + // i.e. in that case the test fails with rtol smaller than {5e-3, 5e-4, 5e-5}. + // The point of this test is to show that CG convergence is actually not very + // precise, simply changing whether islands are used changes the solution by + // quite a lot, even at high iteration count and zero {ls_}tolerance. + // Increasing the iteration count higher than 60 does not improve convergence. + constexpr int kNumTol = 3; + mjtNum maxiter[kNumTol] = {30, 40, 60}; + mjtNum rtol[kNumTol] = {1e-2, 1e-3, 1e-4}; - for (bool warmstart : {true, false}) { - if (warmstart) { - model->opt.disableflags |= mjDSBL_WARMSTART; - } else { - model->opt.disableflags &= ~mjDSBL_WARMSTART; - } - mj_resetData(model, data_noisland); + for (int i = 0; i < kNumTol; ++i) { + model->opt.iterations = maxiter[i]; + model->opt.ls_iterations = maxiter[i]; - while (data_noisland->time < .3) { - mj_step(model, data_noisland); + for (bool coldstart : {true, false}) { + mj_resetDataKeyframe(model, data_noisland, 0); - mj_getState(model, data_noisland, state, mjSTATE_INTEGRATION); - mj_setState(model, data_island, state, mjSTATE_INTEGRATION); + if (coldstart) { + model->opt.disableflags |= mjDSBL_WARMSTART; + } else { + model->opt.disableflags &= ~mjDSBL_WARMSTART; + } - mj_forward(model, data_noisland); + while (data_noisland->time < .1) { + mj_getState(model, data_noisland, state, mjSTATE_INTEGRATION); + mj_setState(model, data_island, state, mjSTATE_INTEGRATION); - model->opt.enableflags |= mjENBL_ISLAND; // enable islands - mj_forward(model, data_island); - model->opt.enableflags &= ~mjENBL_ISLAND; // disable islands + model->opt.enableflags |= mjENBL_ISLAND; // enable islands + mj_forward(model, data_island); - ExpectEqRel(AsVector(data_noisland->qacc, nv), - AsVector(data_island->qacc, nv), rtol); + model->opt.enableflags &= ~mjENBL_ISLAND; // disable islands + mj_forward(model, data_noisland); + + auto time = std::to_string(data_noisland->time); + for (int j = 0; j < nv; j++) { + // increase tolerance for large elements + mjtNum scale = 0.5 * max(2.0, abs(data_noisland->qacc[j]) + + abs(data_island->qacc[j])); + EXPECT_THAT(data_noisland->qacc[j], + DoubleNear(data_island->qacc[j], scale * rtol[i])) + << "time: " << time << '\n' + << "dof: " << j << '\n' + << "maxiter: " << maxiter[i] << '\n' + << "rtol: " << scale * rtol[i]; + } + + mj_step(model, data_noisland); + } } } mj_deleteData(data_noisland); mj_deleteData(data_island); - mju_free(qacc_diff); mju_free(state); mj_deleteModel(model); } +// compare accelerations produced by CG solver with and without islands +TEST_F(SolverTest, IslandsEquivalentForward) { + const std::string xml_path = GetTestDataFilePath(kModelPath); + char error[1024]; + mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, error, sizeof(error)); + ASSERT_THAT(model, NotNull()) << error; + model->opt.solver = mjSOL_CG; // use CG solver + model->opt.tolerance = 0; // set tolerance to 0 + model->opt.ls_tolerance = 0; // set ls_tolerance to 0 + + mjtNum rtol = 2e-6; + + mjData* data_island = mj_makeData(model); + mjData* data_noisland = mj_makeData(model); + + for (bool coldstart : {true, false}) { + mj_resetDataKeyframe(model, data_island, 0); + mj_resetDataKeyframe(model, data_noisland, 0); + + if (coldstart) { + model->opt.disableflags |= mjDSBL_WARMSTART; + } else { + model->opt.disableflags &= ~mjDSBL_WARMSTART; + } + + model->opt.enableflags &= ~mjENBL_ISLAND; // disable islands + mj_forward(model, data_noisland); + + model->opt.enableflags |= mjENBL_ISLAND; // enable islands + mj_forward(model, data_island); + for (int j = 0; j < model->nv; j++) { + mjtNum scale = 0.5 * max(2.0, abs(data_noisland->qacc[j]) + + abs(data_island->qacc[j])); + EXPECT_THAT(data_noisland->qacc[j], + DoubleNear(data_island->qacc[j], scale * rtol)) + << "dof: " << j << '\n' + << "rtol: " << scale * rtol; + } + } + + mj_deleteData(data_noisland); + mj_deleteData(data_island); + mj_deleteModel(model); +} + +static const char* const kIlslandEfcPath = + "engine/testdata/island/island_efc.xml"; + // compare qacc from 1 iteration of monolithic CG solver and one big island TEST_F(SolverTest, OneBigIsland) { const std::string xml_path = GetTestDataFilePath(kIlslandEfcPath); diff --git a/test/pipeline_test.cc b/test/pipeline_test.cc index 6f1eb737..c602da54 100644 --- a/test/pipeline_test.cc +++ b/test/pipeline_test.cc @@ -45,24 +45,35 @@ TEST_F(PipelineTest, SparseDenseEquivalent) { mjtNum tol = 1e-11; - for (mjtSolver solver : {mjSOL_NEWTON, mjSOL_PGS, mjSOL_CG}) { - model->opt.solver = solver; + const char* sname[4] = {"NEWTON", "PGS", "CG", "NOSLIP"}; + mjtSolver solver[4] = {mjSOL_NEWTON, mjSOL_PGS, mjSOL_CG, mjSOL_NEWTON}; - // set dense jacobian, call mj_forward, save accelerations + for (int i : {0, 1, 2, 3}) { + model->opt.solver = solver[i]; + if (i == 3) { + model->opt.noslip_iterations = 2; + } + + // set dense jacobian, call mj_step, save qacc and new qpos model->opt.jacobian = mjJAC_DENSE; mj_resetDataKeyframe(model, data, 0); - mj_forward(model, data); + mj_step(model, data); std::vector qacc_dense = AsVector(data->qacc, model->nv); + std::vector qpos_dense = AsVector(data->qpos, model->nq); - // set sparse jacobian, call mj_forward, save accelerations + // set sparse jacobian, call mj_step, save qacc and new qpos model->opt.jacobian = mjJAC_SPARSE; mj_resetDataKeyframe(model, data, 0); - mj_forward(model, data); + mj_step(model, data); std::vector qacc_sparse = AsVector(data->qacc, model->nv); + std::vector qpos_sparse = AsVector(data->qpos, model->nq); // expect accelerations to be insignificantly different EXPECT_THAT(qacc_dense, Pointwise(DoubleNear(tol), qacc_sparse)) - << "failed equivalence for solver=" << solver; + << "failed qacc equivalence for solver=" << sname[i]; + // expect positions to be insignificantly different + EXPECT_THAT(qpos_dense, Pointwise(DoubleNear(tol), qpos_sparse)) + << "failed qpos equivalence for solver=" << sname[i]; } mj_deleteData(data); From 7742f6280323bd22f1cbaf7299dd2494882ef14b Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 29 Apr 2025 16:32:49 -0700 Subject: [PATCH 087/191] Add `mju_mulSymVecSparse`, private engine function Multiply sparse symmetric matrix (only lower triangle represented) by vector PiperOrigin-RevId: 752926934 Change-Id: I5aaae1266256aa88aee8c25e242b7a7c51ea8dd7 --- src/engine/engine_util_sparse.c | 38 ++++++++++++++++++++++++++ src/engine/engine_util_sparse.h | 6 ++++ test/engine/engine_util_sparse_test.cc | 36 ++++++++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/src/engine/engine_util_sparse.c b/src/engine/engine_util_sparse.c index 23c038ec..00fb99d9 100644 --- a/src/engine/engine_util_sparse.c +++ b/src/engine/engine_util_sparse.c @@ -193,6 +193,44 @@ void mju_mulMatTVecSparse(mjtNum* res, const mjtNum* mat, const mjtNum* vec, int +// multiply symmetric matrix (only lower triangle represented) by vector: +// res = (mat + strict_upper(mat')) * vec +void mju_mulSymVecSparse(mjtNum* restrict res, const mjtNum* restrict mat, + const mjtNum* restrict vec, int n, + const int* restrict rownnz, const int* restrict rowadr, + const int* restrict diagnum, const int* restrict colind) { + // clear res + mju_zero(res, n); + + // multiply + for (int i=0; i < n; i++) { + int adr = rowadr[i]; + int diag = rownnz[i] - 1; + const mjtNum* row = mat + adr; + + // diagonal + res[i] = row[diag] * vec[i]; + + // TODO: consider using SIMD if diagnum[i] >= 4 + + // shortcut for diagonal row/column + if (diagnum[i]) { + continue; + } + + // off-diagonals + const int* ind = colind + adr; + for (int k=0; k < diag; k++) { + int j = ind[k]; + mjtNum val = row[k]; + res[i] += val * vec[j]; // strict lower + res[j] += val * vec[i]; // strict upper + } + } +} + + + // count the number of non-zeros in the sum of two sparse vectors int mju_combineSparseCount(int a_nnz, int b_nnz, const int* a_ind, const int* b_ind) { int a = 0, b = 0, c_nnz = 0; diff --git a/src/engine/engine_util_sparse.h b/src/engine/engine_util_sparse.h index 1f75739b..1246abc1 100644 --- a/src/engine/engine_util_sparse.h +++ b/src/engine/engine_util_sparse.h @@ -50,6 +50,12 @@ MJAPI void mju_mulMatVecSparse(mjtNum* res, const mjtNum* mat, const mjtNum* vec MJAPI void mju_mulMatTVecSparse(mjtNum* res, const mjtNum* mat, const mjtNum* vec, int nr, int nc, const int* rownnz, const int* rowadr, const int* colind); +// multiply symmetric matrix (only lower triangle represented) by vector: +// res = (mat + strict_upper(mat')) * vec +MJAPI void mju_mulSymVecSparse(mjtNum* res, const mjtNum* mat, const mjtNum* vec, int n, + const int* rownnz, const int* rowadr, const int* diagnum, + const int* colind); + // compress sparse matrix, remove elements with abs(value) <= minval, return total non-zeros MJAPI int mju_compressSparse(mjtNum* mat, int nr, int nc, int* rownnz, int* rowadr, int* colind, mjtNum minval); diff --git a/test/engine/engine_util_sparse_test.cc b/test/engine/engine_util_sparse_test.cc index 41231c2c..a11d18e3 100644 --- a/test/engine/engine_util_sparse_test.cc +++ b/test/engine/engine_util_sparse_test.cc @@ -1034,6 +1034,42 @@ TEST_F(EngineUtilSparseTest, MjuMulMatTVec) { EXPECT_THAT(AsVector(res, 3), ElementsAre(5, 28, 24)); } +TEST_F(EngineUtilSparseTest, MjuMulSymVecSparse) { + constexpr int n = 4; + constexpr int nnz = 9; + + mjtNum mat[n*n] = {1, 0, 0, 0, + -1, 2, 0, 0, // spurious (ignored) -1 at (1, 0) + 3, 0, 4, 0, + 5, 6, 7, 8}; + + // dense, full matrix + mjtNum sym[n*n] = {1, 0, 3, 5, + 0, 2, 0, 6, + 3, 0, 4, 7, + 5, 6, 7, 8}; + + mjtNum mat_sparse[nnz]; + int rownnz[n]; + int rowadr[n]; + int colind[nnz]; + mju_dense2sparse(mat_sparse, mat, n, n, rownnz, rowadr, colind, nnz); + int diagnum[n] = {0, 1, 0, 0}; + + // multiply: res = (mat + strict_upper(mat')) * vec + mjtNum vec[n] = {4, 3, 2, 1}; + mjtNum res[n]; + mju_mulSymVecSparse(res, mat_sparse, vec, n, rownnz, rowadr, diagnum, colind); + + // dense multiply + mjtNum res2[n]; + mju_mulMatVec(res2, sym, vec, n, n); + + for (int i=0; i < n; i++) { + EXPECT_EQ(res[i], res2[i]); + } +} + TEST_F(EngineUtilSparseTest, MjuDenseToSparse) { int nr = 2; int nc = 2; From 45d3fef2fc31f02667d3a63014a6ba171840d88c Mon Sep 17 00:00:00 2001 From: Robin Alazard Date: Wed, 30 Apr 2025 04:14:43 -0700 Subject: [PATCH 088/191] Add initial support for colliders By default every geom is a collider unless contype=conaffinity=0. PiperOrigin-RevId: 753108713 Change-Id: I0e1a81b254c41e9103c34f69696205ff5c0bd6dc --- .../usd/plugins/mjcf/mujoco_to_usd.cc | 16 ++- .../usd/plugins/mjcf/mjcf_file_format_test.cc | 122 ++++++++++++++++++ 2 files changed, 134 insertions(+), 4 deletions(-) diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc index 8ddba52d..832d1cf3 100644 --- a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc @@ -832,7 +832,7 @@ class ModelWriter { site_path, pxr::VtArray{kTokens->xformOpTransform}); } - void WriteGeom(mjsGeom *geom, const mjsBody *body) { + void WriteGeom(mjsGeom *geom, const mjsBody *body, bool write_physics) { const int body_id = mjs_getId(body->element); const auto &body_path = body_paths_[body_id]; @@ -866,6 +866,14 @@ class ModelWriter { return; } + // Apply the PhysicsCollisionAPI schema if we are writing physics and the + // geom participates in collisions. + if (write_physics && (model_->geom_contype[geom_id] != 0 || + model_->geom_conaffinity[geom_id] != 0)) { + ApplyApiSchema(data_, geom_path, + pxr::UsdPhysicsTokens->PhysicsCollisionAPI); + } + mjsDefault *spec_default = mjs_getDefault(geom->element); pxr::TfToken valid_class_name = GetValidPrimName(*spec_default->name); pxr::SdfPath geom_class_path = class_path_.AppendChild(valid_class_name); @@ -940,10 +948,10 @@ class ModelWriter { } } - void WriteGeoms(mjsBody *body) { + void WriteGeoms(mjsBody *body, bool write_physics) { mjsGeom *geom = mjs_asGeom(mjs_firstChild(body, mjOBJ_GEOM, false)); while (geom) { - WriteGeom(geom, body); + WriteGeom(geom, body, write_physics); geom = mjs_asGeom(mjs_nextChild(body, geom->element, false)); } } @@ -1106,7 +1114,7 @@ class ModelWriter { WriteBody(body, write_physics); } WriteSites(body); - WriteGeoms(body); + WriteGeoms(body, write_physics); WriteCameras(body); WriteLights(body); body = mjs_asBody(mjs_nextElement(spec_, body->element)); diff --git a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc index 78f10f46..2273ac5a 100644 --- a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc +++ b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc @@ -45,6 +45,7 @@ #include #include #include +#include #include PXR_NAMESPACE_OPEN_SCOPE @@ -607,5 +608,126 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsRigidBody) { pxr::UsdPhysicsRigidBodyAPI); } +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsColliders) { + static constexpr char kXml[] = R"( + + + + + + + + + + + + + + + + + + + + + + )"; + + pxr::SdfFileFormat::FileFormatArguments args; + args["usdMjcfToggleUsdPhysics"] = "true"; + pxr::SdfLayerRefPtr layer = LoadLayer(kXml, args); + auto stage = pxr::UsdStage::Open(layer); + + EXPECT_THAT(stage, testing::NotNull()); + EXPECT_PRIM_VALID(stage, "/test"); + + // Expected hierarchy under /test: + // + // ground [collider] + // + // body_0/body_0 [rigidbody] + // body_0/body_0/body_0_col [collider] + // + // body_0/body_0_0 [rigidbody] <-- Note: USD reparents nested rigid bodies + // body_0/body_0/body_0_0/body_0_0_col [collider] + // + // body_1/body_1 [rigidbody] + // body_1/body_1/body_1_col_0 [collider] + // body_1/body_1/body_1_col_1 [collider] + // + // body_2/body_2 [rigidbody] + // body_2/body_2/body_2_nocol [] + + // ground [collider] (Static collider) + EXPECT_PRIM_VALID(stage, "/test/ground"); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/ground", + pxr::UsdPhysicsRigidBodyAPI); + EXPECT_PRIM_API_APPLIED(stage, "/test/ground", pxr::UsdPhysicsCollisionAPI); + + // body_0/body_0 [rigidbody] + EXPECT_PRIM_VALID(stage, "/test/body_0/body_0"); + EXPECT_PRIM_API_APPLIED(stage, "/test/body_0/body_0", + pxr::UsdPhysicsRigidBodyAPI); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_0/body_0", + pxr::UsdPhysicsCollisionAPI); + // body_0/body_0/body_0_col [collider] + EXPECT_PRIM_VALID(stage, "/test/body_0/body_0/body_0_col"); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_0/body_0/body_0_col", + pxr::UsdPhysicsRigidBodyAPI); + EXPECT_PRIM_API_APPLIED(stage, "/test/body_0/body_0/body_0_col", + pxr::UsdPhysicsCollisionAPI); + + // body_0/body_0_0 [rigidbody] (Nested body - reparented) + EXPECT_PRIM_VALID(stage, "/test/body_0/body_0_0"); + EXPECT_PRIM_API_APPLIED(stage, "/test/body_0/body_0_0", + pxr::UsdPhysicsRigidBodyAPI); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_0/body_0_0", + pxr::UsdPhysicsCollisionAPI); + // body_0/body_0_0/body_0_0_col [collider] + EXPECT_PRIM_VALID(stage, "/test/body_0/body_0_0/body_0_0_col"); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_0/body_0_0/body_0_0_col", + pxr::UsdPhysicsRigidBodyAPI); + EXPECT_PRIM_API_APPLIED(stage, "/test/body_0/body_0_0/body_0_0_col", + pxr::UsdPhysicsCollisionAPI); + + // body_1/body_1 [rigidbody] + EXPECT_PRIM_VALID(stage, "/test/body_1/body_1"); + EXPECT_PRIM_API_APPLIED(stage, "/test/body_1/body_1", + pxr::UsdPhysicsRigidBodyAPI); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_1/body_1", + pxr::UsdPhysicsCollisionAPI); + // body_1/body_1/body_1_col_0 [collider] + EXPECT_PRIM_VALID(stage, "/test/body_1/body_1/body_1_col_0"); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_1/body_1/body_1_col_0", + pxr::UsdPhysicsRigidBodyAPI); + EXPECT_PRIM_API_APPLIED(stage, "/test/body_1/body_1/body_1_col_0", + pxr::UsdPhysicsCollisionAPI); + // body_1/body_1/body_1_col_1 [collider] + EXPECT_PRIM_VALID(stage, "/test/body_1/body_1/body_1_col_1"); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_1/body_1/body_1_col_1", + pxr::UsdPhysicsRigidBodyAPI); + EXPECT_PRIM_API_APPLIED(stage, "/test/body_1/body_1/body_1_col_1", + pxr::UsdPhysicsCollisionAPI); + + // body_2/body_2 [rigidbody] + EXPECT_PRIM_VALID(stage, "/test/body_2/body_2"); + EXPECT_PRIM_API_APPLIED(stage, "/test/body_2/body_2", + pxr::UsdPhysicsRigidBodyAPI); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_2/body_2", + pxr::UsdPhysicsCollisionAPI); + // body_2/body_2/body_2_nocol [] (No physics APIs applied) + EXPECT_PRIM_VALID(stage, "/test/body_2/body_2/body_2_nocol"); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_2/body_2/body_2_nocol", + pxr::UsdPhysicsRigidBodyAPI); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_2/body_2/body_2_nocol", + pxr::UsdPhysicsCollisionAPI); +} + } // namespace } // namespace mujoco From 177fce9fa17405f7bdbb1741043e3e7dca7a3ec5 Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Wed, 30 Apr 2025 06:33:41 -0700 Subject: [PATCH 089/191] Remove absl dep and custom MujocoTest from usd plugin tests. PiperOrigin-RevId: 753144576 Change-Id: I3d4fd470c698967a50b189b99c1bb808a9a80306 --- test/experimental/usd/plugins/mjcf/fixture.cc | 2 -- test/experimental/usd/plugins/mjcf/fixture.h | 4 ---- test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/test/experimental/usd/plugins/mjcf/fixture.cc b/test/experimental/usd/plugins/mjcf/fixture.cc index 74063da2..cc3362e0 100644 --- a/test/experimental/usd/plugins/mjcf/fixture.cc +++ b/test/experimental/usd/plugins/mjcf/fixture.cc @@ -18,7 +18,6 @@ #include #include -#include #include #include #include @@ -62,5 +61,4 @@ void ExpectAttributeHasConnection(pxr::UsdStageRefPtr stage, const char* path, EXPECT_EQ(sources.size(), 1); EXPECT_EQ(sources[0], SdfPath(connection_path)); } -// } // namespace mujoco diff --git a/test/experimental/usd/plugins/mjcf/fixture.h b/test/experimental/usd/plugins/mjcf/fixture.h index d7b8cda1..c31082d0 100644 --- a/test/experimental/usd/plugins/mjcf/fixture.h +++ b/test/experimental/usd/plugins/mjcf/fixture.h @@ -18,7 +18,6 @@ #include #include -#include "test/fixture.h" #include #include #include @@ -95,8 +94,5 @@ void ExpectAttributeEqual(pxr::UsdStageRefPtr stage, void ExpectAttributeHasConnection(pxr::UsdStageRefPtr stage, const char* path, const char* connection_path); - -using MjcfSdfFileFormatPluginTest = MujocoTest; - } // namespace mujoco #endif // MUJOCO_TEST_EXPERIMENTAL_USD_PLUGINS_MJCF_FIXTURE_H_ diff --git a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc index 2273ac5a..fd6e2563 100644 --- a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc +++ b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc @@ -17,7 +17,6 @@ #include #include -#include #include "test/experimental/usd/plugins/mjcf/fixture.h" #include "test/fixture.h" #include @@ -60,6 +59,7 @@ namespace mujoco { namespace { using pxr::SdfPath; +using MjcfSdfFileFormatPluginTest = MujocoTest; static const char* kMaterialsPath = "experimental/usd/plugins/mjcf/testdata/materials.xml"; From bb07fa0c78934b59c87eadb30e03f8ec8366f924 Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Wed, 30 Apr 2025 08:43:18 -0700 Subject: [PATCH 090/191] Move Mujoco USD test fixture out of MJCF plugin directory and rename to test_utils. PiperOrigin-RevId: 753183581 Change-Id: Iedd6594ac679ca026f8af29c647d18ae93003a53 --- test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc | 4 +++- .../usd/{plugins/mjcf/fixture.cc => test_utils.cc} | 4 +++- .../experimental/usd/{plugins/mjcf/fixture.h => test_utils.h} | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) rename test/experimental/usd/{plugins/mjcf/fixture.cc => test_utils.cc} (96%) rename test/experimental/usd/{plugins/mjcf/fixture.h => test_utils.h} (99%) diff --git a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc index fd6e2563..fb2a1a65 100644 --- a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc +++ b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc @@ -17,7 +17,7 @@ #include #include -#include "test/experimental/usd/plugins/mjcf/fixture.h" +#include "test/experimental/usd/test_utils.h" #include "test/fixture.h" #include #include @@ -56,6 +56,7 @@ TF_DEFINE_PRIVATE_TOKENS(_tokens, PXR_NAMESPACE_CLOSE_SCOPE namespace mujoco { +namespace usd { namespace { using pxr::SdfPath; @@ -730,4 +731,5 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsColliders) { } } // namespace +} // namespace usd } // namespace mujoco diff --git a/test/experimental/usd/plugins/mjcf/fixture.cc b/test/experimental/usd/test_utils.cc similarity index 96% rename from test/experimental/usd/plugins/mjcf/fixture.cc rename to test/experimental/usd/test_utils.cc index cc3362e0..8bfc30e0 100644 --- a/test/experimental/usd/plugins/mjcf/fixture.cc +++ b/test/experimental/usd/test_utils.cc @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "test/experimental/usd/plugins/mjcf/fixture.h" +#include "test/experimental/usd/test_utils.h" #include @@ -28,6 +28,7 @@ #include #include namespace mujoco { +namespace usd { using pxr::SdfPath; @@ -61,4 +62,5 @@ void ExpectAttributeHasConnection(pxr::UsdStageRefPtr stage, const char* path, EXPECT_EQ(sources.size(), 1); EXPECT_EQ(sources[0], SdfPath(connection_path)); } +} // namespace usd } // namespace mujoco diff --git a/test/experimental/usd/plugins/mjcf/fixture.h b/test/experimental/usd/test_utils.h similarity index 99% rename from test/experimental/usd/plugins/mjcf/fixture.h rename to test/experimental/usd/test_utils.h index c31082d0..ec5e3de7 100644 --- a/test/experimental/usd/plugins/mjcf/fixture.h +++ b/test/experimental/usd/test_utils.h @@ -67,6 +67,7 @@ EXPECT_FALSE((stage)->GetAttributeAtPath(SdfPath(path)).HasValue()); namespace mujoco { +namespace usd { pxr::SdfLayerRefPtr LoadLayer( const std::string& xml, @@ -94,5 +95,6 @@ void ExpectAttributeEqual(pxr::UsdStageRefPtr stage, void ExpectAttributeHasConnection(pxr::UsdStageRefPtr stage, const char* path, const char* connection_path); +} // namespace usd } // namespace mujoco #endif // MUJOCO_TEST_EXPERIMENTAL_USD_PLUGINS_MJCF_FIXTURE_H_ From 6c319310b88cca9a7880c210602426fbb6e9f519 Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Wed, 30 Apr 2025 11:00:48 -0700 Subject: [PATCH 091/191] On Darwin archs, add ArchConstructEntry using to placate compile errors. PiperOrigin-RevId: 753235230 Change-Id: I621fd879ba1a0e6b7668454386f35ae76a190c13 --- src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc index 832d1cf3..d4da714a 100644 --- a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc @@ -98,6 +98,9 @@ using pxr::Tf_RegistryInit; using pxr::TfEnum; template using Arch_PerLibInit = pxr::Arch_PerLibInit; +#if defined(ARCH_OS_DARWIN) +using Arch_ConstructorEntry = pxr::Arch_ConstructorEntry; +#endif enum ErrorCodes { UnsupportedGeomTypeError, MujocoCompilationError }; TF_REGISTRY_FUNCTION(pxr::TfEnum) { From 02656532c2ef7ebb1f7e45158fc6a01a856fd61e Mon Sep 17 00:00:00 2001 From: Robin Alazard Date: Fri, 2 May 2025 03:46:34 -0700 Subject: [PATCH 092/191] Fix Mesh PhysicsCollisionAPI by accounting for how it's referenced/instanced. Because we introduce an additional parent scope when referencing a `Mesh` prim (required for instancing), we need to create an `over` prim that allows to actually operate on the to-be-referenced Mesh. PiperOrigin-RevId: 753941355 Change-Id: I829be9e82fe650802afcbc46f4b2f4c81eb53bd1 --- .../usd/plugins/mjcf/mujoco_to_usd.cc | 16 ++++++--- .../usd/plugins/mjcf/mjcf_file_format_test.cc | 34 ++++++++++++++++++- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc index d4da714a..b89abc3c 100644 --- a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc @@ -66,7 +66,6 @@ using TfStaticData = pxr::TfStaticData; // clang-format off TF_DEFINE_PRIVATE_TOKENS(kTokens, - // Xform ops ((body, "Body")) ((body_name, "mujoco:body_name")) ((geom, "Geom")) @@ -89,7 +88,8 @@ TF_DEFINE_PRIVATE_TOKENS(kTokens, ((outputsRgb, "outputs:rgb")) ((inputsMetallic, "inputs:metallic")) (repeat) - ); + ((sourceMesh, pxr::UsdGeomTokens->Mesh)) + ); // Using to satisfy TF_REGISTRY_FUNCTION macro below and avoid operating in PXR_NS. using pxr::TfEnum; @@ -330,7 +330,7 @@ class ModelWriter { pxr::SdfPath subcomponent_path = CreatePrimSpec(data_, parent_path, name, pxr::UsdGeomTokens->Xform); pxr::SdfPath mesh_path = - CreatePrimSpec(data_, subcomponent_path, pxr::UsdGeomTokens->Mesh, + CreatePrimSpec(data_, subcomponent_path, kTokens->sourceMesh, pxr::UsdGeomTokens->Mesh); mesh_paths_[*mesh->name] = subcomponent_path; @@ -600,7 +600,15 @@ class ModelWriter { // Reference the mesh asset written in WriteMeshes. AddPrimReference(data_, subcomponent_path, mesh_paths_[*geom->meshname]); - return subcomponent_path; + // We want to use instancing with meshes, and it requires creating a parent + // scope to be referenced, with the Mesh prim as a child. + // To be able to actually manipulate the Mesh prim, we need to create and + // return the corresponding `over` prim as a child of the referencing prim. + pxr::SdfPath over_mesh_path = + CreatePrimSpec(data_, subcomponent_path, kTokens->sourceMesh, + pxr::UsdGeomTokens->Mesh, pxr::SdfSpecifierOver); + + return over_mesh_path; } pxr::SdfPath WriteSiteGeom(const mjsSite *site, diff --git a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc index fb2a1a65..abdf6a23 100644 --- a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc +++ b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc @@ -612,6 +612,9 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsRigidBody) { TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsColliders) { static constexpr char kXml[] = R"( + + + @@ -636,6 +639,11 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsColliders) { + + + + )"; @@ -655,7 +663,7 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsColliders) { // body_0/body_0 [rigidbody] // body_0/body_0/body_0_col [collider] // - // body_0/body_0_0 [rigidbody] <-- Note: USD reparents nested rigid bodies + // body_0/body_0_0 [rigidbody] <-- USD reparents nested rigid bodies // body_0/body_0/body_0_0/body_0_0_col [collider] // // body_1/body_1 [rigidbody] @@ -664,6 +672,10 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsColliders) { // // body_2/body_2 [rigidbody] // body_2/body_2/body_2_nocol [] + // + // body_3/body_3 [rigidbody] + // body_3/body_3/body_3_col [] <-- Intermediate prim for mesh instancing + // body_3/body_3/body_3_col/Mesh [collider] // ground [collider] (Static collider) EXPECT_PRIM_VALID(stage, "/test/ground"); @@ -728,6 +740,26 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsColliders) { pxr::UsdPhysicsRigidBodyAPI); EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_2/body_2/body_2_nocol", pxr::UsdPhysicsCollisionAPI); + + // body_3/body_3 [rigidbody] + EXPECT_PRIM_VALID(stage, "/test/body_3/body_3"); + EXPECT_PRIM_API_APPLIED(stage, "/test/body_3/body_3", + pxr::UsdPhysicsRigidBodyAPI); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_3/body_3", + pxr::UsdPhysicsCollisionAPI); + // body_3/body_3/body_3_col [] (Intermediate prim for mesh instancing) + EXPECT_PRIM_VALID(stage, "/test/body_3/body_3/body_3_col"); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_3/body_3/body_3_col", + pxr::UsdPhysicsRigidBodyAPI); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_3/body_3/body_3_col", + pxr::UsdPhysicsCollisionAPI); + // body_3/body_3/body_3_col/Mesh [collider] + EXPECT_PRIM_VALID(stage, "/test/body_3/body_3/body_3_col/Mesh"); + EXPECT_PRIM_API_NOT_APPLIED( + stage, "/test/body_3/body_3/body_3_col/Mesh", + pxr::UsdPhysicsRigidBodyAPI); + EXPECT_PRIM_API_APPLIED(stage, "/test/body_3/body_3/body_3_col/Mesh", + pxr::UsdPhysicsCollisionAPI); } } // namespace From ab63a7e824dc26d7a8a8519bdcc77dc611208f6a Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Fri, 2 May 2025 05:47:25 -0700 Subject: [PATCH 093/191] Add CMakeLists for usdMjcf file format plugin. This adds an option MUJOCO_BUILD_USD_PLUGINS to the top level CMakeLists which allows us to build the usdMjcf file format plugin with the rest of mujoco. In this first version, we allow building against either standalone USD or against Houdini (DCC) since it has it's own fork of USD. When building against standalone USD we also build the file format tests. PiperOrigin-RevId: 753969974 Change-Id: If691eecbddf45d18b7c4ff76f7650999d42e99c5 --- .github/workflows/build.yml | 2 +- CMakeLists.txt | 12 ++ plugin/sdf/CMakeLists.txt | 12 +- src/experimental/usd/plugins/CMakeLists.txt | 137 ++++++++++++++++++ .../usd/plugins/mjcf/plugInfo.json | 2 +- test/CMakeLists.txt | 1 + test/experimental/CMakeLists.txt | 19 +++ .../usd/plugins/mjcf/CMakeLists.txt | 30 ++++ 8 files changed, 207 insertions(+), 8 deletions(-) create mode 100644 src/experimental/usd/plugins/CMakeLists.txt create mode 100644 test/experimental/CMakeLists.txt create mode 100644 test/experimental/usd/plugins/mjcf/CMakeLists.txt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1a3288b9..4762eb73 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -179,7 +179,7 @@ jobs: cp lib/libactuator.* ${{ matrix.tmpdir }}/mujoco_install/mujoco_plugin && cp lib/libelasticity.* ${{ matrix.tmpdir }}/mujoco_install/mujoco_plugin && cp lib/libsensor.* ${{ matrix.tmpdir }}/mujoco_install/mujoco_plugin && - cp lib/libsdf.* ${{ matrix.tmpdir }}/mujoco_install/mujoco_plugin + cp lib/libsdf_plugin.* ${{ matrix.tmpdir }}/mujoco_install/mujoco_plugin - name: Copy plugins (Windows) if: ${{ runner.os == 'Windows' }} working-directory: build diff --git a/CMakeLists.txt b/CMakeLists.txt index 087a81e7..3cab120d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,14 @@ option(MUJOCO_BUILD_EXAMPLES "Build samples for MuJoCo" ON) option(MUJOCO_BUILD_SIMULATE "Build simulate library for MuJoCo" ON) option(MUJOCO_BUILD_TESTS "Build tests for MuJoCo" ON) option(MUJOCO_TEST_PYTHON_UTIL "Build and test utility libraries for Python bindings" ON) +option(MUJOCO_BUILD_USD_PLUGINS "Build OpenUSD plugins" OFF) + +# USD libs to compile against. +set(MUJOCO_USD_ALLOWED_TARGET_VALUES "USD" "Houdini") +set(MUJOCO_USD_TARGET "USD" CACHE STRING "Select the USD target for the project.") +set_property(CACHE MUJOCO_USD_TARGET + PROPERTY STRINGS ${MUJOCO_USD_ALLOWED_TARGET_VALUES} +) if(APPLE AND (MUJOCO_BUILD_EXAMPLES OR MUJOCO_BUILD_SIMULATE)) enable_language(OBJC) @@ -181,6 +189,10 @@ if(MUJOCO_BUILD_EXAMPLES) add_subdirectory(sample) endif() +if(MUJOCO_BUILD_USD_PLUGINS) + add_subdirectory(src/experimental/usd/plugins) +endif() + if(BUILD_TESTING AND MUJOCO_BUILD_TESTS) enable_testing() add_subdirectory(test) diff --git a/plugin/sdf/CMakeLists.txt b/plugin/sdf/CMakeLists.txt index 3e216fc4..8b834971 100644 --- a/plugin/sdf/CMakeLists.txt +++ b/plugin/sdf/CMakeLists.txt @@ -34,19 +34,19 @@ set(MUJOCO_SDF_SRCS torus.h ) -add_library(sdf SHARED) -target_sources(sdf PRIVATE ${MUJOCO_SDF_SRCS}) -target_include_directories(sdf PRIVATE ${MUJOCO_SDF_INCLUDE}) -target_link_libraries(sdf PRIVATE mujoco SdfLib) +add_library(sdf_plugin SHARED) +target_sources(sdf_plugin PRIVATE ${MUJOCO_SDF_SRCS}) +target_include_directories(sdf_plugin PRIVATE ${MUJOCO_SDF_INCLUDE}) +target_link_libraries(sdf_plugin PRIVATE mujoco SdfLib) target_compile_options( - sdf + sdf_plugin PRIVATE ${AVX_COMPILE_OPTIONS} ${MUJOCO_MACOS_COMPILE_OPTIONS} ${EXTRA_COMPILE_OPTIONS} ${MUJOCO_CXX_FLAGS} ) target_link_options( - sdf + sdf_plugin PRIVATE ${MUJOCO_MACOS_LINK_OPTIONS} ${EXTRA_LINK_OPTIONS} diff --git a/src/experimental/usd/plugins/CMakeLists.txt b/src/experimental/usd/plugins/CMakeLists.txt new file mode 100644 index 00000000..6e506493 --- /dev/null +++ b/src/experimental/usd/plugins/CMakeLists.txt @@ -0,0 +1,137 @@ +# Copyright 2025 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 +# +# https://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. + + +# Plugin target name (used for library and plugInfo.json) +set(MJCF_PLUGIN_TARGET_NAME usdMjcf) + +add_library(${MJCF_PLUGIN_TARGET_NAME} SHARED + mjcf/mjcf_file_format.cc + mjcf/mjcf_file_format.h + mjcf/mujoco_to_usd.cc + mjcf/mujoco_to_usd.h + mjcf/utils.cc + mjcf/utils.h +) + +# We need to set the visibility to default until core type symbol visibility +# is resolved in OpenUSD https://github.com/PixarAnimationStudios/OpenUSD/issues/1475 +# Otherwise we will run into issues during composition on MacOS due to std::type_info +# comparisons failing for pxr::TfTokenVector and the like that we place in SdfAbstractData. +set_target_properties(${MJCF_PLUGIN_TARGET_NAME} PROPERTIES + OUTPUT_NAME ${MJCF_PLUGIN_TARGET_NAME} + CXX_VISIBILITY_PRESET default +) + +target_include_directories(${MJCF_PLUGIN_TARGET_NAME} PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}" +) + +if (MUJOCO_USD_TARGET STREQUAL "USD") + find_package(pxr REQUIRED) + + # --- Link Dependencies --- + # Link against the necessary OpenUSD components + target_link_libraries(${MJCF_PLUGIN_TARGET_NAME} PRIVATE + usd + ar + kind + tf + gf + vt + usdShade + usdLux + usdGeom + usdImaging + usdPhysics + mujoco + tinyxml2 + ) +elseif (MUJOCO_USD_TARGET STREQUAL "Houdini") + + if (NOT DEFINED ENV{HFS}) + message(FATAL_ERROR "Environment variable 'HFS' is not defined: $ENV{HFS}. Please run houdini_setup.") + endif() + + # In Houdini, the Houdini package we would typically use via find_package + # does not have all the USD dependencies that we need (namely UsdPhysics) + # so we need to manually link all the required libraries. + + set(HFS_ENV "$ENV{HFS}") + set(HOUDINI_LIBS "${HFS_ENV}/../Libraries") + get_filename_component(HOUDINI_LIBS "${HOUDINI_LIBS}" ABSOLUTE) # Normalize the path + target_link_directories(${MJCF_PLUGIN_TARGET_NAME} PRIVATE ${HOUDINI_LIBS}) + + target_include_directories(${MJCF_PLUGIN_TARGET_NAME} PRIVATE + "${HFS_ENV}/toolkit/include" + "${HFS_ENV}/toolkit/include/python3.11" + ) + + # Assume everyone using Houdini on 3.11 for now. + set(USD_MJCF_PYTHON_LIB python3.11) + set(USD_MJCF_PYTHON_LIB_NUMBER python311) + set(PYTHON_LIB "${HFS_ENV}/Frameworks/Python.framework/Versions/3.11/Python") + set(PXR_LIB_PREFIX "pxr_") + + # --- Link Dependencies --- + # Link against the necessary OpenUSD components + target_link_libraries(${MJCF_PLUGIN_TARGET_NAME} PRIVATE + ${PXR_LIB_PREFIX}usd + ${PXR_LIB_PREFIX}ar + ${PXR_LIB_PREFIX}kind + ${PXR_LIB_PREFIX}tf + ${PXR_LIB_PREFIX}gf + ${PXR_LIB_PREFIX}vt + ${PXR_LIB_PREFIX}sdf + ${PXR_LIB_PREFIX}usdShade + ${PXR_LIB_PREFIX}usdLux + ${PXR_LIB_PREFIX}usdGeom + ${PXR_LIB_PREFIX}usdImaging + ${PXR_LIB_PREFIX}usdPhysics + tbb + hboost_${USD_MJCF_PYTHON_LIB_NUMBER} + ${PYTHON_LIB} + mujoco + tinyxml2 + ) +endif() + + +# --- Generate plugInfo.json --- +if(CMAKE_SHARED_LIBRARY_PREFIX) + set(LIB_PREFIX ${CMAKE_SHARED_LIBRARY_PREFIX}) # Usually "lib" on Unix +else() + set(LIB_PREFIX "") +endif() +set(PLUG_INFO_LIBRARY_PATH "${LIB_PREFIX}${MJCF_PLUGIN_TARGET_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}") + +# --- Installation --- + +set(USD_PLUGIN_INSTALL_DIR_LIB ${CMAKE_INSTALL_LIBDIR}/usdMjcf) + +message(STATUS "Copying plugInfo.json to ${CMAKE_BINARY_DIR}/${USD_PLUGIN_INSTALL_DIR_LIB}/plugInfo.json") +configure_file( + mjcf/plugInfo.json + ${CMAKE_BINARY_DIR}/${USD_PLUGIN_INSTALL_DIR_LIB}/plugInfo.json +) + +install(FILES ${CMAKE_BINARY_DIR}/${USD_PLUGIN_INSTALL_DIR_LIB}/plugInfo.json DESTINATION ${USD_PLUGIN_INSTALL_DIR_LIB}) + +# Install shared lib and plugInfo to same location for simplicity. +install(TARGETS ${MJCF_PLUGIN_TARGET_NAME} + LIBRARY DESTINATION ${USD_PLUGIN_INSTALL_DIR_LIB} +) + +message(STATUS "USD MJCF Plugin will be installed to: ${CMAKE_INSTALL_PREFIX}/${USD_PLUGIN_INSTALL_DIR_LIB}") +message(STATUS "Make sure PXR_PLUGINPATH_NAME includes: ${CMAKE_INSTALL_PREFIX}/${USD_PLUGIN_INSTALL_DIR_LIB}") diff --git a/src/experimental/usd/plugins/mjcf/plugInfo.json b/src/experimental/usd/plugins/mjcf/plugInfo.json index 25869b76..da1f9ebd 100644 --- a/src/experimental/usd/plugins/mjcf/plugInfo.json +++ b/src/experimental/usd/plugins/mjcf/plugInfo.json @@ -15,7 +15,7 @@ } } }, - "LibraryPath": "", + "LibraryPath": "@PLUG_INFO_LIBRARY_PATH@", "Name": "usdMjcf", "ResourcePath": "", "Root": ".", diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0f92803d..a286a1c6 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -100,3 +100,4 @@ add_subdirectory(user) add_subdirectory(xml) add_subdirectory(plugin/elasticity) add_subdirectory(plugin/actuator) +add_subdirectory(experimental) diff --git a/test/experimental/CMakeLists.txt b/test/experimental/CMakeLists.txt new file mode 100644 index 00000000..9f5100db --- /dev/null +++ b/test/experimental/CMakeLists.txt @@ -0,0 +1,19 @@ +# 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 +# +# https://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. + +if(MUJOCO_BUILD_USD_PLUGINS AND MUJOCO_USD_TARGET STREQUAL "USD") + add_subdirectory(usd/plugins/mjcf) +endif() + + diff --git a/test/experimental/usd/plugins/mjcf/CMakeLists.txt b/test/experimental/usd/plugins/mjcf/CMakeLists.txt new file mode 100644 index 00000000..1b747aee --- /dev/null +++ b/test/experimental/usd/plugins/mjcf/CMakeLists.txt @@ -0,0 +1,30 @@ +# Copyright 2025 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 +# +# https://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. + +find_package(pxr REQUIRED) + +add_library(usd_fixture STATIC fixture.h fixture.cc) +target_include_directories(usd_fixture PUBLIC ${MUJOCO_TEST_INCLUDE}) +target_compile_definitions(usd_fixture PUBLIC MJSTATIC) + +target_link_libraries( + usd_fixture + PUBLIC usd + tf + gtest + gmock + mujoco +) + +mujoco_test(mjcf_file_format_test ADDITIONAL_LINK_LIBRARIES usd tf usdGeom usdImaging usdPhysics usdShade usd_fixture) From 253abad210823254f0f238ef3ad4cc8d975372d3 Mon Sep 17 00:00:00 2001 From: Robin Alazard Date: Fri, 2 May 2025 08:34:28 -0700 Subject: [PATCH 094/191] Add MeshCollisionAPI to collider Meshes with convexHull approximation. PiperOrigin-RevId: 754014642 Change-Id: I44cff505a2b9b7120a38f8c896b2c6560ce51c43 --- .../usd/plugins/mjcf/mujoco_to_usd.cc | 15 +++++++++++++++ .../usd/plugins/mjcf/mjcf_file_format_test.cc | 16 +++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc index b89abc3c..ef5e9f71 100644 --- a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc @@ -883,6 +883,21 @@ class ModelWriter { model_->geom_conaffinity[geom_id] != 0)) { ApplyApiSchema(data_, geom_path, pxr::UsdPhysicsTokens->PhysicsCollisionAPI); + // For meshes, also apply PhysicsMeshCollisionAPI and set the + // approximation attribute. + if (geom->type == mjGEOM_MESH) { + ApplyApiSchema(data_, geom_path, + pxr::UsdPhysicsTokens->PhysicsMeshCollisionAPI); + + // Note: MuJoCo documentation states that for collision purposes, meshes + // are always replaced with their convex hulls. Therefore, we set the + // approximation attribute to convexHull explicitly. + pxr::SdfPath approximation_attr = CreateAttributeSpec( + data_, geom_path, pxr::UsdPhysicsTokens->physicsApproximation, + pxr::SdfValueTypeNames->Token, pxr::SdfVariabilityUniform); + SetAttributeDefault(data_, approximation_attr, + pxr::UsdPhysicsTokens->convexHull); + } } mjsDefault *spec_default = mjs_getDefault(geom->element); diff --git a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc index abdf6a23..22b8ac52 100644 --- a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc +++ b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc @@ -45,7 +45,9 @@ #include #include #include +#include #include +#include PXR_NAMESPACE_OPEN_SCOPE // clang-format off @@ -675,7 +677,7 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsColliders) { // // body_3/body_3 [rigidbody] // body_3/body_3/body_3_col [] <-- Intermediate prim for mesh instancing - // body_3/body_3/body_3_col/Mesh [collider] + // body_3/body_3/body_3_col/Mesh [collider, mesh collider] // ground [collider] (Static collider) EXPECT_PRIM_VALID(stage, "/test/ground"); @@ -753,13 +755,17 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsColliders) { pxr::UsdPhysicsRigidBodyAPI); EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_3/body_3/body_3_col", pxr::UsdPhysicsCollisionAPI); - // body_3/body_3/body_3_col/Mesh [collider] + // body_3/body_3/body_3_col/Mesh [collider, mesh collider] EXPECT_PRIM_VALID(stage, "/test/body_3/body_3/body_3_col/Mesh"); - EXPECT_PRIM_API_NOT_APPLIED( - stage, "/test/body_3/body_3/body_3_col/Mesh", - pxr::UsdPhysicsRigidBodyAPI); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_3/body_3/body_3_col/Mesh", + pxr::UsdPhysicsRigidBodyAPI); EXPECT_PRIM_API_APPLIED(stage, "/test/body_3/body_3/body_3_col/Mesh", pxr::UsdPhysicsCollisionAPI); + EXPECT_PRIM_API_APPLIED(stage, "/test/body_3/body_3/body_3_col/Mesh", + pxr::UsdPhysicsMeshCollisionAPI); + ExpectAttributeEqual( + stage, "/test/body_3/body_3/body_3_col/Mesh.physics:approximation", + pxr::UsdPhysicsTokens->convexHull); } } // namespace From 449de7343035b61f8624c4beb57c3206e9a70218 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Tue, 6 May 2025 07:53:51 -0700 Subject: [PATCH 095/191] Do not skip cow and touch_grid during WriteReadCompare. PiperOrigin-RevId: 755370618 Change-Id: I90dfe66b20c316b19c943eab8d031f5aef187fa7 --- src/xml/xml_native_writer.cc | 1 + test/xml/xml_native_writer_test.cc | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index 001ecf18..bf5093c5 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -1551,6 +1551,7 @@ void mjXWriter::Asset(XMLElement* root) { if (mesh->Plugin().active) { elem = InsertEnd(section, "mesh"); WriteAttrTxt(elem, "name", mesh->name); + WriteAttrTxt(elem, "file", mesh->File()); OnePlugin(InsertEnd(elem, "plugin"), &mesh->Plugin()); } else{ elem = InsertEnd(section, "mesh"); diff --git a/test/xml/xml_native_writer_test.cc b/test/xml/xml_native_writer_test.cc index 9bb31819..d3a2b59b 100644 --- a/test/xml/xml_native_writer_test.cc +++ b/test/xml/xml_native_writer_test.cc @@ -1383,10 +1383,8 @@ TEST_F(XMLWriterTest, WriteReadCompare) { // if file is meant to fail, skip it if (absl::StrContains(p.path().string(), "100_humanoids") || absl::StrContains(p.path().string(), "malformed_") || - absl::StrContains(p.path().string(), "touch_grid") || absl::StrContains(p.path().string(), "gmsh_") || absl::StrContains(p.path().string(), "shark_") || - absl::StrContains(p.path().string(), "cow") || absl::StrContains(p.path().string(), "frameless_contact_hfield") || absl::StrContains(p.path().string(), "spheremesh")) { continue; From c3e71a3fb1166eb081bd441cf111ecb4e352e48e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Budzianowski?= Date: Tue, 6 May 2025 16:46:46 -0700 Subject: [PATCH 096/191] Update simulation.rst --- doc/programming/simulation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/programming/simulation.rst b/doc/programming/simulation.rst index 3739f548..d0cf5029 100644 --- a/doc/programming/simulation.rst +++ b/doc/programming/simulation.rst @@ -913,7 +913,7 @@ elliptic, depending on which solver is selected in ``mjModel.opt``. The function can be used to determine which friction cone type is used. For pyramidal cones, the interpretation of the contact force (whose address we computed above) is non-trivial, because the components are forces along redundant non-orthogonal axes corresponding to the edges of the pyramid. The function :ref:`mj_contactForce` can be -used to convert the force generated by a given contact into a more intuitive format: a 3D force followed by a 3D toque. +used to convert the force generated by a given contact into a more intuitive format: a 3D force followed by a 3D torque. The torque component will be zero when :at:`condim` is 1 or 3, and non-zero otherwise. This force and torque are expressed in the contact frame given by mjContact.frame. Unlike all other matrices in mjData, this matrix is stored in transposed form. Normally a 3-by-3 matrix corresponding to a coordinate frame would have the frame axes along the From ecb769fc3a7dbe192753acb1328406661f03d559 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Wed, 7 May 2025 05:05:34 -0700 Subject: [PATCH 097/191] Refactor islands to be memory contiguous. PiperOrigin-RevId: 755803476 Change-Id: I41972b07e0d5ef5d0117c94f565b93367b87458b --- doc/changelog.rst | 10 +- doc/includes/references.h | 59 ++- include/mujoco/mjdata.h | 58 ++- include/mujoco/mjvisualize.h | 1 - include/mujoco/mjxmacro.h | 64 ++- python/mujoco/introspect/structs.py | 302 +++++++++++++- simulate/simulate.cc | 2 +- src/engine/engine_core_constraint.c | 192 ++------- src/engine/engine_core_constraint.h | 20 +- src/engine/engine_core_smooth.c | 69 +-- src/engine/engine_core_smooth.h | 5 +- src/engine/engine_forward.c | 33 +- src/engine/engine_io.c | 1 + src/engine/engine_island.c | 308 ++++++++++---- src/engine/engine_print.c | 46 +- src/engine/engine_solver.c | 415 +++++++++++++------ src/engine/engine_support.c | 72 +--- src/engine/engine_support.h | 8 +- src/engine/engine_util_misc.c | 18 + src/engine/engine_util_misc.h | 10 +- src/engine/engine_vis_state.c | 6 - src/engine/engine_vis_visualize.c | 12 +- test/engine/engine_core_constraint_test.cc | 294 +++---------- test/engine/engine_core_smooth_test.cc | 60 --- test/engine/engine_island_test.cc | 213 ++++++++-- test/engine/engine_solver_test.cc | 85 ---- test/engine/engine_support_test.cc | 71 ---- test/engine/testdata/island/2humanoid100.xml | 123 ++++++ test/engine/testdata/island/humanoid.xml | 252 +++++++++++ unity/Runtime/Bindings/MjBindings.cs | 49 ++- 30 files changed, 1742 insertions(+), 1116 deletions(-) create mode 100644 test/engine/testdata/island/2humanoid100.xml create mode 100644 test/engine/testdata/island/humanoid.xml diff --git a/doc/changelog.rst b/doc/changelog.rst index 77726e1f..3e40dc9a 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -2,9 +2,17 @@ Changelog ========= -Version 3.3.2 (April 28, 2025) +Upcoming version (not yet release) ---------------------------------- +General +^^^^^^^ +- Refactored island implementation so that island data is memory-contiguous. This speeds up island processing in the + solver and clears the way for the addition of the Newton and PGS solvers (currently only CG is supported). + +Version 3.3.2 (April 28, 2025) +------------------------------ + MJX ^^^ 1. Added inverse dynamics. diff --git a/doc/includes/references.h b/doc/includes/references.h index ce9bd670..6ebc759a 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -171,6 +171,7 @@ struct mjData_ { int nJ; // number of non-zeros in constraint Jacobian int nA; // number of non-zeros in constraint inverse inertia matrix int nisland; // number of detected constraint islands + int nidof; // number of dofs in all islands // global properties mjtNum time; // simulation time @@ -381,16 +382,51 @@ struct mjData_ { mjtNum* efc_R; // inverse constraint mass (nefc x 1) int* tendon_efcadr; // first efc address involving tendon; -1: none (ntendon x 1) - // computed by mj_island + // computed by mj_island (island dof structure) int* dof_island; // island id of this dof; -1: none (nv x 1) - int* island_dofnum; // number of dofs in island (nisland x 1) - int* island_dofadr; // start address in island_dofind (nisland x 1) - int* island_dofind; // island dof indices; -1: none (nv x 1) - int* dof_islandind; // dof island indices; -1: none (nv x 1) + int* island_nv; // number of dofs in this island (nisland x 1) + int* island_idofadr; // island start address in idof vector (nisland x 1) + int* island_dofadr; // island start address in dof vector (nisland x 1) + int* map_dof2idof; // map from dof to idof (nv x 1) + int* map_idof2dof; // map from idof to dof; idof >= ni: unconstrained (nv x 1) + + // computed by mj_island (dofs sorted by island) + mjtNum* ifrc_smooth; // net unconstrained force (nidof x 1) + mjtNum* iacc_smooth; // unconstrained acceleration (nidof x 1) + int* iM_rownnz; // inertia: non-zeros in each row (nidof x 1) + int* iM_rowadr; // inertia: address of each row in iM_colind (nidof x 1) + int* iM_diagnum; // inertia: num of consecutive diagonal elements (nidof x 1) + int* iM_colind; // inertia: column indices of non-zeros (nM x 1) + mjtNum* iM; // total inertia (sparse) (nM x 1) + mjtNum* iLD; // L'*D*L factorization of M (sparse) (nM x 1) + mjtNum* iLDiagInv; // 1/diag(D) (nidof x 1) + mjtNum* iacc; // acceleration (nidof x 1) + + // computed by mj_island (island constraint structure) int* efc_island; // island id of this constraint (nefc x 1) - int* island_efcnum; // number of constraints in island (nisland x 1) - int* island_efcadr; // start address in island_efcind (nisland x 1) - int* island_efcind; // island constraint indices (nefc x 1) + int* island_ne; // number of equality constraints in island (nisland x 1) + int* island_nf; // number of friction constraints in island (nisland x 1) + int* island_nefc; // number of constraints in island (nisland x 1) + int* island_iefcadr; // start address in iefc vector (nisland x 1) + int* map_efc2iefc; // map from efc to iefc (nefc x 1) + int* map_iefc2efc; // map from iefc to efc (nefc x 1) + + // computed by mj_island (constraints sorted by island) + int* iefc_type; // constraint type (mjtConstraint) (nefc x 1) + int* iefc_id; // id of object of specified type (nefc x 1) + int* iefc_J_rownnz; // number of non-zeros in constraint Jacobian row (nefc x 1) + int* iefc_J_rowadr; // row start address in colind array (nefc x 1) + int* iefc_J_rowsuper; // number of subsequent rows in supernode (nefc x 1) + int* iefc_J_colind; // column indices in constraint Jacobian (nJ x 1) + int* iefc_JT_rownnz; // number of non-zeros in constraint Jacobian row T (nidof x 1) + int* iefc_JT_rowadr; // row start address in colind array T (nidof x 1) + int* iefc_JT_rowsuper; // number of subsequent rows in supernode T (nidof x 1) + int* iefc_JT_colind; // column indices in constraint Jacobian T (nJ x 1) + mjtNum* iefc_J; // constraint Jacobian (nJ x 1) + mjtNum* iefc_JT; // constraint Jacobian transposed (nJ x 1) + mjtNum* iefc_frictionloss; // frictionloss (friction) (nefc x 1) + mjtNum* iefc_D; // constraint mass (nefc x 1) + mjtNum* iefc_R; // inverse constraint mass (nefc x 1) // computed by mj_projectConstraint (PGS solver) int* efc_AR_rownnz; // number of non-zeros in AR (nefc x 1) @@ -408,8 +444,12 @@ struct mjData_ { // computed by mj_fwdConstraint/mj_inverse mjtNum* efc_b; // linear cost term: J*qacc_smooth - aref (nefc x 1) - mjtNum* efc_force; // constraint force in constraint space (nefc x 1) + mjtNum* iefc_aref; // reference pseudo-acceleration (nefc x 1) + int* iefc_state; // constraint state (mjtConstraintState) (nefc x 1) + mjtNum* iefc_force; // constraint force in constraint space (nefc x 1) int* efc_state; // constraint state (mjtConstraintState) (nefc x 1) + mjtNum* efc_force; // constraint force in constraint space (nefc x 1) + mjtNum* ifrc_constraint; // constraint force (nidof x 1) // thread pool pointer uintptr_t threadpool; @@ -3174,7 +3214,6 @@ struct mjvSceneState_ { mjtNum* bvh_aabb_dyn; mjtByte* bvh_active; int* island_dofadr; - int* island_dofind; int* dof_island; int* efc_island; int* tendon_efcadr; diff --git a/include/mujoco/mjdata.h b/include/mujoco/mjdata.h index d6f98775..839b3aed 100644 --- a/include/mujoco/mjdata.h +++ b/include/mujoco/mjdata.h @@ -199,6 +199,7 @@ struct mjData_ { int nJ; // number of non-zeros in constraint Jacobian int nA; // number of non-zeros in constraint inverse inertia matrix int nisland; // number of detected constraint islands + int nidof; // number of dofs in all islands // global properties mjtNum time; // simulation time @@ -409,16 +410,51 @@ struct mjData_ { mjtNum* efc_R; // inverse constraint mass (nefc x 1) int* tendon_efcadr; // first efc address involving tendon; -1: none (ntendon x 1) - // computed by mj_island + // computed by mj_island (island dof structure) int* dof_island; // island id of this dof; -1: none (nv x 1) - int* island_dofnum; // number of dofs in island (nisland x 1) - int* island_dofadr; // start address in island_dofind (nisland x 1) - int* island_dofind; // island dof indices; -1: none (nv x 1) - int* dof_islandind; // dof island indices; -1: none (nv x 1) + int* island_nv; // number of dofs in this island (nisland x 1) + int* island_idofadr; // island start address in idof vector (nisland x 1) + int* island_dofadr; // island start address in dof vector (nisland x 1) + int* map_dof2idof; // map from dof to idof (nv x 1) + int* map_idof2dof; // map from idof to dof; idof >= ni: unconstrained (nv x 1) + + // computed by mj_island (dofs sorted by island) + mjtNum* ifrc_smooth; // net unconstrained force (nidof x 1) + mjtNum* iacc_smooth; // unconstrained acceleration (nidof x 1) + int* iM_rownnz; // inertia: non-zeros in each row (nidof x 1) + int* iM_rowadr; // inertia: address of each row in iM_colind (nidof x 1) + int* iM_diagnum; // inertia: num of consecutive diagonal elements (nidof x 1) + int* iM_colind; // inertia: column indices of non-zeros (nM x 1) + mjtNum* iM; // total inertia (sparse) (nM x 1) + mjtNum* iLD; // L'*D*L factorization of M (sparse) (nM x 1) + mjtNum* iLDiagInv; // 1/diag(D) (nidof x 1) + mjtNum* iacc; // acceleration (nidof x 1) + + // computed by mj_island (island constraint structure) int* efc_island; // island id of this constraint (nefc x 1) - int* island_efcnum; // number of constraints in island (nisland x 1) - int* island_efcadr; // start address in island_efcind (nisland x 1) - int* island_efcind; // island constraint indices (nefc x 1) + int* island_ne; // number of equality constraints in island (nisland x 1) + int* island_nf; // number of friction constraints in island (nisland x 1) + int* island_nefc; // number of constraints in island (nisland x 1) + int* island_iefcadr; // start address in iefc vector (nisland x 1) + int* map_efc2iefc; // map from efc to iefc (nefc x 1) + int* map_iefc2efc; // map from iefc to efc (nefc x 1) + + // computed by mj_island (constraints sorted by island) + int* iefc_type; // constraint type (mjtConstraint) (nefc x 1) + int* iefc_id; // id of object of specified type (nefc x 1) + int* iefc_J_rownnz; // number of non-zeros in constraint Jacobian row (nefc x 1) + int* iefc_J_rowadr; // row start address in colind array (nefc x 1) + int* iefc_J_rowsuper; // number of subsequent rows in supernode (nefc x 1) + int* iefc_J_colind; // column indices in constraint Jacobian (nJ x 1) + int* iefc_JT_rownnz; // number of non-zeros in constraint Jacobian row T (nidof x 1) + int* iefc_JT_rowadr; // row start address in colind array T (nidof x 1) + int* iefc_JT_rowsuper; // number of subsequent rows in supernode T (nidof x 1) + int* iefc_JT_colind; // column indices in constraint Jacobian T (nJ x 1) + mjtNum* iefc_J; // constraint Jacobian (nJ x 1) + mjtNum* iefc_JT; // constraint Jacobian transposed (nJ x 1) + mjtNum* iefc_frictionloss; // frictionloss (friction) (nefc x 1) + mjtNum* iefc_D; // constraint mass (nefc x 1) + mjtNum* iefc_R; // inverse constraint mass (nefc x 1) // computed by mj_projectConstraint (PGS solver) int* efc_AR_rownnz; // number of non-zeros in AR (nefc x 1) @@ -436,8 +472,12 @@ struct mjData_ { // computed by mj_fwdConstraint/mj_inverse mjtNum* efc_b; // linear cost term: J*qacc_smooth - aref (nefc x 1) - mjtNum* efc_force; // constraint force in constraint space (nefc x 1) + mjtNum* iefc_aref; // reference pseudo-acceleration (nefc x 1) + int* iefc_state; // constraint state (mjtConstraintState) (nefc x 1) + mjtNum* iefc_force; // constraint force in constraint space (nefc x 1) int* efc_state; // constraint state (mjtConstraintState) (nefc x 1) + mjtNum* efc_force; // constraint force in constraint space (nefc x 1) + mjtNum* ifrc_constraint; // constraint force (nidof x 1) // thread pool pointer uintptr_t threadpool; diff --git a/include/mujoco/mjvisualize.h b/include/mujoco/mjvisualize.h index 0a757cf1..fa0aec4f 100644 --- a/include/mujoco/mjvisualize.h +++ b/include/mujoco/mjvisualize.h @@ -677,7 +677,6 @@ struct mjvSceneState_ { mjtNum* bvh_aabb_dyn; mjtByte* bvh_active; int* island_dofadr; - int* island_dofind; int* dof_island; int* efc_island; int* tendon_efcadr; diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index 7c73c598..88e6c0f7 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -739,23 +739,56 @@ X( int, efc_state, MJ_D(nefc), 1 ) // array fields of mjData that are used in the dual problem -#define MJDATA_ARENA_POINTERS_DUAL \ - X( int, efc_AR_rownnz, MJ_D(nefc), 1 ) \ - X( int, efc_AR_rowadr, MJ_D(nefc), 1 ) \ - X( int, efc_AR_colind, MJ_D(nA), 1 ) \ - X( mjtNum, efc_AR, MJ_D(nA), 1 ) +#define MJDATA_ARENA_POINTERS_DUAL \ + X( int, efc_AR_rownnz, MJ_D(nefc), 1 ) \ + X( int, efc_AR_rowadr, MJ_D(nefc), 1 ) \ + X( int, efc_AR_colind, MJ_D(nA), 1 ) \ + X( mjtNum, efc_AR, MJ_D(nA), 1 ) // array fields of mjData that are used for constraint islands -#define MJDATA_ARENA_POINTERS_ISLAND \ - X( int, dof_island, MJ_M(nv), 1 ) \ - X( int, island_dofnum, MJ_D(nisland), 1 ) \ - X( int, island_dofadr, MJ_D(nisland), 1 ) \ - X( int, island_dofind, MJ_M(nv), 1 ) \ - X( int, dof_islandind, MJ_M(nv), 1 ) \ - X( int, efc_island, MJ_D(nefc), 1 ) \ - X( int, island_efcnum, MJ_D(nisland), 1 ) \ - X( int, island_efcadr, MJ_D(nisland), 1 ) \ - X( int, island_efcind, MJ_D(nefc), 1 ) +#define MJDATA_ARENA_POINTERS_ISLAND \ + X( int, dof_island, MJ_M(nv), 1 ) \ + X( int, island_nv, MJ_D(nisland), 1 ) \ + X( int, island_idofadr, MJ_D(nisland), 1 ) \ + X( int, island_dofadr, MJ_D(nisland), 1 ) \ + X( int, map_dof2idof, MJ_M(nv), 1 ) \ + X( int, map_idof2dof, MJ_M(nv), 1 ) \ + X( mjtNum, ifrc_smooth, MJ_D(nidof), 1 ) \ + X( mjtNum, iacc_smooth, MJ_D(nidof), 1 ) \ + X( int, iM_rownnz, MJ_D(nidof), 1 ) \ + X( int, iM_rowadr, MJ_D(nidof), 1 ) \ + X( int, iM_diagnum, MJ_D(nidof), 1 ) \ + X( int, iM_colind, MJ_M(nM), 1 ) \ + X( mjtNum, iM, MJ_M(nM), 1 ) \ + X( mjtNum, iLD, MJ_M(nM), 1 ) \ + X( mjtNum, iLDiagInv, MJ_D(nidof), 1 ) \ + X( mjtNum, iacc, MJ_D(nidof), 1 ) \ + X( int, efc_island, MJ_D(nefc), 1 ) \ + X( int, island_ne, MJ_D(nisland), 1 ) \ + X( int, island_nf, MJ_D(nisland), 1 ) \ + X( int, island_nefc, MJ_D(nisland), 1 ) \ + X( int, island_iefcadr, MJ_D(nisland), 1 ) \ + X( int, map_efc2iefc, MJ_D(nefc), 1 ) \ + X( int, map_iefc2efc, MJ_D(nefc), 1 ) \ + X( int, iefc_type, MJ_D(nefc), 1 ) \ + X( int, iefc_id, MJ_D(nefc), 1 ) \ + X( int, iefc_J_rownnz, MJ_D(nefc), 1 ) \ + X( int, iefc_J_rowadr, MJ_D(nefc), 1 ) \ + X( int, iefc_J_rowsuper, MJ_D(nefc), 1 ) \ + X( int, iefc_J_colind, MJ_D(nJ), 1 ) \ + X( int, iefc_JT_rownnz, MJ_D(nidof), 1 ) \ + X( int, iefc_JT_rowadr, MJ_D(nidof), 1 ) \ + X( int, iefc_JT_rowsuper, MJ_D(nidof), 1 ) \ + X( int, iefc_JT_colind, MJ_D(nJ), 1 ) \ + X( mjtNum, iefc_J, MJ_D(nJ), 1 ) \ + X( mjtNum, iefc_JT, MJ_D(nJ), 1 ) \ + X( mjtNum, iefc_frictionloss, MJ_D(nefc), 1 ) \ + X( mjtNum, iefc_D, MJ_D(nefc), 1 ) \ + X( mjtNum, iefc_R, MJ_D(nefc), 1 ) \ + X( mjtNum, iefc_aref, MJ_D(nefc), 1 ) \ + X( int, iefc_state, MJ_D(nefc), 1 ) \ + X( mjtNum, iefc_force, MJ_D(nefc), 1 ) \ + X( mjtNum, ifrc_constraint, MJ_D(nidof), 1 ) // array fields of mjData that live in d->arena #define MJDATA_ARENA_POINTERS \ @@ -785,6 +818,7 @@ X( int, nJ ) \ X( int, nA ) \ X( int, nisland ) \ + X( int, nidof ) \ X( mjtNum, time ) \ X( uintptr_t, threadpool ) diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index db237c74..c7c3f4d1 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -4896,6 +4896,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='int'), doc='number of detected constraint islands', ), + StructFieldDecl( + name='nidof', + type=ValueType(name='int'), + doc='number of dofs in all islands', + ), StructFieldDecl( name='time', type=ValueType(name='mjtNum'), @@ -5940,11 +5945,19 @@ STRUCTS: Mapping[str, StructDecl] = dict([ array_extent=('nv',), ), StructFieldDecl( - name='island_dofnum', + name='island_nv', type=PointerType( inner_type=ValueType(name='int'), ), - doc='number of dofs in island', + doc='number of dofs in this island', + array_extent=('nisland',), + ), + StructFieldDecl( + name='island_idofadr', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='island start address in idof vector', array_extent=('nisland',), ), StructFieldDecl( @@ -5952,25 +5965,105 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=PointerType( inner_type=ValueType(name='int'), ), - doc='start address in island_dofind', + doc='island start address in dof vector', array_extent=('nisland',), ), StructFieldDecl( - name='island_dofind', + name='map_dof2idof', type=PointerType( inner_type=ValueType(name='int'), ), - doc='island dof indices; -1: none', + doc='map from dof to idof', array_extent=('nv',), ), StructFieldDecl( - name='dof_islandind', + name='map_idof2dof', type=PointerType( inner_type=ValueType(name='int'), ), - doc='dof island indices; -1: none', + doc='map from idof to dof; idof >= ni: unconstrained', array_extent=('nv',), ), + StructFieldDecl( + name='ifrc_smooth', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='net unconstrained force', + array_extent=('nidof',), + ), + StructFieldDecl( + name='iacc_smooth', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='unconstrained acceleration', + array_extent=('nidof',), + ), + StructFieldDecl( + name='iM_rownnz', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='inertia: non-zeros in each row', + array_extent=('nidof',), + ), + StructFieldDecl( + name='iM_rowadr', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='inertia: address of each row in iM_colind', + array_extent=('nidof',), + ), + StructFieldDecl( + name='iM_diagnum', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='inertia: num of consecutive diagonal elements', + array_extent=('nidof',), + ), + StructFieldDecl( + name='iM_colind', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='inertia: column indices of non-zeros', + array_extent=('nM',), + ), + StructFieldDecl( + name='iM', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='total inertia (sparse)', + array_extent=('nM',), + ), + StructFieldDecl( + name='iLD', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc="L'*D*L factorization of M (sparse)", + array_extent=('nM',), + ), + StructFieldDecl( + name='iLDiagInv', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='1/diag(D)', + array_extent=('nidof',), + ), + StructFieldDecl( + name='iacc', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='acceleration', + array_extent=('nidof',), + ), StructFieldDecl( name='efc_island', type=PointerType( @@ -5980,7 +6073,23 @@ STRUCTS: Mapping[str, StructDecl] = dict([ array_extent=('nefc',), ), StructFieldDecl( - name='island_efcnum', + name='island_ne', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='number of equality constraints in island', + array_extent=('nisland',), + ), + StructFieldDecl( + name='island_nf', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='number of friction constraints in island', + array_extent=('nisland',), + ), + StructFieldDecl( + name='island_nefc', type=PointerType( inner_type=ValueType(name='int'), ), @@ -5988,19 +6097,147 @@ STRUCTS: Mapping[str, StructDecl] = dict([ array_extent=('nisland',), ), StructFieldDecl( - name='island_efcadr', + name='island_iefcadr', type=PointerType( inner_type=ValueType(name='int'), ), - doc='start address in island_efcind', + doc='start address in iefc vector', array_extent=('nisland',), ), StructFieldDecl( - name='island_efcind', + name='map_efc2iefc', type=PointerType( inner_type=ValueType(name='int'), ), - doc='island constraint indices', + doc='map from efc to iefc', + array_extent=('nefc',), + ), + StructFieldDecl( + name='map_iefc2efc', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='map from iefc to efc', + array_extent=('nefc',), + ), + StructFieldDecl( + name='iefc_type', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='constraint type (mjtConstraint)', + array_extent=('nefc',), + ), + StructFieldDecl( + name='iefc_id', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='id of object of specified type', + array_extent=('nefc',), + ), + StructFieldDecl( + name='iefc_J_rownnz', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='number of non-zeros in constraint Jacobian row', + array_extent=('nefc',), + ), + StructFieldDecl( + name='iefc_J_rowadr', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='row start address in colind array', + array_extent=('nefc',), + ), + StructFieldDecl( + name='iefc_J_rowsuper', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='number of subsequent rows in supernode', + array_extent=('nefc',), + ), + StructFieldDecl( + name='iefc_J_colind', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='column indices in constraint Jacobian', + array_extent=('nJ',), + ), + StructFieldDecl( + name='iefc_JT_rownnz', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='number of non-zeros in constraint Jacobian row T', + array_extent=('nidof',), + ), + StructFieldDecl( + name='iefc_JT_rowadr', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='row start address in colind array T', + array_extent=('nidof',), + ), + StructFieldDecl( + name='iefc_JT_rowsuper', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='number of subsequent rows in supernode T', + array_extent=('nidof',), + ), + StructFieldDecl( + name='iefc_JT_colind', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='column indices in constraint Jacobian T', + array_extent=('nJ',), + ), + StructFieldDecl( + name='iefc_J', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='constraint Jacobian', + array_extent=('nJ',), + ), + StructFieldDecl( + name='iefc_JT', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='constraint Jacobian transposed', + array_extent=('nJ',), + ), + StructFieldDecl( + name='iefc_frictionloss', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='frictionloss (friction)', + array_extent=('nefc',), + ), + StructFieldDecl( + name='iefc_D', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='constraint mass', + array_extent=('nefc',), + ), + StructFieldDecl( + name='iefc_R', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='inverse constraint mass', array_extent=('nefc',), ), StructFieldDecl( @@ -6060,7 +6297,23 @@ STRUCTS: Mapping[str, StructDecl] = dict([ array_extent=('nefc',), ), StructFieldDecl( - name='efc_force', + name='iefc_aref', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='reference pseudo-acceleration', + array_extent=('nefc',), + ), + StructFieldDecl( + name='iefc_state', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='constraint state (mjtConstraintState)', + array_extent=('nefc',), + ), + StructFieldDecl( + name='iefc_force', type=PointerType( inner_type=ValueType(name='mjtNum'), ), @@ -6075,6 +6328,22 @@ STRUCTS: Mapping[str, StructDecl] = dict([ doc='constraint state (mjtConstraintState)', array_extent=('nefc',), ), + StructFieldDecl( + name='efc_force', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='constraint force in constraint space', + array_extent=('nefc',), + ), + StructFieldDecl( + name='ifrc_constraint', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='constraint force', + array_extent=('nidof',), + ), StructFieldDecl( name='threadpool', type=ValueType(name='uintptr_t'), @@ -8642,13 +8911,6 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), doc='', ), - StructFieldDecl( - name='island_dofind', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), StructFieldDecl( name='dof_island', type=PointerType( diff --git a/simulate/simulate.cc b/simulate/simulate.cc index 78a5e312..bd255ce3 100644 --- a/simulate/simulate.cc +++ b/simulate/simulate.cc @@ -328,7 +328,7 @@ void UpdateProfiler(mj::Simulate* sim, const mjModel* m, const mjData* d) { sim->figconstraint.linedata[start + 4][2*i] = i; // y - int nefc = nisland == 1 ? d->nefc : d->island_efcnum[k]; + int nefc = nisland == 1 ? d->nefc : d->island_nefc[k]; sim->figconstraint.linedata[start + 0][2*i+1] = nefc; const mjSolverStat* stat = d->solver + k*mjNSOLVER + i; sim->figconstraint.linedata[start + 1][2*i+1] = stat->nactive; diff --git a/src/engine/engine_core_constraint.c b/src/engine/engine_core_constraint.c index 47918d48..391ed4d2 100644 --- a/src/engine/engine_core_constraint.c +++ b/src/engine/engine_core_constraint.c @@ -378,50 +378,6 @@ void mj_mulJacVec(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* -// multiply Jacobian by vector, for one island -// flg_resunc and flg_vecunc denote whether res/vec are uncompressed -void mj_mulJacVec_island(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec, - int island, int flg_resunc, int flg_vecunc) { - // no island, call regular function - if (island < 0) { - mj_mulJacVec(m, d, res, vec); - return; - } - - // sizes - int vecnnz = d->island_dofnum[island]; - int resnnz = d->island_efcnum[island]; - - // indices - int* vecind = d->island_dofind + d->island_dofadr[island]; - int* resind = d->island_efcind + d->island_efcadr[island]; - - // sparse Jacobian - if (mj_isSparse(m)) { - for (int i=0; i < resnnz; i++) { - int row = resind[i]; - int Jnnz = d->efc_J_rownnz[row]; - int Jrowadr = d->efc_J_rowadr[row]; - int* Jind = d->efc_J_colind + Jrowadr; - mjtNum* J = d->efc_J + Jrowadr; - int j = flg_resunc ? row : i; - res[j] = mju_dotSparse2(J, vec, Jnnz, Jind, vecnnz, vecind, flg_vecunc); - } - } - - // dense Jacobian - else { - int nv = m->nv; - for (int i=0; i < resnnz; i++) { - int row = resind[i]; - int j = flg_resunc ? row : i; - res[j] = mju_dotSparse(vec, d->efc_J + nv*row, vecnnz, vecind, flg_vecunc); - } - } -} - - - // multiply JacobianT by vector void mj_mulJacTVec(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec) { // exit if no constraints @@ -443,50 +399,6 @@ void mj_mulJacTVec(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* -// multiply Jacobian transpose by vector, for one island -// flg_resunc and flg_vecunc denote whether res/vec are uncompressed -void mj_mulJacTVec_island(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec, - int island, int flg_resunc, int flg_vecunc) { - // no island, call regular function - if (island < 0) { - mj_mulJacTVec(m, d, res, vec); - return; - } - - // sizes - int vecnnz = d->island_efcnum[island]; - int resnnz = d->island_dofnum[island]; - - // indices - int* vecind = d->island_efcind + d->island_efcadr[island]; - int* resind = d->island_dofind + d->island_dofadr[island]; - - // sparse Jacobian - if (mj_isSparse(m)) { - for (int i=0; i < resnnz; i++) { - int row = resind[i]; - int JTnnz = d->efc_JT_rownnz[row]; - int JTrowadr = d->efc_JT_rowadr[row]; - int* JTind = d->efc_JT_colind + JTrowadr; - mjtNum* JT = d->efc_JT + JTrowadr; - int j = flg_resunc ? row : i; - res[j] = mju_dotSparse2(JT, vec, JTnnz, JTind, vecnnz, vecind, flg_vecunc); - } - } - - // dense Jacobian - else { - int nefc = d->nefc; - for (int i=0; i < resnnz; i++) { - int row = resind[i]; - int j = flg_resunc ? row : i; - res[j] = mju_dotSparse(vec, d->efc_JT + nefc*row, vecnnz, vecind, flg_vecunc); - } - } -} - - - //--------------------- instantiate constraints by type -------------------------------------------- // equality constraints @@ -2102,10 +2014,6 @@ void mj_makeConstraint(const mjModel* m, mjData* d) { // supernodes of JT mju_superSparse(m->nv, d->efc_JT_rowsuper, d->efc_JT_rownnz, d->efc_JT_rowadr, d->efc_JT_colind); - } else { - if (mjENABLED(mjENBL_ISLAND)) { - mju_transpose(d->efc_JT, d->efc_J, d->nefc, m->nv); - } } // compute diagApprox @@ -2377,25 +2285,17 @@ void mj_referenceConstraint(const mjModel* m, mjData* d) { //---------------------------- update constraint state --------------------------------------------- -// compute efc_state, efc_force, qfrc_constraint, optionally restricted to one island -// island < 0: update all d->nefc constraints -// island >= 0: update only d->island_efcnum[island] constraints -// jar = Jac*qacc-aref is restricted to the island, in the above sense +// compute efc_state, efc_force // optional: cost(qacc) = shat(jar); cone Hessians -void mj_constraintUpdate_island(const mjModel* m, mjData* d, const mjtNum* jar, - mjtNum cost[1], int flg_coneHessian, int island) { - int ne = d->ne, nf = d->nf; - const mjtNum *D = d->efc_D, *R = d->efc_R, *floss = d->efc_frictionloss; - mjtNum* force = d->efc_force; +void mj_constraintUpdate_impl(int ne, int nf, int nefc, + const mjtNum* D, const mjtNum* R, const mjtNum* floss, + const mjtNum* jar, const int* type, const int* id, + mjContact* contact, int* state, mjtNum* force, mjtNum cost[1], + int flg_coneHessian) { mjtNum s = 0; - int nefc = island < 0 ? d->nefc : d->island_efcnum[island]; - int* efcind = island < 0 ? NULL : d->island_efcind + d->island_efcadr[island]; - - // no constraints: clear qfrc_constraint and cost, return + // no constraints: clear cost, return if (!nefc) { - // can only occur for island == -1 - mju_zero(d->qfrc_constraint, m->nv); if (cost) { *cost = 0; } @@ -2403,55 +2303,49 @@ void mj_constraintUpdate_island(const mjModel* m, mjData* d, const mjtNum* jar, } // compute unconstrained efc_force - for (int c=0; c < nefc; c++) { - int i = efcind ? efcind[c] : c; - force[i] = -D[i]*jar[c]; + for (int i=0; i < nefc; i++) { + force[i] = -D[i]*jar[i]; } // update constraints - for (int c=0; c < nefc; c++) { - int i = efcind ? efcind[c] : c; - + for (int i=0; i < nefc; i++) { // ==== equality if (i < ne) { if (cost) { - s += 0.5*D[i]*jar[c]*jar[c]; + s += 0.5*D[i]*jar[i]*jar[i]; } - d->efc_state[i] = mjCNSTRSTATE_QUADRATIC; + state[i] = mjCNSTRSTATE_QUADRATIC; continue; } // ==== friction if (i < ne + nf) { // linear negative - if (jar[c] <= -R[i]*floss[i]) { + if (jar[i] <= -R[i]*floss[i]) { if (cost) { - s += -0.5*R[i]*floss[i]*floss[i] - floss[i]*jar[c]; + s += -0.5*R[i]*floss[i]*floss[i] - floss[i]*jar[i]; } force[i] = floss[i]; - - d->efc_state[i] = mjCNSTRSTATE_LINEARNEG; + state[i] = mjCNSTRSTATE_LINEARNEG; } // linear positive - else if (jar[c] >= R[i]*floss[i]) { + else if (jar[i] >= R[i]*floss[i]) { if (cost) { - s += -0.5*R[i]*floss[i]*floss[i] + floss[i]*jar[c]; + s += -0.5*R[i]*floss[i]*floss[i] + floss[i]*jar[i]; } force[i] = -floss[i]; - - d->efc_state[i] = mjCNSTRSTATE_LINEARPOS; + state[i] = mjCNSTRSTATE_LINEARPOS; } // quadratic else { if (cost) { - s += 0.5*D[i]*jar[c]*jar[c]; + s += 0.5*D[i]*jar[i]*jar[i]; } - - d->efc_state[i] = mjCNSTRSTATE_QUADRATIC; + state[i] = mjCNSTRSTATE_QUADRATIC; } continue; } @@ -2459,36 +2353,35 @@ void mj_constraintUpdate_island(const mjModel* m, mjData* d, const mjtNum* jar, // ==== contact // non-negative constraint - if (d->efc_type[i] != mjCNSTR_CONTACT_ELLIPTIC) { + if (type[i] != mjCNSTR_CONTACT_ELLIPTIC) { // constraint is satisfied: no cost - if (jar[c] >= 0) { + if (jar[i] >= 0) { force[i] = 0; - d->efc_state[i] = mjCNSTRSTATE_SATISFIED; + state[i] = mjCNSTRSTATE_SATISFIED; } // quadratic else { if (cost) { - s += 0.5*D[i]*jar[c]*jar[c]; + s += 0.5*D[i]*jar[i]*jar[i]; } - - d->efc_state[i] = mjCNSTRSTATE_QUADRATIC; + state[i] = mjCNSTRSTATE_QUADRATIC; } } // contact with elliptic cone else { // get contact - mjContact* con = d->contact + d->efc_id[i]; + mjContact* con = contact + id[i]; mjtNum mu = con->mu, *friction = con->friction; int dim = con->dim; // map to regular dual cone space mjtNum U[6]; - U[0] = jar[c]*mu; + U[0] = jar[i]*mu; for (int j=1; j < dim; j++) { - U[j] = jar[c+j]*friction[j-1]; + U[j] = jar[i+j]*friction[j-1]; } // decompose into normal and tangent @@ -2498,19 +2391,17 @@ void mj_constraintUpdate_island(const mjModel* m, mjData* d, const mjtNum* jar, // top zone if (N >= mu*T || (T <= 0 && N >= 0)) { mju_zero(force+i, dim); - - d->efc_state[i] = mjCNSTRSTATE_SATISFIED; + state[i] = mjCNSTRSTATE_SATISFIED; } // bottom zone else if (mu*N+T <= 0 || (T <= 0 && N < 0)) { if (cost) { for (int j=0; j < dim; j++) { - s += 0.5*D[i+j]*jar[c+j]*jar[c+j]; + s += 0.5*D[i+j]*jar[i+j]*jar[i+j]; } } - - d->efc_state[i] = mjCNSTRSTATE_QUADRATIC; + state[i] = mjCNSTRSTATE_QUADRATIC; } // middle zone @@ -2530,12 +2421,12 @@ void mj_constraintUpdate_island(const mjModel* m, mjData* d, const mjtNum* jar, } // set state - d->efc_state[i] = mjCNSTRSTATE_CONE; + state[i] = mjCNSTRSTATE_CONE; // cone Hessian if (flg_coneHessian) { // get Hessian pointer - mjtNum* H = d->contact[d->efc_id[i]].H; + mjtNum* H = contact[id[i]].H; // set first row: (1, -mu/T * U) mjtNum scl = -mu/T; @@ -2546,10 +2437,11 @@ void mj_constraintUpdate_island(const mjModel* m, mjData* d, const mjtNum* jar, // set upper block: mu*N/T^3 * U*U' scl = mu*N/(T*T*T); - for (int k=1; k < dim; k++) + for (int k=1; k < dim; k++) { for (int j=k; j < dim; j++) { H[k*dim+j] = scl*U[j]*U[k]; } + } // add to diagonal: (mu^2 - mu*N/T) * I scl = mu*mu - mu*N/T; @@ -2576,19 +2468,14 @@ void mj_constraintUpdate_island(const mjModel* m, mjData* d, const mjtNum* jar, // replicate state in all cone dimensions for (int j=1; j < dim; j++) { - d->efc_state[i+j] = d->efc_state[i]; + state[i+j] = state[i]; } // advance to end of contact - c += (dim-1); + i += (dim-1); } } - // compute qfrc_constraint - int flg_vecunc = 1; - int flg_resunc = 1; - mj_mulJacTVec_island(m, d, d->qfrc_constraint, d->efc_force, island, flg_vecunc, flg_resunc); - // assign cost if (cost) { *cost = s; @@ -2601,5 +2488,8 @@ void mj_constraintUpdate_island(const mjModel* m, mjData* d, const mjtNum* jar, // optional: cost(qacc) = shat(jar) where jar = Jac*qacc-aref; cone Hessians void mj_constraintUpdate(const mjModel* m, mjData* d, const mjtNum* jar, mjtNum cost[1], int flg_coneHessian) { - mj_constraintUpdate_island(m, d, jar, cost, flg_coneHessian, -1); + mj_constraintUpdate_impl(d->ne, d->nf, d->nefc, d->efc_D, d->efc_R, d->efc_frictionloss, + jar, d->efc_type, d->efc_id, d->contact, d->efc_state, d->efc_force, + cost, flg_coneHessian); + mj_mulJacTVec(m, d, d->qfrc_constraint, d->efc_force); } diff --git a/src/engine/engine_core_constraint.h b/src/engine/engine_core_constraint.h index 05c5fd57..f752b143 100644 --- a/src/engine/engine_core_constraint.h +++ b/src/engine/engine_core_constraint.h @@ -24,6 +24,7 @@ extern "C" { #endif + //-------------------------- Jacobian-related ------------------------------------------------------ // determine type of friction cone @@ -38,16 +39,9 @@ MJAPI int mj_isDual(const mjModel* m); // multiply Jacobian by vector MJAPI void mj_mulJacVec(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec); -// multiply Jacobian by vector, for one island -MJAPI void mj_mulJacVec_island(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec, - int island, int flg_resunc, int flg_vecunc); - // multiply JacobianT by vector MJAPI void mj_mulJacTVec(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec); -// multiply JacobianT by vector, for one island -MJAPI void mj_mulJacTVec_island(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec, - int island, int flg_resunc, int flg_vecunc); //-------------------------- utility functions ----------------------------------------------------- @@ -90,6 +84,7 @@ void mj_diagApprox(const mjModel* m, mjData* d); // compute efc_R, efc_D, efc_KDIP, adjust diagApprox void mj_makeImpedance(const mjModel* m, mjData* d); + //---------------------------- top-level API for constraint construction --------------------------- // main driver: call all functions above @@ -101,14 +96,19 @@ MJAPI void mj_projectConstraint(const mjModel* m, mjData* d); // compute efc_vel, efc_aref MJAPI void mj_referenceConstraint(const mjModel* m, mjData* d); +// compute efc_state, efc_force +// optional: cost(qacc) = shat(jar); cone Hessians +MJAPI void mj_constraintUpdate_impl(int ne, int nf, int nefc, + const mjtNum* D, const mjtNum* R, const mjtNum* floss, + const mjtNum* jar, const int* type, const int* id, + mjContact* contact, int* state, mjtNum* force, mjtNum cost[1], + int flg_coneHessian); + // compute efc_state, efc_force, qfrc_constraint // optional: cost(qacc) = shat(jar) where jar = Jac*qacc-aref; cone Hessians MJAPI void mj_constraintUpdate(const mjModel* m, mjData* d, const mjtNum* jar, mjtNum cost[1], int flg_coneHessian); -// compute efc_state, efc_force, qfrc_constraint for one island -MJAPI void mj_constraintUpdate_island(const mjModel* m, mjData* d, const mjtNum* jar, - mjtNum cost[1], int flg_coneHessian, int island); #ifdef __cplusplus } diff --git a/src/engine/engine_core_smooth.c b/src/engine/engine_core_smooth.c index 09ea01c2..efd74614 100644 --- a/src/engine/engine_core_smooth.c +++ b/src/engine/engine_core_smooth.c @@ -1803,7 +1803,7 @@ void mj_solveLD_legacy(const mjModel* m, mjtNum* restrict x, int n, // in-place sparse backsubstitution: x = inv(L'*D*L)*x -void mj_solveLD(mjtNum* restrict x, const mjtNum* qLDs, const mjtNum* qLDiagInv, int nv, int n, +void mj_solveLD(mjtNum* restrict x, const mjtNum* qLD, const mjtNum* qLDiagInv, int nv, int n, const int* rownnz, const int* rowadr, const int* diagnum, const int* colind) { // x <- L^-T x for (int i=nv-1; i > 0; i--) { @@ -1819,7 +1819,7 @@ void mj_solveLD(mjtNum* restrict x, const mjtNum* qLDs, const mjtNum* qLDiagInv, int start = rowadr[i]; int end = start + rownnz[i] - 1; for (int adr=start; adr < end; adr++) { - x[colind[adr]] -= qLDs[adr] * x_i; + x[colind[adr]] -= qLD[adr] * x_i; } } } @@ -1832,7 +1832,7 @@ void mj_solveLD(mjtNum* restrict x, const mjtNum* qLDs, const mjtNum* qLDiagInv, mjtNum x_i; if ((x_i = x[i+offset])) { for (int adr=start; adr < end; adr++) { - x[offset + colind[adr]] -= qLDs[adr] * x_i; + x[offset + colind[adr]] -= qLD[adr] * x_i; } } } @@ -1870,13 +1870,13 @@ void mj_solveLD(mjtNum* restrict x, const mjtNum* qLDs, const mjtNum* qLDiagInv, // one vector if (n == 1) { - x[i] -= mju_dotSparse(qLDs+adr, x, d, colind+adr, /*flg_unc1=*/0); + x[i] -= mju_dotSparse(qLD+adr, x, d, colind+adr, /*flg_unc1=*/0); } // multiple vectors else { for (int offset=0; offset < n*nv; offset+=nv) { - x[i+offset] -= mju_dotSparse(qLDs+adr, x+offset, d, colind+adr, /*flg_unc1=*/0); + x[i+offset] -= mju_dotSparse(qLD+adr, x+offset, d, colind+adr, /*flg_unc1=*/0); } } } @@ -1896,65 +1896,6 @@ void mj_solveM(const mjModel* m, mjData* d, mjtNum* x, const mjtNum* y, int n) { } -// in-place sparse backsubstitution for one island: x = inv(L'*D*L)*x -// L is in lower triangle of qLD; D is on diagonal of qLD -void mj_solveM_island(const mjModel* m, const mjData* d, mjtNum* restrict x, int island) { - // if no islands, call mj_solveLD - const mjtNum* qLD = d->qLD; - const mjtNum* qLDiagInv = d->qLDiagInv; - if (island < 0) { - mj_solveLD(x, qLD, qLDiagInv, m->nv, 1, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); - return; - } - - // local copies of key variables - const int* rownnz = d->M_rownnz; - const int* rowadr = d->M_rowadr; - const int* colind = d->M_colind; - const int* diagnum = m->dof_simplenum; - - // local constants: island specific - int ndof = d->island_dofnum[island]; - const int* dofind = d->island_dofind + d->island_dofadr[island]; - const int* islandind = d->dof_islandind; - - // x <- inv(L') * x; skip simple, exploit sparsity of input vector - for (int k=ndof-1; k >= 0; k--) { - int i = dofind[k]; - mjtNum x_k; - if (!diagnum[i] && (x_k = x[k])) { - int start = rowadr[i]; - int end = start + rownnz[i] - 1; - for (int adr=end-1; adr >= start; adr--) { - x[islandind[colind[adr]]] -= qLD[adr] * x_k; - } - } - } - - // x <- inv(D) * x - for (int k=ndof-1; k >= 0; k--) { - x[k] *= qLDiagInv[dofind[k]]; // x(i) /= L(i,i) - } - - // x <- inv(L) * x; skip simple - for (int k=0; k < ndof; k++) { - int i = dofind[k]; - - // skip diagonal rows - if (diagnum[i]) { - continue; - } - - int start = rowadr[i]; - int end = start + rownnz[i] - 1; - for (int adr=end-1; adr >= start; adr--) { - x[k] -= x[islandind[colind[adr]]] * qLD[adr]; - } - } -} - - // half of sparse backsubstitution: x = sqrt(inv(D))*inv(L')*y void mj_solveM2(const mjModel* m, mjData* d, mjtNum* x, const mjtNum* y, diff --git a/src/engine/engine_core_smooth.h b/src/engine/engine_core_smooth.h index 4bc124b0..38decdca 100644 --- a/src/engine/engine_core_smooth.h +++ b/src/engine/engine_core_smooth.h @@ -71,15 +71,12 @@ MJAPI void mj_solveLD_legacy(const mjModel* m, mjtNum* x, int n, // in-place sparse backsubstitution: x = inv(L'*D*L)*x // handle n vectors at once -MJAPI void mj_solveLD(mjtNum* x, const mjtNum* qLDs, const mjtNum* qLDiagInv, int nv, int n, +MJAPI void mj_solveLD(mjtNum* x, const mjtNum* qLD, const mjtNum* qLDiagInv, int nv, int n, const int* rownnz, const int* rowadr, const int* diagnum, const int* colind); // sparse backsubstitution: x = inv(L'*D*L)*y, use factorization in d MJAPI void mj_solveM(const mjModel* m, mjData* d, mjtNum* x, const mjtNum* y, int n); -// sparse backsubstitution for one island: x = inv(L'*D*L)*x, use factorization in d -MJAPI void mj_solveM_island(const mjModel* m, const mjData* d, mjtNum* x, int island); - // half of sparse backsubstitution: x = sqrt(inv(D))*inv(L')*y MJAPI void mj_solveM2(const mjModel* m, mjData* d, mjtNum* x, const mjtNum* y, const mjtNum* sqrtInvD, int n); diff --git a/src/engine/engine_forward.c b/src/engine/engine_forward.c index a720f156..b409e1ea 100644 --- a/src/engine/engine_forward.c +++ b/src/engine/engine_forward.c @@ -631,10 +631,10 @@ static void warmstart(const mjModel* m, mjData* d) { // have island structure: unconstrained qacc = qacc_smooth if (d->nisland > 0) { - for (int i=0; i < nv; i++) { - if (d->dof_island[i] < 0) { - d->qacc[i] = d->qacc_smooth[i]; - } + // loop over unconstrained dofs in map_idof2dof[nidof, nv) + for (int i=d->nidof; i < nv; i++) { + int dof = d->map_idof2dof[i]; + d->qacc[dof] = d->qacc_smooth[dof]; } } @@ -723,22 +723,37 @@ void mj_fwdConstraint(const mjModel* m, mjData* d) { // check if islands are supported int islands_supported = mjENABLED(mjENBL_ISLAND) && - d->nisland > 0 && + nisland > 0 && m->opt.solver == mjSOL_CG && m->opt.noslip_iterations == 0; // run solver over constraint islands if (islands_supported) { - // no threadpool, loop over islands + int nidof = d->nidof; + + // copy CG inputs to islands (vel+acc deps, pos-dependent already copied in mj_island) + mju_gather(d->ifrc_smooth, d->qfrc_smooth, d->map_idof2dof, nidof); + mju_gather(d->ifrc_constraint, d->qfrc_constraint, d->map_idof2dof, nidof); + mju_gather(d->iacc_smooth, d->qacc_smooth, d->map_idof2dof, nidof); + mju_gather(d->iacc, d->qacc, d->map_idof2dof, nidof); + mju_gather(d->iefc_force, d->efc_force, d->map_iefc2efc, nefc); + mju_gather(d->iefc_aref, d->efc_aref, d->map_iefc2efc, nefc); + + // solve per island if (!d->threadpool) { + // no threadpool, loop over islands for (int island=0; island < nisland; island++) { mj_solCG_island(m, d, island, m->opt.iterations); } - } - else { - // solve using threads + } else { + // have threadpool, solve using threads mj_solCG_island_multithreaded(m, d); } + + // copy back solver outputs (scatter dofs since ni <= nv) + mju_scatter(d->qacc, d->iacc, d->map_idof2dof, nidof); + mju_scatter(d->qfrc_constraint, d->ifrc_constraint, d->map_idof2dof, nidof); + mju_gather(d->efc_force, d->iefc_force, d->map_efc2iefc, nefc); } // run solver over all constraints diff --git a/src/engine/engine_io.c b/src/engine/engine_io.c index 78aed97d..31084c46 100644 --- a/src/engine/engine_io.c +++ b/src/engine/engine_io.c @@ -1917,6 +1917,7 @@ static void _resetData(const mjModel* m, mjData* d, unsigned char debug_value) { d->nJ = 0; d->nA = 0; d->nisland = 0; + d->nidof = 0; // clear global properties d->time = 0; diff --git a/src/engine/engine_island.c b/src/engine/engine_island.c index 9a8be763..108844d8 100644 --- a/src/engine/engine_island.c +++ b/src/engine/engine_island.c @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -26,12 +27,65 @@ #include "engine/engine_support.h" #include "engine/engine_util_errmem.h" #include "engine/engine_util_misc.h" +#include "engine/engine_util_sparse.h" #ifdef MEMORY_SANITIZER #include #endif +//-------------------------- local utilities ------------------------------------------------------- + +// clear island-related arena pointers in mjData +static void clearIsland(mjData* d, size_t parena) { +#define X(type, name, nr, nc) d->name = NULL; + MJDATA_ARENA_POINTERS_ISLAND +#undef X + d->nefc = 0; + d->nisland = 0; + d->nidof = 0; + d->parena = parena; + + // poison remaining memory +#ifdef ADDRESS_SANITIZER + ASAN_POISON_MEMORY_REGION( + (char*)d->arena + d->parena, d->narena - d->pstack - d->parena); +#endif +} + + + +// allocate island arrays on arena, return 1 on success, 0 on failure +static int arenaAllocIsland(const mjModel* m, mjData* d) { +#undef MJ_M +#define MJ_M(n) m->n +#undef MJ_D +#define MJ_D(n) d->n + + size_t parena_old = d->parena; + +#define X(type, name, nr, nc) \ + d->name = mj_arenaAllocByte(d, sizeof(type) * (nr) * (nc), _Alignof(type)); \ + if (!d->name) { \ + mj_warning(d, mjWARN_CNSTRFULL, d->narena); \ + clearIsland(d, parena_old); \ + return 0; \ + } + + MJDATA_ARENA_POINTERS_ISLAND + +#undef X + +#undef MJ_M +#define MJ_M(n) n +#undef MJ_D +#define MJ_D(n) n + return 1; +} + + + +//-------------------------- flood-fill and graph construction ------------------------------------ // find disjoint subgraphs ("islands") given sparse symmetric adjacency matrix // arguments: @@ -87,54 +141,6 @@ int mj_floodFill(int* island, int nr, const int* rownnz, const int* rowadr, cons -// clear island-related arena pointers in mjData -static void clearIsland(mjData* d, size_t parena) { -#define X(type, name, nr, nc) d->name = NULL; - MJDATA_ARENA_POINTERS_ISLAND -#undef X - d->nefc = 0; - d->nisland = 0; - d->parena = parena; - - // poison remaining memory -#ifdef ADDRESS_SANITIZER - ASAN_POISON_MEMORY_REGION( - (char*)d->arena + d->parena, d->narena - d->pstack - d->parena); -#endif -} - - - -// allocate island arrays on arena, return 1 on success, 0 on failure -static int arenaAllocIsland(const mjModel* m, mjData* d) { -#undef MJ_M -#define MJ_M(n) m->n -#undef MJ_D -#define MJ_D(n) d->n - - size_t parena_old = d->parena; - -#define X(type, name, nr, nc) \ - d->name = mj_arenaAllocByte(d, sizeof(type) * (nr) * (nc), _Alignof(type)); \ - if (!d->name) { \ - mj_warning(d, mjWARN_CNSTRFULL, d->narena); \ - clearIsland(d, parena_old); \ - return 0; \ - } - - MJDATA_ARENA_POINTERS_ISLAND - -#undef X - -#undef MJ_M -#define MJ_M(n) n -#undef MJ_D -#define MJ_D(n) n - return 1; -} - - - // return upper bound on number of tree-tree edges static int countMaxEdge(const mjModel* m, const mjData* d) { int nedge_max = 0; @@ -411,14 +417,17 @@ static int findEdges(const mjModel* m, const mjData* d, int* treenedge, int* edg +//-------------------------- main entry-point ----------------------------------------------------- + // discover islands: -// nisland, island_dofadr, dof_island, dof_islandnext, island_efcadr, efc_island, efc_islandnext +// nisland, island_idofadr, dof_island, dof_islandnext, island_efcadr, efc_island, efc_islandnext void mj_island(const mjModel* m, mjData* d) { int nv = m->nv, nefc = d->nefc, ntree=m->ntree; // no constraints: quick return if (!nefc || m->nflex) { // TODO: add flex support to island discovery d->nisland = 0; + d->nidof = 0; return; } @@ -454,86 +463,201 @@ void mj_island(const mjModel* m, mjData* d) { int* stack = mjSTACKALLOC(d, nedge, int); d->nisland = mj_floodFill(tree_island, ntree, rownnz, rowadr, colind, stack); + // no islands found: quick return + if (!d->nisland) { + d->nidof = 0; + mj_freeStack(d); + return; + } + + // count ni: total number of dofs in islands + int nidof = 0; + for (int i=0; i < nv; i++) { + nidof += (tree_island[m->dof_treeid[i]] >= 0); + } + d->nidof = nidof; + // allocate island arrays on arena if (!arenaAllocIsland(m, d)) { mj_freeStack(d); return; } - int nisland = d->nisland; // local copy + // local copy + int nisland = d->nisland; - // compute dof_island, island_dofnum - int num_dof_unc = 0; // number of unconstrained dofs - mju_zeroInt(d->island_dofnum, nisland); + + // ------------------------------------- degrees of freedom -------------------------------------- + + // compute dof_island, island_nv + mju_zeroInt(d->island_nv, nisland); for (int i=0; i < nv; i++) { - // dof_island - int island = tree_island[m->dof_treeid[i]]; + // assign dofs to islands + int island = tree_island[m->dof_treeid[i]]; // -1 if unconstrained d->dof_island[i] = island; - // island_dofnum + // increment island_nv if (island >= 0) { - d->island_dofnum[island]++; - } else { - num_dof_unc++; + d->island_nv[island]++; } } - // compute island_dofadr - if (nisland) d->island_dofadr[0] = 0; + // compute island_idofadr (cumsum of island_nv) + d->island_idofadr[0] = 0; for (int i=1; i < nisland; i++) { - d->island_dofadr[i] = d->island_dofadr[i-1] + d->island_dofnum[i-1]; + d->island_idofadr[i] = d->island_idofadr[i-1] + d->island_nv[i-1]; } - // reset island_dofnum - mju_zeroInt(d->island_dofnum, nisland); - - // compute dof_islandind, island_dofind - int num_dof_island = 0; - for (int i=0; i < nv; i++) { - int island = d->dof_island[i]; + // compute dof <-> idof maps + int* island_nv2 = mjSTACKALLOC(d, nisland + 1, int); // last element counts unconstrained dofs + mju_zeroInt(island_nv2, nisland + 1); + for (int dof=0; dof < nv; dof++) { + int island = d->dof_island[dof]; + int idof; if (island >= 0) { - d->island_dofind[d->island_dofadr[island] + d->island_dofnum[island]] = i; - d->dof_islandind[i] = d->island_dofnum[island]++; - num_dof_island++; + // constrained dof + idof = d->island_idofadr[island] + island_nv2[island]++; } else { - d->dof_islandind[i] = -1; + // unconstrained dof + idof = nidof + island_nv2[nisland]++; } + + d->map_dof2idof[dof] = idof; + d->map_idof2dof[idof] = dof; // only the first ni elements of map_idof2dof are in some island } - // sanity check, SHOULD NOT OCCUR - if (num_dof_island + num_dof_unc != nv) { - mjERROR("not all islands assigned to dofs"); + // SHOULD NOT OCCUR + if (!mju_compare(island_nv2, d->island_nv, nisland)) mjERROR("island_nv miscount"); + if (nidof + island_nv2[nisland] != nv) mjERROR("miscount of unconstrained dofs"); + + // compute island_dofadr (used for visualization) + for (int i=0; i < nisland; i++) { + d->island_dofadr[i] = d->map_idof2dof[d->island_idofadr[i]]; } - // finalize dof_islandind: set remaining indices to -1 - for (int i=num_dof_island; i < nv; i++) { - d->island_dofind[i] = -1; + // local CSR copy of qM + mjtNum* qM = mjSTACKALLOC(d, m->nM, mjtNum); + mju_gather(qM, d->qM, d->mapM2M, m->nM); + + // inertia: block-diagonalize both iLD <- qLD and iM <- qM + mju_blockDiagSparse(d->iLD, d->iM_rownnz, d->iM_rowadr, d->iM_colind, + d->qLD, d->M_rownnz, d->M_rowadr, d->M_colind, + nidof, nisland, + d->map_idof2dof, d->map_dof2idof, + d->island_idofadr, d->island_idofadr, + d->iM, qM); + mju_gather(d->iLDiagInv, d->qLDiagInv, d->map_idof2dof, nidof); + + // compute iM_diagnum (dof_simplenum per island) + int count = 0; + int dof_next = d->map_idof2dof[nidof-1]; + for (int i=nidof-1; i >= 0; i--) { + // check if island boundary was crossed + int dof = d->map_idof2dof[i]; + int island_boundary = (d->dof_island[dof] != d->dof_island[dof_next]); + dof_next = dof; + + // accumulate and set simple dof (diagonal row) counter + if (m->dof_simplenum[dof] && !island_boundary) { + count++; // increment counter + } else { + count = 0; // reset + } + d->iM_diagnum[i] = count; } - // compute efc_island, island_efcnum - mju_zeroInt(d->island_efcnum, nisland); + + + // ------------------------------------- constraints --------------------------------------------- + + // compute efc_island, island_{ne,nf,nefc} + mju_zeroInt(d->island_ne, nisland); + mju_zeroInt(d->island_nf, nisland); + mju_zeroInt(d->island_nefc, nisland); for (int i=0; i < nefc; i++) { int tree[2]; treeFirst(m, d, tree, i); int island = tree_island[tree[0]]; d->efc_island[i] = island; - d->island_efcnum[island]++; + d->island_nefc[island]++; + switch (d->efc_type[i]) { + case mjCNSTR_EQUALITY: + d->island_ne[island]++; + break; + case mjCNSTR_FRICTION_DOF: + case mjCNSTR_FRICTION_TENDON: + d->island_nf[island]++; + break; + default: + break; + } } - // compute island_efcadr - if (nisland) d->island_efcadr[0] = 0; + // compute island_iefcadr (cumsum of island_nefc) + d->island_iefcadr[0] = 0; for (int i=1; i < nisland; i++) { - d->island_efcadr[i] = d->island_efcadr[i-1] + d->island_efcnum[i-1]; + d->island_iefcadr[i] = d->island_iefcadr[i-1] + d->island_nefc[i-1]; } - // reset island_efcnum - mju_zeroInt(d->island_efcnum, nisland); - - // compute efc_islandind - for (int i=0; i < nefc; i++) { - int island = d->efc_island[i]; - d->island_efcind[d->island_efcadr[island] + (d->island_efcnum[island]++)] = i; + // compute efc <-> iefc maps + int* island_nefc2 = island_nv2; // reuse island_nv2 + mju_zeroInt(island_nefc2, nisland); + for (int c=0; c < nefc; c++) { + int island = d->efc_island[c]; + int ic = d->island_iefcadr[island] + island_nefc2[island]++; + d->map_efc2iefc[c] = ic; + d->map_iefc2efc[ic] = c; } + // SHOULD NOT OCCUR + if (!mju_compare(island_nefc2, d->island_nefc, nisland)) mjERROR("island_nefc miscount"); + + // dense: block-diagonalize Jacobian + if (!mj_isSparse(m)) { + mju_blockDiag(d->iefc_J, d->efc_J, + nv, nidof, nisland, + d->map_iefc2efc, d->map_idof2dof, + d->island_nefc, d->island_nv, + d->island_iefcadr, d->island_idofadr); + } + + // sparse + else { + // block-diagonalize Jacobian + mju_blockDiagSparse(d->iefc_J, d->iefc_J_rownnz, d->iefc_J_rowadr, d->iefc_J_colind, + d->efc_J, d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind, + nefc, nisland, + d->map_iefc2efc, d->map_dof2idof, + d->island_iefcadr, d->island_idofadr, NULL, NULL); + + // recompute rowsuper per island + for (int island=0; island < nisland; island++) { + int adr = d->island_iefcadr[island]; + mju_superSparse(d->island_nefc[island], d->iefc_J_rowsuper + adr, + d->iefc_J_rownnz + adr, d->iefc_J_rowadr + adr, d->iefc_J_colind); + } + + // block-diagonalize Jacobian-transpose + mju_blockDiagSparse(d->iefc_JT, d->iefc_JT_rownnz, d->iefc_JT_rowadr, d->iefc_JT_colind, + d->efc_JT, d->efc_JT_rownnz, d->efc_JT_rowadr, d->efc_JT_colind, + nidof, nisland, + d->map_idof2dof, d->map_efc2iefc, + d->island_idofadr, d->island_iefcadr, NULL, NULL); + + // recompute rowsuper per island + for (int island=0; island < nisland; island++) { + int adr = d->island_idofadr[island]; + mju_superSparse(d->island_nv[island], d->iefc_JT_rowsuper + adr, + d->iefc_JT_rownnz + adr, d->iefc_JT_rowadr + adr, d->iefc_JT_colind); + } + } + + // copy position-dependent efc vectors required by solver + mju_gatherInt(d->iefc_type, d->efc_type, d->map_iefc2efc, nefc); + mju_gatherInt(d->iefc_id, d->efc_id, d->map_iefc2efc, nefc); + mju_gather(d->iefc_frictionloss, d->efc_frictionloss, d->map_iefc2efc, nefc); + mju_gather(d->iefc_D, d->efc_D, d->map_iefc2efc, nefc); + mju_gather(d->iefc_R, d->efc_R, d->map_iefc2efc, nefc); + mj_freeStack(d); } diff --git a/src/engine/engine_print.c b/src/engine/engine_print.c index 49d0afa3..61a9ef02 100644 --- a/src/engine/engine_print.c +++ b/src/engine/engine_print.c @@ -1392,27 +1392,30 @@ void mj_printFormattedData(const mjModel* m, const mjData* d, const char* filena } fprintf(fp, "\n\n"); - fprintf(fp, NAME_FORMAT, "ISLAND_DOFNUM"); + fprintf(fp, NAME_FORMAT, "ISLAND_NV"); for (int i = 0; i < d->nisland; i++) { - fprintf(fp, " %d", d->island_dofnum[i]); + fprintf(fp, " %d", d->island_nv[i]); } fprintf(fp, "\n\n"); - fprintf(fp, NAME_FORMAT, "ISLAND_DOFADR"); + fprintf(fp, NAME_FORMAT, "ISLAND_IDOFADR"); for (int i = 0; i < d->nisland; i++) { - fprintf(fp, " %d", d->island_dofadr[i]); + fprintf(fp, " %d", d->island_idofadr[i]); } fprintf(fp, "\n\n"); - fprintf(fp, NAME_FORMAT, "ISLAND_DOFIND"); + fprintf(fp, NAME_FORMAT, "MAP_IDOF2DOF"); for (int i = 0; i < m->nv; i++) { - fprintf(fp, " %d", d->island_dofind[i]); - } - fprintf(fp, "\n\n"); + int dof = d->map_idof2dof[i]; + if (i > 0) { + int dofprev = d->map_idof2dof[i-1]; - fprintf(fp, NAME_FORMAT, "DOF_ISLANDIND"); - for (int i = 0; i < m->nv; i++) { - fprintf(fp, " %d", d->dof_islandind[i]); + // print '|' at island boundaries + if (d->dof_island[dof] != d->dof_island[dofprev]) { + fprintf(fp, " |"); + } + } + fprintf(fp, " %d", dof); } fprintf(fp, "\n\n"); @@ -1422,21 +1425,30 @@ void mj_printFormattedData(const mjModel* m, const mjData* d, const char* filena } fprintf(fp, "\n\n"); - fprintf(fp, NAME_FORMAT, "ISLAND_EFCNUM"); + fprintf(fp, NAME_FORMAT, "ISLAND_NEFC"); for (int i = 0; i < d->nisland; i++) { - fprintf(fp, " %d", d->island_efcnum[i]); + fprintf(fp, " %d", d->island_nefc[i]); } fprintf(fp, "\n\n"); - fprintf(fp, NAME_FORMAT, "ISLAND_EFCADR"); + fprintf(fp, NAME_FORMAT, "ISLAND_IEFCADR"); for (int i = 0; i < d->nisland; i++) { - fprintf(fp, " %d", d->island_efcadr[i]); + fprintf(fp, " %d", d->island_iefcadr[i]); } fprintf(fp, "\n\n"); - fprintf(fp, NAME_FORMAT, "ISLAND_EFCIND"); + fprintf(fp, NAME_FORMAT, "MAP_IEFC2EFC"); for (int i = 0; i < d->nefc; i++) { - fprintf(fp, " %d", d->island_efcind[i]); + int efc = d->map_iefc2efc[i]; + if (i > 0) { + int efcprev = d->map_iefc2efc[i-1]; + + // print '|' at island boundaries + if (d->efc_island[efc] != d->efc_island[efcprev]) { + fprintf(fp, " |"); + } + } + fprintf(fp, " %d", efc); } fprintf(fp, "\n\n"); } diff --git a/src/engine/engine_solver.c b/src/engine/engine_solver.c index 8e83ace5..fb9dedd1 100644 --- a/src/engine/engine_solver.c +++ b/src/engine/engine_solver.c @@ -766,13 +766,55 @@ void mj_solNoSlip(const mjModel* m, mjData* d, int maxiter) { // CG context struct _mjCGContext { int flg_Newton; // 1: Newton, 0: CG - - // island-related int island; // current island index, -1 if monolithic + + // sizes int nv; // number of dofs - int nefc; // number of constraints - int* dofind; // dof indices of this island, NULL if monolithic - int* efcind; // constraint indices of this island, NULL if monolithic + int ne; // number of equalities + int nf; // number of friction constraints + int nefc; // number of all constraints + + // contact array + mjContact* contact; + + // dof arrays + const mjtNum* qfrc_smooth; + const mjtNum* qacc_smooth; + mjtNum* qfrc_constraint; + mjtNum* qacc; + + // inertia + const int* M_rownnz; + const int* M_rowadr; + const int* M_diagnum; + const int* M_colind; + const int* dof_Madr; + const int* dof_parentid; + const mjtNum* qM; + const mjtNum* qLD; + const mjtNum* qLDiagInv; + + // efc arrays + const mjtNum* efc_D; + const mjtNum* efc_R; + const mjtNum* efc_frictionloss; + const mjtNum* efc_aref; + const int* efc_id; + const int* efc_type; + mjtNum* efc_force; + int* efc_state; + + // Jacobians + const int* J_rownnz; + const int* J_rowadr; + const int* J_rowsuper; + const int* J_colind; + const int* JT_rownnz; + const int* JT_rowadr; + const int* JT_rowsuper; + const int* JT_colind; + const mjtNum* J; + const mjtNum* JT; // common arrays (CGallocate) mjtNum* Jaref; // Jac*qacc - aref (nefc x 1) @@ -793,7 +835,7 @@ struct _mjCGContext { int* L_rownnz; // Hessian factor row nonzeros (nv x 1) int* L_rowadr; // Hessian factor row addresses (nv x 1) - // Newton arrays, computed-size (HessianMake) + // Newton arrays, computed-size (MakeHessian) int nH; // number of nonzeros in Hessian H int* H_colind; // Hessian column indices (nH x 1) mjtNum* H; // Hessian (nH x 1) @@ -818,23 +860,130 @@ struct _mjCGContext { typedef struct _mjCGContext mjCGContext; + +// set sizes and pointers to mjData arrays in mjCGContext +static void CGpointers(const mjModel* m, const mjData* d, mjCGContext* ctx, int island) { + int is_sparse = mj_isSparse(m); + ctx->contact = d->contact; + ctx->island = island; + + // set sizes and pointers (monolithic) + if (island < 0) { + // sizes + ctx->nv = m->nv; + ctx->ne = d->ne; + ctx->nf = d->nf; + ctx->nefc = d->nefc; + + // dof arrays + ctx->qfrc_smooth = d->qfrc_smooth; + ctx->qfrc_constraint = d->qfrc_constraint; + ctx->qacc_smooth = d->qacc_smooth; + ctx->qacc = d->qacc; + + // inertia + ctx->M_rownnz = d->M_rownnz; + ctx->M_rowadr = d->M_rowadr; + ctx->M_diagnum = m->dof_simplenum; + ctx->M_colind = d->M_colind; + ctx->dof_Madr = m->dof_Madr; + ctx->dof_parentid = m->dof_parentid; + ctx->qM = d->qM; + ctx->qLD = d->qLD; + ctx->qLDiagInv = d->qLDiagInv; + + // efc arrays + ctx->efc_D = d->efc_D; + ctx->efc_R = d->efc_R; + ctx->efc_frictionloss = d->efc_frictionloss; + ctx->efc_aref = d->efc_aref; + ctx->efc_id = d->efc_id; + ctx->efc_type = d->efc_type; + ctx->efc_force = d->efc_force; + ctx->efc_state = d->efc_state; + + // Jacobians + ctx->J = d->efc_J; + if (is_sparse) { + ctx->J_rownnz = d->efc_J_rownnz; + ctx->J_rowadr = d->efc_J_rowadr; + ctx->J_rowsuper = d->efc_J_rowsuper; + ctx->J_colind = d->efc_J_colind; + ctx->JT_rownnz = d->efc_JT_rownnz; + ctx->JT_rowadr = d->efc_JT_rowadr; + ctx->JT_rowsuper = d->efc_JT_rowsuper; + ctx->JT_colind = d->efc_JT_colind; + ctx->JT = d->efc_JT; + } + } + + // set sizes and pointers (per-island) + else { + // sizes + ctx->nv = d->island_nv[island]; + ctx->ne = d->island_ne[island]; + ctx->nf = d->island_nf[island]; + ctx->nefc = d->island_nefc[island]; + + // dof arrays + int idofadr = d->island_idofadr[island]; + ctx->qfrc_smooth = d->ifrc_smooth + idofadr; + ctx->qfrc_constraint = d->ifrc_constraint + idofadr; + ctx->qacc_smooth = d->iacc_smooth + idofadr; + ctx->qacc = d->iacc + idofadr; + + // inertia + ctx->M_rownnz = d->iM_rownnz + idofadr; + ctx->M_rowadr = d->iM_rowadr + idofadr; + ctx->M_diagnum = d->iM_diagnum + idofadr; + ctx->M_colind = d->iM_colind; + ctx->qM = d->iM; + ctx->qLD = d->iLD; + ctx->qLDiagInv = d->iLDiagInv + idofadr; + + // efc arrays + int iefcadr = d->island_iefcadr[island]; + ctx->efc_D = d->iefc_D + iefcadr; + ctx->efc_R = d->iefc_R + iefcadr; + ctx->efc_frictionloss = d->iefc_frictionloss + iefcadr; + ctx->efc_aref = d->iefc_aref + iefcadr; + ctx->efc_id = d->iefc_id + iefcadr; + ctx->efc_type = d->iefc_type + iefcadr; + ctx->efc_force = d->iefc_force + iefcadr; + ctx->efc_state = d->iefc_state + iefcadr; + + // Jacobians + if (!is_sparse) { + ctx->J = d->iefc_J + d->nidof * iefcadr; + } else { + ctx->J_rownnz = d->iefc_J_rownnz + iefcadr; + ctx->J_rowadr = d->iefc_J_rowadr + iefcadr; + ctx->J_rowsuper = d->iefc_J_rowsuper + iefcadr; + ctx->J_colind = d->iefc_J_colind; + ctx->JT_rownnz = d->iefc_JT_rownnz + idofadr; + ctx->JT_rowadr = d->iefc_JT_rowadr + idofadr; + ctx->JT_rowsuper = d->iefc_JT_rowsuper + idofadr; + ctx->JT_colind = d->iefc_JT_colind; + ctx->J = d->iefc_J; + ctx->JT = d->iefc_JT; + } + } +} + + + // allocate fixed-size arrays in mjCGContext // mj_{mark/free}Stack in calling function! -static void CGallocate(const mjModel* m, mjData* d, mjCGContext* ctx, - int island, int flg_Newton) { +static void CGallocate(const mjModel* m, mjData* d, mjCGContext* ctx, int island, int flg_Newton) { // clear everything memset(ctx, 0, sizeof(mjCGContext)); - // get sizes - int nv = island < 0 ? m->nv : d->island_dofnum[island]; - int nefc = island < 0 ? d->nefc : d->island_efcnum[island]; + // set sizes and pointers + CGpointers(m, d, ctx, island); - // island-related - ctx->island = island; - ctx->nv = nv; - ctx->nefc = nefc; - ctx->dofind = island < 0 ? NULL : d->island_dofind + d->island_dofadr[island]; - ctx->efcind = island < 0 ? NULL : d->island_efcind + d->island_efcadr[island]; + // local sizes + int nv = ctx->nv; + int nefc = ctx->nefc; // common arrays ctx->Jaref = mjSTACKALLOC(d, nefc, mjtNum); @@ -849,7 +998,7 @@ static void CGallocate(const mjModel* m, mjData* d, mjCGContext* ctx, // Newton only, known-size arrays ctx->flg_Newton = flg_Newton; if (flg_Newton) { - ctx->D = mjSTACKALLOC(d, nefc, mjtNum); + ctx->D = mjSTACKALLOC(d, nefc, mjtNum); // sparse Newton only if (mj_isSparse(m)) { @@ -866,28 +1015,35 @@ static void CGallocate(const mjModel* m, mjData* d, mjCGContext* ctx, // update efc_force, qfrc_constraint, cost-related -static void CGupdateConstraint(const mjModel* m, mjData* d, mjCGContext* ctx) { +static void CGupdateConstraint(mjCGContext* ctx) { int nefc = ctx->nefc, nv = ctx->nv; - const int* dofind = ctx->dofind; - const int* efcind = ctx->efcind; // update constraints - mj_constraintUpdate_island(m, d, ctx->Jaref, &(ctx->cost), ctx->flg_Newton, ctx->island); + mj_constraintUpdate_impl(ctx->ne, ctx->nf, ctx->nefc, ctx->efc_D, ctx->efc_R, + ctx->efc_frictionloss, ctx->Jaref, ctx->efc_type, ctx->efc_id, + ctx->contact, ctx->efc_state, ctx->efc_force, + &(ctx->cost), ctx->flg_Newton); + + // compute qfrc_constraint (dense or sparse) + if (!ctx->JT) { + mju_mulMatTVec(ctx->qfrc_constraint, ctx->J, ctx->efc_force, nefc, nv); + } else { + mju_mulMatVecSparse(ctx->qfrc_constraint, ctx->JT, ctx->efc_force, nv, + ctx->JT_rownnz, ctx->JT_rowadr, ctx->JT_colind, ctx->JT_rowsuper); + } // count active and cone ctx->nactive = 0; ctx->ncone = 0; - for (int c=0; c < nefc; c++) { - int i = efcind ? efcind[c] : c; - ctx->nactive += (d->efc_state[i] != mjCNSTRSTATE_SATISFIED); - ctx->ncone += (d->efc_state[i] == mjCNSTRSTATE_CONE); + for (int i=0; i < nefc; i++) { + ctx->nactive += (ctx->efc_state[i] != mjCNSTRSTATE_SATISFIED); + ctx->ncone += (ctx->efc_state[i] == mjCNSTRSTATE_CONE); } // add Gauss cost, set in quadratic[0] mjtNum Gauss = 0; - for (int c=0; c < nv; c++) { - int i = dofind ? dofind[c] : c; - Gauss += 0.5 * (ctx->Ma[c] - d->qfrc_smooth[i]) * (d->qacc[i] - d->qacc_smooth[i]); + for (int i=0; i < nv; i++) { + Gauss += 0.5 * (ctx->Ma[i] - ctx->qfrc_smooth[i]) * (ctx->qacc[i] - ctx->qacc_smooth[i]); } ctx->quadGauss[0] = Gauss; @@ -895,22 +1051,20 @@ static void CGupdateConstraint(const mjModel* m, mjData* d, mjCGContext* ctx) { } -// TODO(tassa): Restore mjData const-ness. + // update grad, Mgrad -static void CGupdateGradient(const mjModel* m, mjData* d, mjCGContext* ctx) { +static void CGupdateGradient(mjCGContext* ctx) { int nv = ctx->nv; - const int* dofind = ctx->dofind; // grad = M*qacc - qfrc_smooth - qfrc_constraint - for (int c=0; c < nv; c++) { - int i = dofind ? dofind[c] : c; - ctx->grad[c] = ctx->Ma[c] - d->qfrc_smooth[i] - d->qfrc_constraint[i]; + for (int i=0; i < nv; i++) { + ctx->grad[i] = ctx->Ma[i] - ctx->qfrc_smooth[i] - ctx->qfrc_constraint[i]; } // Newton: Mgrad = H \ grad // TODO: b/295296178 - add island support to Newton solver if (ctx->flg_Newton) { - if (mj_isSparse(m)) { + if (ctx->L_rowadr) { mju_cholSolveSparse(ctx->Mgrad, (ctx->ncone ? ctx->Lcone : ctx->L), ctx->grad, nv, ctx->L_rownnz, ctx->L_rowadr, ctx->L_colind); } else { @@ -921,44 +1075,32 @@ static void CGupdateGradient(const mjModel* m, mjData* d, mjCGContext* ctx) { // CG: Mgrad = M \ grad else { mju_copy(ctx->Mgrad, ctx->grad, nv); - mj_solveM_island(m, d, ctx->Mgrad, ctx->island); + mj_solveLD(ctx->Mgrad, ctx->qLD, ctx->qLDiagInv, nv, 1, + ctx->M_rownnz, ctx->M_rowadr, ctx->M_diagnum, ctx->M_colind); } } // prepare quadratic polynomials and contact cone quantities -static void CGprepare(const mjModel* m, const mjData* d, mjCGContext* ctx) { - int nv = ctx->nv, nefc = ctx->nefc, island = ctx->island; - const int* dofind = ctx->dofind; - const int* efcind = ctx->efcind; +static void CGprepare(mjCGContext* ctx) { + int nv = ctx->nv, nefc = ctx->nefc; const mjtNum* v = ctx->search; // Gauss: alpha^2*0.5*v'*M*v + alpha*v'*(Ma-qfrc_smooth) + 0.5*(a-qacc_smooth)'*(Ma-qfrc_smooth) // quadGauss[0] already computed in CGupdateConstraint - mjtNum v_dot_smooth; - if (island < 0) { - v_dot_smooth = mju_dot(d->qfrc_smooth, v, nv); - } else { - v_dot_smooth = 0; - for (int c=0; c < nv; c++) { - v_dot_smooth += d->qfrc_smooth[dofind[c]] * v[c]; - } - } - ctx->quadGauss[1] = mju_dot(v, ctx->Ma, nv) - v_dot_smooth; + ctx->quadGauss[1] = mju_dot(v, ctx->Ma, nv) - mju_dot(ctx->qfrc_smooth, v, nv); ctx->quadGauss[2] = 0.5*mju_dot(v, ctx->Mv, nv); // process constraints - for (int c=0; c < nefc; c++) { - int i = efcind ? efcind[c] : c; - + for (int i=0; i < nefc; i++) { // pointers to numeric data - const mjtNum* Jv = ctx->Jv + c; - const mjtNum* Jaref = ctx->Jaref + c; - const mjtNum* D = d->efc_D + i; + const mjtNum* Jv = ctx->Jv + i; + const mjtNum* Jaref = ctx->Jaref + i; + const mjtNum* D = ctx->efc_D + i; // pointer to this quadratic - mjtNum* quad = ctx->quad + 3*c; + mjtNum* quad = ctx->quad + 3*i; // init with scalar quadratic mjtNum DJ0 = D[0]*Jaref[0]; @@ -967,12 +1109,12 @@ static void CGprepare(const mjModel* m, const mjData* d, mjCGContext* ctx) { quad[2] = Jv[0]*D[0]*Jv[0]; // elliptic cone: extra processing - if (d->efc_type[i] == mjCNSTR_CONTACT_ELLIPTIC) { + if (ctx->efc_type[i] == mjCNSTR_CONTACT_ELLIPTIC) { // extract contact info - mjContact* con = d->contact + d->efc_id[i]; + const mjContact* con = ctx->contact + ctx->efc_id[i]; int dim = con->dim; mjtNum U[6], V[6], UU = 0, UV = 0, VV = 0, mu = con->mu; - mjtNum* friction = con->friction; + const mjtNum* friction = con->friction; // complete vector quadratic (for bottom zone) for (int j=1; j < dim; j++) { @@ -1006,7 +1148,7 @@ static void CGprepare(const mjModel* m, const mjData* d, mjCGContext* ctx) { quad[8] = D[0] / ((mu*mu) * (1 + (mu*mu))); // advance to next constraint - c += (dim-1); + i += (dim-1); } // apply scaling @@ -1028,9 +1170,8 @@ typedef struct _mjCGPnt mjCGPnt; // evaluate linesearch cost, return first and second derivatives -static void CGeval(const mjModel* m, const mjData* d, mjCGContext* ctx, mjCGPnt* p) { - int ne = d->ne, nf = d->nf, nefc = ctx->nefc; - const int* efcind = ctx->efcind; +static void CGeval(mjCGContext* ctx, mjCGPnt* p) { + int ne = ctx->ne, nf = ctx->nf, nefc = ctx->nefc; // clear result mjtNum cost = 0, alpha = p->alpha; @@ -1041,26 +1182,24 @@ static void CGeval(const mjModel* m, const mjData* d, mjCGContext* ctx, mjCGPnt* mju_copy3(quadTotal, ctx->quadGauss); // process constraints - for (int c=0; c < nefc; c++) { - int i = efcind ? efcind[c] : c; - + for (int i=0; i < nefc; i++) { // equality if (i < ne) { - mju_addTo3(quadTotal, ctx->quad+3*c); + mju_addTo3(quadTotal, ctx->quad+3*i); continue; } // friction if (i < ne + nf) { // search point, friction loss, bound (Rf) - mjtNum start = ctx->Jaref[c], dir = ctx->Jv[c]; + mjtNum start = ctx->Jaref[i], dir = ctx->Jv[i]; mjtNum x = start + alpha*dir; - mjtNum f = d->efc_frictionloss[i]; - mjtNum Rf = d->efc_R[i]*f; + mjtNum f = ctx->efc_frictionloss[i]; + mjtNum Rf = ctx->efc_R[i]*f; // -bound < x < bound : quadratic if (-Rf < x && x < Rf) { - mju_addTo3(quadTotal, ctx->quad+3*c); + mju_addTo3(quadTotal, ctx->quad+3*i); } // x < -bound : linear negative @@ -1078,10 +1217,10 @@ static void CGeval(const mjModel* m, const mjData* d, mjCGContext* ctx, mjCGPnt* } // limit and contact - if (d->efc_type[i] == mjCNSTR_CONTACT_ELLIPTIC) { // elliptic cone + if (ctx->efc_type[i] == mjCNSTR_CONTACT_ELLIPTIC) { // elliptic cone // extract contact info - mjContact* con = d->contact + d->efc_id[i]; - mjtNum* quad = ctx->quad + 3*c; + const mjContact* con = ctx->contact + ctx->efc_id[i]; + mjtNum* quad = ctx->quad + 3*i; int dim = con->dim; mjtNum mu = con->mu; @@ -1137,14 +1276,14 @@ static void CGeval(const mjModel* m, const mjData* d, mjCGContext* ctx, mjCGPnt* } // advance to next constraint - c += (dim-1); + i += (dim-1); } else { // inequality // search point - mjtNum x = ctx->Jaref[c] + alpha*ctx->Jv[c]; + mjtNum x = ctx->Jaref[i] + alpha*ctx->Jv[i]; // active if (x < 0) { - mju_addTo3(quadTotal, ctx->quad+3*c); + mju_addTo3(quadTotal, ctx->quad+3*i); } } } @@ -1170,7 +1309,7 @@ static void CGeval(const mjModel* m, const mjData* d, mjCGContext* ctx, mjCGPnt* // update bracket point given 3 candidate points -static int updateBracket(const mjModel* m, const mjData* d, mjCGContext* ctx, +static int updateBracket(mjCGContext* ctx, mjCGPnt* p, const mjCGPnt candidates[3], mjCGPnt* pnext) { int flag = 0; for (int i=0; i < 3; i++) { @@ -1192,7 +1331,7 @@ static int updateBracket(const mjModel* m, const mjData* d, mjCGContext* ctx, // compute next point if updated if (flag) { pnext->alpha = p->alpha - p->deriv[0]/p->deriv[1]; - CGeval(m, d, ctx, pnext); + CGeval(ctx, pnext); } return flag; @@ -1201,8 +1340,8 @@ static int updateBracket(const mjModel* m, const mjData* d, mjCGContext* ctx, // line search -static mjtNum CGsearch(const mjModel* m, const mjData* d, mjCGContext* ctx) { - int nv = ctx->nv; +static mjtNum CGsearch(mjCGContext* ctx, mjtNum tolerance, mjtNum ls_iterations) { + int nv = ctx->nv, nefc = ctx->nefc; mjCGPnt p0, p1, p2, pmid, p1next, p2next; // clear results @@ -1218,23 +1357,36 @@ static mjtNum CGsearch(const mjModel* m, const mjData* d, mjCGContext* ctx) { } // compute scaled gradtol and slope scaling - mjtNum gtol = m->opt.tolerance * m->opt.ls_tolerance * snorm / ctx->scale; + mjtNum gtol = tolerance * snorm / ctx->scale; mjtNum slopescl = ctx->scale / snorm; - // compute Mv, Jv - mj_mulM_island(m, d, ctx->Mv, ctx->search, ctx->island, /*flg_vecunc=*/0); - mj_mulJacVec_island(m, d, ctx->Jv, ctx->search, ctx->island, /*flg_resunc=*/0, /*flg_vecunc=*/0); + // compute Mv = M * v (island or monolithic) + if (ctx->island >= 0) { + mju_mulSymVecSparse(ctx->Mv, ctx->qM, ctx->search, nv, + ctx->M_rownnz, ctx->M_rowadr, ctx->M_diagnum, ctx->M_colind); + } else { + mj_mulM_impl(ctx->Mv, ctx->search, nv, ctx->qM, + ctx->dof_Madr, ctx->dof_parentid, ctx->M_diagnum); + } + + // compute Jv = J * search (dense or sparse) + if (!ctx->J_rowadr) { + mju_mulMatVec(ctx->Jv, ctx->J, ctx->search, nefc, nv); + } else { + mju_mulMatVecSparse(ctx->Jv, ctx->J, ctx->search, nefc, + ctx->J_rownnz, ctx->J_rowadr, ctx->J_colind, ctx->J_rowsuper); + } // prepare quadratics and cones - CGprepare(m, d, ctx); + CGprepare(ctx); // init at alpha = 0, save p0.alpha = 0; - CGeval(m, d, ctx, &p0); + CGeval(ctx, &p0); // always attempt one Newton step p1.alpha = p0.alpha - p0.deriv[0]/p0.deriv[1]; - CGeval(m, d, ctx, &p1); + CGeval(ctx, &p1); if (p0.cost < p1.cost) { p1 = p0; } @@ -1289,14 +1441,14 @@ static mjtNum CGsearch(const mjModel* m, const mjData* d, mjCGContext* ctx) { // one-sided search int p2update = 0; - while (p1.deriv[0]*dir <= -gtol && ctx->LSiter < m->opt.ls_iterations) { + while (p1.deriv[0]*dir <= -gtol && ctx->LSiter < ls_iterations) { // save current p2 = p1; p2update = 1; // move to Newton point w.r.t current p1.alpha -= p1.deriv[0]/p1.deriv[1]; - CGeval(m, d, ctx, &p1); + CGeval(ctx, &p1); // check for convergence if (mju_abs(p1.deriv[0]) < gtol) { @@ -1306,7 +1458,7 @@ static mjtNum CGsearch(const mjModel* m, const mjData* d, mjCGContext* ctx) { } // check for failure to bracket - if (ctx->LSiter >= m->opt.ls_iterations) { + if (ctx->LSiter >= ls_iterations) { ctx->LSresult = 3; // could not bracket ctx->LSslope = mju_abs(p1.deriv[0])*slopescl; return p1.alpha; @@ -1322,13 +1474,13 @@ static mjtNum CGsearch(const mjModel* m, const mjData* d, mjCGContext* ctx) { // compute next-points for bracket p2next = p1; p1next.alpha = p1.alpha - p1.deriv[0]/p1.deriv[1]; - CGeval(m, d, ctx, &p1next); + CGeval(ctx, &p1next); // bracketed search - while (ctx->LSiter < m->opt.ls_iterations) { + while (ctx->LSiter < ls_iterations) { // evaluate at midpoint pmid.alpha = 0.5*(p1.alpha + p2.alpha); - CGeval(m, d, ctx, &pmid); + CGeval(ctx, &pmid); // make list of candidates mjCGPnt candidates[3] = {p1next, p2next, pmid}; @@ -1349,8 +1501,8 @@ static mjtNum CGsearch(const mjModel* m, const mjData* d, mjCGContext* ctx) { } // update brackets - int b1 = updateBracket(m, d, ctx, &p1, candidates, &p1next); - int b2 = updateBracket(m, d, ctx, &p2, candidates, &p2next); + int b1 = updateBracket(ctx, &p1, candidates, &p1next); + int b2 = updateBracket(ctx, &p2, candidates, &p2next); // no update possible: numerical accuracy reached, use midpoint if (!b1 && !b2) { @@ -1730,8 +1882,6 @@ static void mj_solCGNewton(const mjModel* m, mjData* d, int island, int maxiter, // local copies int nv = ctx.nv; int nefc = ctx.nefc; - const int* dofind = ctx.dofind; - const int* efcind = ctx.efcind; // allocate local storage if (!flg_Newton) { @@ -1741,27 +1891,32 @@ static void mj_solCGNewton(const mjModel* m, mjData* d, int island, int maxiter, } int* oldstate = mjSTACKALLOC(d, nefc, int); - // initialize matrix-vector products - int flg_vecunc = 1; // d->qacc is uncompressed - mj_mulM_island(m, d, ctx.Ma, d->qacc, island, flg_vecunc); - int flg_resunc = 0; // ctx.Jaref is compressed - mj_mulJacVec_island(m, d, ctx.Jaref, d->qacc, island, flg_resunc, flg_vecunc); - if (island < 0) { - mju_subFrom(ctx.Jaref, d->efc_aref, nefc); + // compute Ma = M * qacc (island or monolithic) + if (island >= 0) { + mju_mulSymVecSparse(ctx.Ma, ctx.qM, ctx.qacc, nv, + ctx.M_rownnz, ctx.M_rowadr, ctx.M_diagnum, ctx.M_colind); } else { - for (int c=0; c < nefc; c++) { - ctx.Jaref[c] -= d->efc_aref[efcind[c]]; - } + mj_mulM_impl(ctx.Ma, ctx.qacc, nv, ctx.qM, + ctx.dof_Madr, ctx.dof_parentid, ctx.M_diagnum); } + // compute Jaref = J * qacc - aref (dense or sparse) + if (!ctx.J_rownnz) { + mju_mulMatVec(ctx.Jaref, ctx.J, ctx.qacc, nefc, nv); + } else { + mju_mulMatVecSparse(ctx.Jaref, ctx.J, ctx.qacc, nefc, + ctx.J_rownnz, ctx.J_rowadr, ctx.J_colind, ctx.J_rowsuper); + } + mju_subFrom(ctx.Jaref, ctx.efc_aref, nefc); + // first update - CGupdateConstraint(m, d, &ctx); + CGupdateConstraint(&ctx); if (flg_Newton) { // compute and factorize Hessian MakeHessian(m, d, &ctx); FactorizeHessian(m, d, &ctx, /*flg_recompute=*/0); } - CGupdateGradient(m, d, &ctx); + CGupdateGradient(&ctx); // start both with preconditioned gradient mju_scl(ctx.search, ctx.Mgrad, -1, nv); @@ -1772,8 +1927,9 @@ static void mj_solCGNewton(const mjModel* m, mjData* d, int island, int maxiter, scale = 1 / (m->stat.meaninertia * mjMAX(1, m->nv)); } else { mjtNum island_inertia = 0; - for (int c=0; c < nv; c++) { - island_inertia += d->qM[m->dof_Madr[dofind[c]]]; + for (int i=0; i < nv; i++) { + int* map2dof = d->map_idof2dof + d->island_idofadr[island]; + island_inertia += d->qM[m->dof_Madr[map2dof[i]]]; } scale = 1 / island_inertia; } @@ -1782,7 +1938,7 @@ static void mj_solCGNewton(const mjModel* m, mjData* d, int island, int maxiter, // main loop while (iter < maxiter) { // perform linesearch - alpha = CGsearch(m, d, &ctx); + alpha = CGsearch(&ctx, m->opt.tolerance * m->opt.ls_tolerance, m->opt.ls_iterations); // no improvement: done if (alpha == 0) { @@ -1790,13 +1946,7 @@ static void mj_solCGNewton(const mjModel* m, mjData* d, int island, int maxiter, } // move to new solution - if (island < 0) { - mju_addToScl(d->qacc, ctx.search, alpha, nv); - } else { - for (int c=0; c < nv; c++) { - d->qacc[dofind[c]] += alpha * ctx.search[c]; - } - } + mju_addToScl(ctx.qacc, ctx.search, alpha, nv); mju_addToScl(ctx.Ma, ctx.Mv, alpha, nv); mju_addToScl(ctx.Jaref, ctx.Jv, alpha, nefc); @@ -1805,27 +1955,20 @@ static void mj_solCGNewton(const mjModel* m, mjData* d, int island, int maxiter, mju_copy(gradold, ctx.grad, nv); mju_copy(Mgradold, ctx.Mgrad, nv); } - if (island < 0) { - mju_copyInt(oldstate, d->efc_state, nefc); - } else { - for (int c=0; c < nefc; c++) { - oldstate[c] = d->efc_state[efcind[c]]; - } - } + mju_copyInt(oldstate, ctx.efc_state, nefc); mjtNum oldcost = ctx.cost; // update - CGupdateConstraint(m, d, &ctx); + CGupdateConstraint(&ctx); if (flg_Newton) { HessianIncremental(m, d, &ctx, oldstate); } - CGupdateGradient(m, d, &ctx); + CGupdateGradient(&ctx); // count state changes int nchange = 0; - for (int c=0; c < nefc; c++) { - int i = efcind ? efcind[c] : c; - nchange += (d->efc_state[i] != oldstate[c]); + for (int i=0; i < nefc; i++) { + nchange += (ctx.efc_state[i] != oldstate[i]); } // scale improvement, gradient, save stats @@ -1857,8 +2000,8 @@ static void mj_solCGNewton(const mjModel* m, mjData* d, int island, int maxiter, } // update - for (int c=0; c < nv; c++) { - ctx.search[c] = -ctx.Mgrad[c] + beta*ctx.search[c]; + for (int i=0; i < nv; i++) { + ctx.search[i] = -ctx.Mgrad[i] + beta*ctx.search[i]; } } } diff --git a/src/engine/engine_support.c b/src/engine/engine_support.c index 2b3960e8..541cf31f 100644 --- a/src/engine/engine_support.c +++ b/src/engine/engine_support.c @@ -972,14 +972,9 @@ void mj_fullM(const mjModel* m, mjtNum* dst, const mjtNum* M) { -// multiply vector by inertia matrix -void mj_mulM(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec) { - int nv = m->nv; - const mjtNum* M = d->qM; - const int* Madr = m->dof_Madr; - const int* parentid = m->dof_parentid; - const int* simplenum = m->dof_simplenum; - +// multiply vector by inertia matrix (implementation) +void mj_mulM_impl(mjtNum* res, const mjtNum* vec, int nv, const mjtNum* M, + const int* Madr, const int* parentid, const int* simplenum) { mju_zero(res, nv); for (int i=0; i < nv; i++) { @@ -1031,64 +1026,9 @@ void mj_mulM(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec) -// multiply vector by inertia matrix for one dof island -void mj_mulM_island(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec, - int island, int flg_vecunc) { - // if no island, call regular function - if (island < 0) { - mj_mulM(m, d, res, vec); - return; - } - - // local constants: general - const mjtNum* M = d->qM; - const int* Madr = m->dof_Madr; - const int* parentid = m->dof_parentid; - const int* simplenum = m->dof_simplenum; - - // local constants: island specific - int ndof = d->island_dofnum[island]; - const int* dofind = d->island_dofind + d->island_dofadr[island]; - const int* islandind = d->dof_islandind; - - mju_zero(res, ndof); - - for (int k=0; k < ndof; k++) { - // address in full dof vector - int i = dofind[k]; - - // address in M - int adr = Madr[i]; - - // diagonal - if (flg_vecunc) { - res[k] = M[adr]*vec[i]; - } else { - res[k] = M[adr]*vec[k]; - } - - // simple dof: continue - if (simplenum[i]) { - continue; - } - - // off-diagonal - int j = parentid[i]; - while (j >= 0) { - adr++; - int l = islandind[j]; - if (flg_vecunc) { - res[k] += M[adr]*vec[j]; - res[l] += M[adr]*vec[i]; - } else { - res[k] += M[adr]*vec[l]; - res[l] += M[adr]*vec[k]; - } - - // advance to parent - j = parentid[j]; - } - } +// multiply vector by inertia matrix +void mj_mulM(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec) { + mj_mulM_impl(res, vec, m->nv, d->qM, m->dof_Madr, m->dof_parentid, m->dof_simplenum); } diff --git a/src/engine/engine_support.h b/src/engine/engine_support.h index c188ea38..220979da 100644 --- a/src/engine/engine_support.h +++ b/src/engine/engine_support.h @@ -120,13 +120,13 @@ MJAPI void mj_angmomMat(const mjModel* m, mjData* d, mjtNum* mat, int body); // convert sparse inertia matrix M into full matrix MJAPI void mj_fullM(const mjModel* m, mjtNum* dst, const mjtNum* M); +// multiply vector by inertia matrix (implementation) +MJAPI void mj_mulM_impl(mjtNum* res, const mjtNum* vec, int nv, const mjtNum* M, + const int* Madr, const int* parentid, const int* simplenum); + // multiply vector by inertia matrix MJAPI void mj_mulM(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec); -// multiply vector by inertia matrix for one dof island -MJAPI void mj_mulM_island(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec, - int island, int flg_vecunc); - // multiply vector by (inertia matrix)^(1/2) MJAPI void mj_mulM2(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec); diff --git a/src/engine/engine_util_misc.c b/src/engine/engine_util_misc.c index bec28aa3..b7f06601 100644 --- a/src/engine/engine_util_misc.c +++ b/src/engine/engine_util_misc.c @@ -1408,6 +1408,24 @@ void mju_scatter(mjtNum* restrict res, const mjtNum* restrict vec, const int* re +// gather integers +void mju_gatherInt(int* restrict res, const int* restrict vec, const int* restrict ind, int n) { + for (int i=0; i < n; i++) { + res[i] = vec[ind[i]]; + } +} + + + +// scatter integers +void mju_scatterInt(int* restrict res, const int* restrict vec, const int* restrict ind, int n) { + for (int i=0; i < n; i++) { + res[ind[i]] = vec[i]; + } +} + + + // insertion sort, increasing order void mju_insertionSort(mjtNum* list, int n) { for (int i=1; i < n; i++) { diff --git a/src/engine/engine_util_misc.h b/src/engine/engine_util_misc.h index de10f907..f7f23782 100644 --- a/src/engine/engine_util_misc.h +++ b/src/engine/engine_util_misc.h @@ -156,12 +156,18 @@ MJAPI void mju_d2n(mjtNum* res, const double* vec, int n); // convert from mjtNum to double MJAPI void mju_n2d(double* res, const mjtNum* vec, int n); -// gather +// gather mjtNums MJAPI void mju_gather(mjtNum* res, const mjtNum* vec, const int* ind, int n); -// scatter +// scatter mjtNums MJAPI void mju_scatter(mjtNum* res, const mjtNum* vec, const int* ind, int n); +// gather integers +MJAPI void mju_gatherInt(int* res, const int* vec, const int* ind, int n); + +// scatter integers +MJAPI void mju_scatterInt(int* res, const int* vec, const int* ind, int n); + // insertion sort, increasing order MJAPI void mju_insertionSort(mjtNum* list, int n); diff --git a/src/engine/engine_vis_state.c b/src/engine/engine_vis_state.c index 9a0ea210..ff305f05 100644 --- a/src/engine/engine_vis_state.c +++ b/src/engine/engine_vis_state.c @@ -98,7 +98,6 @@ void mjv_makeSceneState(const mjModel* m, const mjData* d, mjvSceneState* scnsta // buffer space required for islands scnstate->nbuffer += roundUpToCacheLine(sizeof(*d->island_dofadr) * m->ntree); - scnstate->nbuffer += roundUpToCacheLine(sizeof(*d->island_dofind) * m->nv); scnstate->nbuffer += roundUpToCacheLine(sizeof(*d->dof_island) * m->nv); scnstate->nbuffer += roundUpToCacheLine(sizeof(*d->efc_island) * maxgeom * condimmax); scnstate->nbuffer += roundUpToCacheLine(sizeof(*d->tendon_efcadr) * m->ntendon); @@ -136,9 +135,6 @@ void mjv_makeSceneState(const mjModel* m, const mjData* d, mjvSceneState* scnsta scnstate->data.island_dofadr = (int*)ptr; ptr += roundUpToCacheLine(sizeof(*scnstate->data.island_dofadr) * scnstate->model.ntree); - scnstate->data.island_dofind = (int*)ptr; - ptr += roundUpToCacheLine(sizeof(*scnstate->data.island_dofind) * scnstate->model.nv); - scnstate->data.dof_island = (int*)ptr; ptr += roundUpToCacheLine(sizeof(*scnstate->data.dof_island) * scnstate->model.nv); @@ -224,7 +220,6 @@ void mjv_assignFromSceneState(const mjvSceneState* scnstate, mjModel* m, mjData* d->contact = scnstate->data.contact; d->efc_force = scnstate->data.efc_force; d->island_dofadr = scnstate->data.island_dofadr; - d->island_dofind = scnstate->data.island_dofind; d->dof_island = scnstate->data.dof_island; d->efc_island = scnstate->data.efc_island; d->tendon_efcadr = scnstate->data.tendon_efcadr; @@ -385,7 +380,6 @@ void mjv_updateSceneState(const mjModel* m, mjData* d, const mjvOption* opt, scnstate->data.nisland = d->nisland; if (d->nisland) { memcpy(scnstate->data.island_dofadr, d->island_dofadr, sizeof(*d->island_dofadr) * d->nisland); - memcpy(scnstate->data.island_dofind, d->island_dofind, sizeof(*d->island_dofind) * m->nv); memcpy(scnstate->data.dof_island, d->dof_island, sizeof(*d->dof_island) * m->nv); memcpy(scnstate->data.tendon_efcadr, d->tendon_efcadr, sizeof(*d->tendon_efcadr) * m->ntendon); } diff --git a/src/engine/engine_vis_visualize.c b/src/engine/engine_vis_visualize.c index 1e665a2c..012cb966 100644 --- a/src/engine/engine_vis_visualize.c +++ b/src/engine/engine_vis_visualize.c @@ -91,9 +91,9 @@ static void makeLabel(const mjModel* m, mjtObj type, int id, char* label) { // assign pseudo-random rgba to constraint island using Halton sequence static void islandColor(float rgba[4], int islanddofadr) { - rgba[0] = 0.1f + 0.8f*mju_Halton(islanddofadr + 1, 2); - rgba[1] = 0.1f + 0.8f*mju_Halton(islanddofadr + 1, 3); - rgba[2] = 0.1f + 0.8f*mju_Halton(islanddofadr + 1, 5); + rgba[0] = 0.1f + 0.9f*mju_Halton(islanddofadr + 1, 2); + rgba[1] = 0.1f + 0.9f*mju_Halton(islanddofadr + 1, 3); + rgba[2] = 0.1f + 0.9f*mju_Halton(islanddofadr + 1, 5); rgba[3] = 1; } @@ -152,7 +152,7 @@ static void addContactGeom(const mjModel* m, mjData* d, const mjtByte* flags, // override standard colors if visualizing islands if (vopt->flags[mjVIS_ISLAND] && d->nisland && efc_adr >= 0) { // set color using island's first dof - islandColor(thisgeom->rgba, d->island_dofind[d->island_dofadr[d->efc_island[efc_adr]]]); + islandColor(thisgeom->rgba, d->island_dofadr[d->efc_island[efc_adr]]); } // otherwise regular colors (different for included and excluded contacts) @@ -1344,7 +1344,7 @@ void mjv_addGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, int island = d->dof_island[m->body_dofadr[weld_id]]; if (island > -1) { // color using island's first dof - islandColor(rgba_island, d->island_dofind[d->island_dofadr[island]]); + islandColor(rgba_island, d->island_dofadr[island]); } } } @@ -1835,7 +1835,7 @@ void mjv_addGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, if (d->tendon_efcadr[i] != -1) { // set color using island's first dof int island = d->efc_island[d->tendon_efcadr[i]]; - islandColor(rgba_island, d->island_dofind[d->island_dofadr[island]]); + islandColor(rgba_island, d->island_dofadr[island]); } } setMaterial(m, thisgeom, tendon_matid, rgba, vopt->flags); diff --git a/test/engine/engine_core_constraint_test.cc b/test/engine/engine_core_constraint_test.cc index a0a8f149..3704bdfc 100644 --- a/test/engine/engine_core_constraint_test.cc +++ b/test/engine/engine_core_constraint_test.cc @@ -25,6 +25,7 @@ #include #include "src/engine/engine_core_constraint.h" #include "src/engine/engine_support.h" +#include "src/engine/engine_util_misc.h" #include "test/fixture.h" namespace mujoco { @@ -284,205 +285,15 @@ TEST_F(CoreConstraintTest, EqualityBodySite) { mj_deleteModel(model); } - static const char* const kIlslandEfcPath = "engine/testdata/island/island_efc.xml"; -TEST_F(CoreConstraintTest, MulJacVecIsland) { +// validate mj_constraintUpdate_impl +TEST_F(CoreConstraintTest, ConstraintUpdateImpl) { const std::string xml_path = GetTestDataFilePath(kIlslandEfcPath); mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0); - mjData* data = mj_makeData(model); - - // allocate vec_nv, fill with arbitrary values - mjtNum* vec_nv = (mjtNum*) mju_malloc(sizeof(mjtNum)*model->nv); - for (int i=0; i < model->nv; i++) { - vec_nv[i] = 0.2 + 0.3*i; - } - - // iterate through dense and sparse - for (mjtJacobian sparsity : {mjJAC_DENSE, mjJAC_SPARSE}) { - model->opt.jacobian = sparsity; - - // simulate for 0.2 seconds - mj_resetData(model, data); - while (data->time < 0.2) { - mj_step(model, data); - } - mj_forward(model, data); - - // multiply by Jacobian: vec_nefc = J * vec_nv - mjtNum* vec_nefc = (mjtNum*) mju_malloc(sizeof(mjtNum)*data->nefc); - mj_mulJacVec(model, data, vec_nefc, vec_nv); - mjtNum* vec_nefc_tmp = (mjtNum*) mju_malloc(sizeof(mjtNum)*data->nefc); - - // iterate over islands - for (int i=0; i < data->nisland; i++) { - // allocate dof and efc vectors for island - int dofnum = data->island_dofnum[i]; - mjtNum* vec_nvi = (mjtNum*)mju_malloc(sizeof(mjtNum) * dofnum); - int efcnum = data->island_efcnum[i]; - mjtNum* vec_nefci = (mjtNum*)mju_malloc(sizeof(mjtNum) * efcnum); - - // get indices - int* dofind = data->island_dofind + data->island_dofadr[i]; - int* efcind = data->island_efcind + data->island_efcadr[i]; - - // copy values into vec_nvi - for (int j=0; j < dofnum; j++) { - vec_nvi[j] = vec_nv[dofind[j]]; - } - - // ===== both compressed - int flg_resunc = 0; - int flg_vecunc = 0; - mju_zero(vec_nefci, efcnum); // clear output - mj_mulJacVec_island(model, data, vec_nefci, vec_nvi, - i, flg_resunc, flg_vecunc); - - // expect corresponding values to match - for (int j=0; j < efcnum; j++) { - EXPECT_THAT(vec_nefci[j], DoubleNear(vec_nefc[efcind[j]], 1e-12)); - } - - // ===== input uncompressed: read from vec_nv - flg_resunc = 0; - flg_vecunc = 1; - mju_zero(vec_nefci, efcnum); // clear output - mj_mulJacVec_island(model, data, vec_nefci, vec_nv, - i, flg_resunc, flg_vecunc); - - // expect corresponding values to match - for (int j=0; j < efcnum; j++) { - EXPECT_THAT(vec_nefci[j], DoubleNear(vec_nefc[efcind[j]], 1e-12)); - } - - // ===== output uncompressed: write to vec_nefc_tmp - flg_resunc = 1; - flg_vecunc = 0; - mju_zero(vec_nefc_tmp, data->nefc); // clear output - mj_mulJacVec_island(model, data, vec_nefc_tmp, vec_nvi, - i, flg_resunc, flg_vecunc); - - // expect corresponding values to match - for (int j=0; j < efcnum; j++) { - EXPECT_THAT(vec_nefc_tmp[efcind[j]], - DoubleNear(vec_nefc[efcind[j]], 1e-12)); - } - - mju_free(vec_nvi); - mju_free(vec_nefci); - } - - mju_free(vec_nefc_tmp); - mju_free(vec_nefc); - } - - mju_free(vec_nv); - mj_deleteData(data); - mj_deleteModel(model); -} - -TEST_F(CoreConstraintTest, MulJacTVecIsland) { - const std::string xml_path = GetTestDataFilePath(kIlslandEfcPath); - mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0); - mjData* data = mj_makeData(model); - - // allocate vec_nv - mjtNum* vec_nv = (mjtNum*) mju_malloc(sizeof(mjtNum)*model->nv); - mjtNum* vec_nv_tmp = (mjtNum*) mju_malloc(sizeof(mjtNum)*model->nv); - - // iterate through dense and sparse - for (mjtJacobian sparsity : {mjJAC_DENSE, mjJAC_SPARSE}) { - model->opt.jacobian = sparsity; - - // simulate for 0.3 seconds - mj_resetData(model, data); - while (data->time < 0.3) { - mj_step(model, data); - } - mj_forward(model, data); - - // allocate vec_nefc, fill with arbitrary values - mjtNum* vec_nefc = (mjtNum*) mju_malloc(sizeof(mjtNum)*data->nefc); - for (int i=0; i < data->nefc; i++) { - vec_nefc[i] = 0.2 + 0.3*i; - } - - // multiply by Jacobian: vec_nv = J^T * vec_nefc - mj_mulJacTVec(model, data, vec_nv, vec_nefc); - - // iterate over islands - for (int i=0; i < data->nisland; i++) { - // allocate dof and efc vectors for island - int dofnum = data->island_dofnum[i]; - mjtNum* vec_nvi = (mjtNum*)mju_malloc(sizeof(mjtNum) * dofnum); - int efcnum = data->island_efcnum[i]; - mjtNum* vec_nefci = (mjtNum*)mju_malloc(sizeof(mjtNum) * efcnum); - - // get indices - int* efcind = data->island_efcind + data->island_efcadr[i]; - int* dofind = data->island_dofind + data->island_dofadr[i]; - - // copy values into vec_nefci - for (int j=0; j < efcnum; j++) { - vec_nefci[j] = vec_nefc[efcind[j]]; - } - - // ==== both compressed - int flg_resunc = 0; - int flg_vecunc = 0; - mju_zero(vec_nvi, dofnum); // clear output - mj_mulJacTVec_island(model, data, vec_nvi, vec_nefci, - i, flg_resunc, flg_vecunc); - - // expect corresponding values to match - for (int j=0; j < dofnum; j++) { - EXPECT_THAT(vec_nvi[j], DoubleNear(vec_nv[dofind[j]], 1e-12)); - } - - // ===== input uncompressed: read from vec_nefc - flg_resunc = 0; - flg_vecunc = 1; - mju_zero(vec_nvi, dofnum); // clear output - mj_mulJacTVec_island(model, data, vec_nvi, vec_nefc, - i, flg_resunc, flg_vecunc); - - // expect corresponding values to match - for (int j=0; j < dofnum; j++) { - EXPECT_THAT(vec_nvi[j], DoubleNear(vec_nv[dofind[j]], 1e-12)); - } - - // ===== output uncompressed: write to vec_nv_tmp - flg_resunc = 1; - flg_vecunc = 0; - mju_zero(vec_nv_tmp, model->nv); // clear output - mj_mulJacTVec_island(model, data, vec_nv_tmp, vec_nefci, - i, flg_resunc, flg_vecunc); - - // expect corresponding values to match - for (int j=0; j < dofnum; j++) { - EXPECT_THAT(vec_nv_tmp[dofind[j]], - DoubleNear(vec_nv[dofind[j]], 1e-12)); - } - - mju_free(vec_nvi); - mju_free(vec_nefci); - } - mju_free(vec_nefc); - } - - mju_free(vec_nv_tmp); - mju_free(vec_nv); - mj_deleteData(data); - mj_deleteModel(model); -} - -// compare mj_constraintUpdate and mj_constraintUpdate_island -TEST_F(CoreConstraintTest, ConstraintUpdateIsland) { - const std::string xml_path = GetTestDataFilePath(kIlslandEfcPath); - mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0); - mjData* data1 = mj_makeData(model); - mjData* data2 = mj_makeData(model); + mjData* d1 = mj_makeData(model); + mjData* d2 = mj_makeData(model); // iterate over sparsity and cone for (mjtJacobian sparsity : {mjJAC_SPARSE, mjJAC_DENSE}) { @@ -491,81 +302,84 @@ TEST_F(CoreConstraintTest, ConstraintUpdateIsland) { model->opt.cone = cone; // simulate for 0.2 seconds - mj_resetData(model, data1); - mj_resetData(model, data2); - while (data1->time < 0.2) { - mj_step(model, data1); - mj_step(model, data2); + mj_resetData(model, d1); + mj_resetData(model, d2); + while (d1->time < 0.2) { + mj_step(model, d1); + mj_step(model, d2); } - mj_forward(model, data1); - mj_forward(model, data2); + mj_forward(model, d1); + mj_forward(model, d2); // get sizes - int nefc = data1->nefc; + int nefc = d1->nefc; int nv = model->nv; - int nisland = data1->nisland; + int nisland = d1->nisland; EXPECT_GT(nisland, 0); // get jar = J*a - aref mjtNum* jar = (mjtNum*)mju_malloc(sizeof(mjtNum) * nefc); - mj_mulJacVec(model, data1, jar, data1->qacc); - mju_subFrom(jar, data1->efc_aref, nefc); + mj_mulJacVec(model, d1, jar, d1->qacc); + mju_subFrom(jar, d1->efc_aref, nefc); // constraint update for data1 given jar mjtNum cost1; - mj_constraintUpdate(model, data1, jar, &cost1, /*flg_coneHessian=*/1); + mj_constraintUpdate(model, d1, jar, &cost1, /*flg_coneHessian=*/1); // iterate over islands, check match mjtNum cost2 = 0; for (int island=0; island < nisland; island++) { // clear outputs from data2 - for (int i=0; i < nefc; i++) data2->efc_state[i] = -1; - mju_zero(data2->efc_force, nefc); - mju_zero(data2->qfrc_constraint, nv); - for (int i=0; i < data2->ncon; i++) mju_zero(data2->contact[i].H, 36); + for (int i=0; i < nefc; i++) d2->efc_state[i] = -1; + mju_zero(d2->efc_force, nefc); + mju_zero(d2->qfrc_constraint, nv); + for (int i=0; i < d2->ncon; i++) mju_zero(d2->contact[i].H, 36); // sizes and indices, in this island - int dofnum = data2->island_dofnum[island]; - int efcnum = data2->island_efcnum[island]; - int* dofind = data2->island_dofind + data2->island_dofadr[island]; - int* efcind = data2->island_efcind + data2->island_efcadr[island]; + int efcnum = d2->island_nefc[island]; - // get jar restricted to island + // gather values into jari mjtNum* jari = (mjtNum*)mju_malloc(sizeof(mjtNum) * efcnum); - for (int c=0; c < efcnum; c++) { - jari[c] = jar[efcind[c]]; - } + int* map2efc = d2->map_iefc2efc + d2->island_iefcadr[island]; + mju_gather(jari, jar, map2efc, efcnum); // update constraints for this island mjtNum cost2i; - mj_constraintUpdate_island(model, data2, jari, &cost2i, - /*flg_coneHessian=*/1, island); + int ne = d2->island_ne[island]; + int nf = d2->island_nf[island]; + int adr = d2->island_iefcadr[island]; + int* state = d2->iefc_state + adr; + mjtNum *force = d2->iefc_force + adr; + mj_constraintUpdate_impl(ne, nf, efcnum, + d2->iefc_D + adr, + d2->iefc_R + adr, + d2->iefc_frictionloss + adr, + jari, + d2->iefc_type + adr, + d2->iefc_id + adr, + d2->contact, + state, + force, + &cost2i, + /*flg_coneHessian=*/1); // compare nefc vectors for (int c=0; c < efcnum; c++) { - int i = efcind[c]; - EXPECT_EQ(data2->efc_island[i], island); - EXPECT_EQ(data2->efc_state[i], data1->efc_state[i]); - EXPECT_THAT(data2->efc_force[i], - DoubleNear(data1->efc_force[i], 1e-12)); - } - - // compare qfrc_constraint - for (int c=0; c < dofnum; c++) { - int i = dofind[c]; - EXPECT_THAT(data2->qfrc_constraint[i], - DoubleNear(data1->qfrc_constraint[i], 1e-12)); + int i = map2efc[c]; + EXPECT_EQ(d2->efc_island[i], island); + EXPECT_EQ(state[c], d1->efc_state[i]); + EXPECT_THAT(force[c], DoubleNear(d1->efc_force[i], 1e-12)); } // compare cone Hessians if (cone == mjCONE_ELLIPTIC) { - for (int c=0; c < data2->ncon; c++) { - int efcadr = data2->contact[c].efc_address; - if (data2->efc_island[efcadr] == island && - data2->efc_state[efcadr] == mjCNSTRSTATE_CONE) { + for (int c=0; c < d2->ncon; c++) { + int efcadr = d2->contact[c].efc_address; + if (d2->efc_island[efcadr] == island && + d2->efc_state[efcadr] == mjCNSTRSTATE_CONE) { for (int j=0; j < 36; j++) { - EXPECT_THAT(data2->contact[c].H[j], - DoubleNear(data1->contact[c].H[j], 1e-12)); + EXPECT_THAT(d2->contact[c].H[j], + DoubleNear(d1->contact[c].H[j], 1e-12)); } } } @@ -584,8 +398,8 @@ TEST_F(CoreConstraintTest, ConstraintUpdateIsland) { } } - mj_deleteData(data2); - mj_deleteData(data1); + mj_deleteData(d2); + mj_deleteData(d1); mj_deleteModel(model); } diff --git a/test/engine/engine_core_smooth_test.cc b/test/engine/engine_core_smooth_test.cc index 1c71475a..7e4c113a 100644 --- a/test/engine/engine_core_smooth_test.cc +++ b/test/engine/engine_core_smooth_test.cc @@ -634,66 +634,6 @@ TEST_F(CoreSmoothTest, RefsiteConservesMomentum) { mj_deleteModel(model); } -static const char* const kIlslandEfcPath = - "engine/testdata/island/island_efc.xml"; -static const char* const kModelPath = - "testdata/model.xml"; - -TEST_F(CoreSmoothTest, SolveMIsland) { - for (auto model_path : {kModelPath, kIlslandEfcPath}) { - const std::string xml_path = GetTestDataFilePath(model_path); - mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0); - mjData* data = mj_makeData(model); - int nv = model->nv; - - // allocate vec, fill with arbitrary values, copy to sol - mjtNum* vec = (mjtNum*) mju_malloc(sizeof(mjtNum) * nv); - mjtNum* res = (mjtNum*) mju_malloc(sizeof(mjtNum) * nv); - for (int i=0; i < nv; i++) { - vec[i] = 0.2 + 0.3*i; - } - mju_copy(res, vec, nv); - - if (model->nkey > 0) mj_resetDataKeyframe(model, data, 0); - - for (int i=0; i < 6; i++) { - mj_step(model, data); - } - - mj_forward(model, data); - - // divide by mass matrix: sol = M^-1 * vec - mj_solveM(model, data, res, res, 1); - - // iterate over islands - for (int i=0; i < data->nisland; i++) { - // allocate dof vectors for island - int dofnum = data->island_dofnum[i]; - mjtNum* res_i = (mjtNum*)mju_malloc(sizeof(mjtNum) * dofnum); - - // copy values into sol_i - int* dofind = data->island_dofind + data->island_dofadr[i]; - for (int j=0; j < dofnum; j++) { - res_i[j] = vec[dofind[j]]; - } - - // divide by mass matrix, for this island - mj_solveM_island(model, data, res_i, i); - - // expect corresponding values to match - for (int j=0; j < dofnum; j++) { - EXPECT_THAT(res_i[j], DoubleNear(res[dofind[j]], 1e-12)); - } - mju_free(res_i); - } - - mju_free(res); - mju_free(vec); - mj_deleteData(data); - mj_deleteModel(model); - } -} - static const char* const kInertiaPath = "engine/testdata/inertia.xml"; TEST_F(CoreSmoothTest, FactorI) { diff --git a/test/engine/engine_island_test.cc b/test/engine/engine_island_test.cc index 3688c15b..e59ac3b4 100644 --- a/test/engine/engine_island_test.cc +++ b/test/engine/engine_island_test.cc @@ -208,17 +208,19 @@ TEST_F(IslandTest, Abacus) { int nv = model->nv; int nefc = data->nefc; int nisland = data->nisland; + int nidof = data->nidof; // 4 dofs, 12 constraints, 2 islands EXPECT_EQ(nv, 4); + EXPECT_EQ(nidof, 3); EXPECT_EQ(nefc, 12); // 3 pyramidal contacts EXPECT_EQ(nisland, 2); // the islands begin at dofs 0 and 1 - EXPECT_THAT(AsVector(data->island_dofadr, nisland), ElementsAre(0, 1)); + EXPECT_THAT(AsVector(data->island_idofadr, nisland), ElementsAre(0, 1)); // number of dofs in the 2 islands - EXPECT_THAT(AsVector(data->island_dofnum, nisland), ElementsAre(1, 2)); + EXPECT_THAT(AsVector(data->island_nv, nisland), ElementsAre(1, 2)); // dof 0 in island 0 // dof 1 in no island @@ -228,19 +230,19 @@ TEST_F(IslandTest, Abacus) { // dof 0 constitutes first island // dofs 2, 3 are the second island // last index is unassigned since dof 1 is unconstrained - EXPECT_THAT(AsVector(data->island_dofind, nv), ElementsAre(0, 2, 3, -1)); + EXPECT_THAT(AsVector(data->map_idof2dof, nv), ElementsAre(0, 2, 3, 1)); // dof 0 constitutes first island // dofs 1 is unassigned // dofs 2, 3 are second island - EXPECT_THAT(AsVector(data->dof_islandind, nv), ElementsAre(0, -1, 0, 1)); + EXPECT_THAT(AsVector(data->map_dof2idof, nv), ElementsAre(0, 3, 1, 2)); // island 0 starts at constraint 0 // island 1 starts at constraint 4 - EXPECT_THAT(AsVector(data->island_efcadr, nisland), ElementsAre(0, 4)); + EXPECT_THAT(AsVector(data->island_iefcadr, nisland), ElementsAre(0, 4)); // number of constraints in the 2 islands - EXPECT_THAT(AsVector(data->island_efcnum, nisland), ElementsAre(4, 8)); + EXPECT_THAT(AsVector(data->island_nefc, nisland), ElementsAre(4, 8)); // first contact (4 constraints) is in island 0 // second contact (8 constraints) is in island 1 @@ -248,7 +250,7 @@ TEST_F(IslandTest, Abacus) { ElementsAre(0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1)); // index lists for islands 0 and 1 - EXPECT_THAT(AsVector(data->island_efcind, nefc), + EXPECT_THAT(AsVector(data->map_iefc2efc, nefc), ElementsAre(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)); // reset, push 0 to the left, 3 to the right, 1,2 to the middle @@ -266,18 +268,20 @@ TEST_F(IslandTest, Abacus) { // local variables nefc = data->nefc; nisland = data->nisland; + nidof = data->nidof; EXPECT_EQ(nisland, 3); - EXPECT_THAT(AsVector(data->island_dofadr, nisland), ElementsAre(0, 1, 3)); - EXPECT_THAT(AsVector(data->island_dofnum, nisland), ElementsAre(1, 2, 1)); + EXPECT_EQ(nidof, 4); + EXPECT_THAT(AsVector(data->island_idofadr, nisland), ElementsAre(0, 1, 3)); + EXPECT_THAT(AsVector(data->island_nv, nisland), ElementsAre(1, 2, 1)); EXPECT_THAT(AsVector(data->dof_island, nv), ElementsAre(0, 1, 1, 2)); - EXPECT_THAT(AsVector(data->island_dofind, nv), ElementsAre(0, 1, 2, 3)); - EXPECT_THAT(AsVector(data->dof_islandind, nv), ElementsAre(0, 0, 1, 0)); - EXPECT_THAT(AsVector(data->island_efcadr, nisland), ElementsAre(0, 4, 8)); - EXPECT_THAT(AsVector(data->island_efcnum, nisland), ElementsAre(4, 4, 4)); + EXPECT_THAT(AsVector(data->map_idof2dof, nv), ElementsAre(0, 1, 2, 3)); + EXPECT_THAT(AsVector(data->map_dof2idof, nv), ElementsAre(0, 1, 2, 3)); + EXPECT_THAT(AsVector(data->island_iefcadr, nisland), ElementsAre(0, 4, 8)); + EXPECT_THAT(AsVector(data->island_nefc, nisland), ElementsAre(4, 4, 4)); EXPECT_THAT(AsVector(data->efc_island, nefc), ElementsAre(0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2)); - EXPECT_THAT(AsVector(data->island_efcind, nefc), + EXPECT_THAT(AsVector(data->map_iefc2efc, nefc), ElementsAre(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)); mj_deleteData(data); @@ -311,27 +315,30 @@ TEST_F(IslandTest, DenseSparse) { int nisland = data1->nisland; // expect sparse and dense to be identical + EXPECT_EQ(data1->nidof, data2->nidof); EXPECT_EQ(data1->nefc, data2->nefc); EXPECT_EQ(data1->nisland, data2->nisland); EXPECT_EQ(data1->nefc, data2->nefc); - EXPECT_EQ(AsVector(data1->island_dofadr, nisland), - AsVector(data2->island_dofadr, nisland)); - EXPECT_EQ(AsVector(data1->island_dofnum, nisland), - AsVector(data2->island_dofnum, nisland)); + EXPECT_EQ(AsVector(data1->island_idofadr, nisland), + AsVector(data2->island_idofadr, nisland)); + EXPECT_EQ(AsVector(data1->island_nv, nisland), + AsVector(data2->island_nv, nisland)); EXPECT_EQ(AsVector(data1->dof_island, nv), AsVector(data2->dof_island, nv)); - EXPECT_EQ(AsVector(data1->island_dofind, nv), - AsVector(data2->island_dofind, nv)); - EXPECT_EQ(AsVector(data1->dof_islandind, nv), - AsVector(data2->dof_islandind, nv)); - EXPECT_EQ(AsVector(data1->island_efcadr, nisland), - AsVector(data2->island_efcadr, nisland)); - EXPECT_EQ(AsVector(data1->island_efcnum, nisland), - AsVector(data2->island_efcnum, nisland)); + EXPECT_EQ(AsVector(data1->map_idof2dof, nv), + AsVector(data2->map_idof2dof, nv)); + EXPECT_EQ(AsVector(data1->map_dof2idof, nv), + AsVector(data2->map_dof2idof, nv)); + EXPECT_EQ(AsVector(data1->island_iefcadr, nisland), + AsVector(data2->island_iefcadr, nisland)); + EXPECT_EQ(AsVector(data1->island_nefc, nisland), + AsVector(data2->island_nefc, nisland)); EXPECT_EQ(AsVector(data1->efc_island, nefc), AsVector(data2->efc_island, nefc)); - EXPECT_EQ(AsVector(data1->island_efcind, nefc), - AsVector(data2->island_efcind, nefc)); + EXPECT_EQ(AsVector(data1->map_iefc2efc, nefc), + AsVector(data2->map_iefc2efc, nefc)); + EXPECT_EQ(AsVector(data1->map_efc2iefc, nefc), + AsVector(data2->map_efc2iefc, nefc)); mj_deleteData(data2); mj_deleteData(data1); @@ -361,6 +368,156 @@ TEST_F(IslandTest, IslandEfc) { mj_deleteModel(model); } +static const char* const k2H100Path = "engine/testdata/island/2humanoid100.xml"; + +TEST_F(IslandTest, IslandJacobian) { + for (const char* local_path : {kIlslandEfcPath, k2H100Path}) { + const std::string xml_path = GetTestDataFilePath(local_path); + mjModel* m = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0); + int jac0 = m->opt.jacobian; + mjData* d = mj_makeData(m); + + for (mjtNum t_stop : {0.0, 0.2, 2.0}) { + while (d->time < t_stop) { + mj_step(m, d); + } + + for (mjtJacobian jac : {mjJAC_DENSE, mjJAC_SPARSE}) { + m->opt.jacobian = jac; + mj_forward(m, d); + + int nv = m->nv; + int nefc = d->nefc; + int nisland = d->nisland; + int nidof = d->nidof; + + mjtNum* J = (mjtNum*)mju_malloc(sizeof(mjtNum) * nefc * nv); + mjtNum* iJ = (mjtNum*)mju_malloc(sizeof(mjtNum) * nefc * nidof); + + // get local dense Jacobian + if (jac == mjJAC_DENSE) { + mju_copy(J, d->efc_J, nefc * nv); + mju_copy(iJ, d->iefc_J, nefc * nidof); + } else { + mju_sparse2dense(J, d->efc_J, nefc, nv, d->efc_J_rownnz, + d->efc_J_rowadr, d->efc_J_colind); + } + + // compare random access in efc_J to contiguous memory in iefc_J + for (int island=0; island < nisland; island++) { + int idof = d->island_idofadr[island]; + int iefc = d->island_iefcadr[island]; + int nefc_island = d->island_nefc[island]; + int nv_island = d->island_nv[island]; + + // === test J + + // get pointer to J_island, dense (nefc_island x nv_island) submatrix + mjtNum* J_island; + if (jac == mjJAC_DENSE) { + // point to starting address of island in efc_J + J_island = iJ + iefc * nidof; + } else { + // dense copy of island in iJ (here used as scratch) + mju_sparse2dense(iJ, d->iefc_J, nefc_island, nv_island, + d->iefc_J_rownnz + iefc, + d->iefc_J_rowadr + iefc, + d->iefc_J_colind); + J_island = iJ; + } + + // sequential memory in J_island equals random access memory in J + for (int i=0; i < nefc_island; i++) { + for (int j=0; j < nv_island; j++) { + int efc = d->map_iefc2efc[iefc + i]; + int dof = d->map_idof2dof[idof + j]; + EXPECT_EQ(J_island[i * nv_island + j], J[efc * nv + dof]); + } + } + + // === test JT (if sparse) + + // get pointer to J_island, dense (nefc_island x nv_island) submatrix + if (jac == mjJAC_SPARSE) { + // dense copy of island in iJ (here used as scratch) + mju_sparse2dense(iJ, d->iefc_JT, nv_island, nefc_island, + d->iefc_JT_rownnz + idof, + d->iefc_JT_rowadr + idof, + d->iefc_JT_colind); + J_island = iJ; + + // sequential memory in J_island equals random access memory in J + for (int i=0; i < nv_island; i++) { + for (int j=0; j < nefc_island; j++) { + int dof = d->map_idof2dof[idof + i]; + int efc = d->map_iefc2efc[iefc + j]; + EXPECT_EQ(J_island[i * nefc_island + j], J[efc * nv + dof]); + } + } + } + } + + mju_free(iJ); + mju_free(J); + } + + // reset opt.jacobian to initial value + m->opt.jacobian = jac0; + } + + mj_deleteData(d); + mj_deleteModel(m); + } +} + +TEST_F(IslandTest, IslandInertia) { + for (const char* local_path : {kIlslandEfcPath, k2H100Path}) { + const std::string xml_path = GetTestDataFilePath(local_path); + mjModel* m = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0); + int nv = m->nv; + mjData* d = mj_makeData(m); + mjtNum* M = (mjtNum*)mju_malloc(sizeof(mjtNum) * nv * nv); + + for (mjtNum t_stop : {0.0, 0.2, 2.0}) { + while (d->time < t_stop) { + mj_step(m, d); + } + mj_forward(m, d); + + int nisland = d->nisland; + + // get dense inertia (lower only) + mj_fullM(m, M, d->qM); + + // compare iM sub-matrix to full M + for (int island=0; island < nisland; island++) { + int nvi = d->island_nv[island]; + mjtNum* Mi = (mjtNum*)mju_malloc(sizeof(mjtNum) * nvi * nvi); + + int adr = d->island_idofadr[island]; + mju_sparse2dense(Mi, d->iM, nvi, nvi, + d->iM_rownnz + adr, + d->iM_rowadr + adr, + d->iM_colind); + + // compare Mi to M (lower triangle only) + for (int i=0; i < nvi; i++) { + for (int j=0; j <= i; j++) { + int dofi = d->map_idof2dof[adr + j]; + int dofj = d->map_idof2dof[adr + i]; + EXPECT_EQ(Mi[i * nvi + j], M[dofi * nv + dofj]); + } + } + mju_free(Mi); + } + } + + mju_free(M); + mj_deleteData(d); + mj_deleteModel(m); + } +} + TEST_F(IslandTest, IslandEfcElliptic) { const std::string xml_path = GetTestDataFilePath(kIlslandEfcPath); mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0); diff --git a/test/engine/engine_solver_test.cc b/test/engine/engine_solver_test.cc index b5b2db6d..75b42cf7 100644 --- a/test/engine/engine_solver_test.cc +++ b/test/engine/engine_solver_test.cc @@ -17,7 +17,6 @@ #include #include #include -#include #include #include @@ -29,19 +28,9 @@ namespace { using ::testing::DoubleNear; using ::testing::NotNull; -using ::std::vector; using ::std::abs; using ::std::max; -// compare two vectors, relative error (increase tolerance for large elements) -inline void ExpectEqRel(vector v1, vector v2, mjtNum rtol) { - ASSERT_TRUE(v1.size() == v2.size()); - for (int i = 0; i < v1.size(); i++) { - mjtNum scale = 0.5 * max(2.0, abs(v1[i]) + abs(v2[i])); - EXPECT_THAT(v1[i], DoubleNear(v2[i], scale*rtol)); - } -} - using SolverTest = MujocoTest; static const char* const kModelPath = @@ -169,79 +158,5 @@ TEST_F(SolverTest, IslandsEquivalentForward) { mj_deleteModel(model); } -static const char* const kIlslandEfcPath = - "engine/testdata/island/island_efc.xml"; - -// compare qacc from 1 iteration of monolithic CG solver and one big island -TEST_F(SolverTest, OneBigIsland) { - const std::string xml_path = GetTestDataFilePath(kIlslandEfcPath); - mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0); - ASSERT_THAT(model, NotNull()); - model->opt.solver = mjSOL_CG; // use CG solver - model->opt.disableflags |= mjDSBL_WARMSTART; // disable warmstart - model->opt.tolerance = 0; // set tolerance to 0 - model->opt.enableflags &= ~mjENBL_ISLAND; // disable islands - - int state_size = mj_stateSize(model, mjSTATE_INTEGRATION); - mjtNum* state = (mjtNum*) mju_malloc(sizeof(mjtNum)*state_size); - - mjData* data_island = mj_makeData(model); - mjData* data_noisland = mj_makeData(model); - - int nv = model->nv; - mjtNum rtol = 1e-7; - - // save current (default) iterations - int iterations_default = model->opt.iterations; - - while (data_noisland->time < .2) { - // step and copy the state to data_island - mj_step(model, data_noisland); - mj_getState(model, data_noisland, state, mjSTATE_INTEGRATION); - mj_setState(model, data_island, state, mjSTATE_INTEGRATION); - - // set small number of iterations - model->opt.iterations = 1; - - // call forward on data_noisland - mj_forward(model, data_noisland); - - // enable islands - model->opt.enableflags |= mjENBL_ISLAND; - - // call forward (just for smooth dynamics and to allocate islands) - mj_forward(model, data_island); - - // overwrite island structure with one big island - data_island->nisland = 1; - data_island->island_dofnum[0] = nv; - data_island->island_dofadr[0] = 0; - for (int i = 0; i < nv; i++) { - data_island->island_dofind[i] = data_island->dof_islandind[i] = i; - } - int nefc = data_island->nefc; - data_island->island_efcnum[0] = nefc; - data_island->island_efcadr[0] = 0; - for (int i = 0; i < nefc; i++) data_island->island_efcind[i] = i; - - // solve using using one big island - mj_fwdConstraint(model, data_island); - - // re-disable islands and reset iterations - model->opt.enableflags &= ~mjENBL_ISLAND; - model->opt.iterations = iterations_default; - - // compare accelerations (relative error) - ExpectEqRel(AsVector(data_noisland->qacc, nv), - AsVector(data_island->qacc, nv), rtol); - } - - mj_deleteData(data_noisland); - mj_deleteData(data_island); - mju_free(state); - mj_deleteModel(model); -} - - } // namespace } // namespace mujoco diff --git a/test/engine/engine_support_test.cc b/test/engine/engine_support_test.cc index c7d4f018..71b8e820 100644 --- a/test/engine/engine_support_test.cc +++ b/test/engine/engine_support_test.cc @@ -830,77 +830,6 @@ TEST_F(InertiaTest, mulM2) { mj_deleteModel(model); } -static const char* const kIlslandEfcPath = - "engine/testdata/island/island_efc.xml"; - -TEST_F(SupportTest, MulMIsland) { - const std::string xml_path = GetTestDataFilePath(kIlslandEfcPath); - mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0); - mjData* data = mj_makeData(model); - - // allocate vec, fill with arbitrary values - mjtNum* vec = (mjtNum*) mju_malloc(sizeof(mjtNum)*model->nv); - for (int i=0; i < model->nv; i++) { - vec[i] = 0.2 + 0.3*i; - } - - // simulate for 0.2 seconds - mj_resetData(model, data); - while (data->time < 0.2) { - mj_step(model, data); - } - mj_forward(model, data); - - // multiply by Mass matrix: Mvec = M * vec - mjtNum* Mvec = (mjtNum*) mju_malloc(sizeof(mjtNum)*data->nefc); - mj_mulM(model, data, Mvec, vec); - - // iterate over islands - for (int i=0; i < data->nisland; i++) { - // allocate dof vectors for island - int dofnum = data->island_dofnum[i]; - mjtNum* vec_i = (mjtNum*)mju_malloc(sizeof(mjtNum) * dofnum); - mjtNum* Mvec_i = (mjtNum*)mju_malloc(sizeof(mjtNum) * dofnum); - - // copy values into vec_i - int* dofind = data->island_dofind + data->island_dofadr[i]; - for (int j=0; j < dofnum; j++) { - vec_i[j] = vec[dofind[j]]; - } - - // === compressed: use vec_i - - // multiply by Jacobian, for this island - int flg_vecunc = 0; - mj_mulM_island(model, data, Mvec_i, vec_i, i, flg_vecunc); - - // expect corresponding values to match - for (int j=0; j < dofnum; j++) { - EXPECT_THAT(Mvec_i[j], DoubleNear(Mvec[dofind[j]], 1e-12)); - } - - // === uncompressed: use vec - mju_zero(Mvec_i, dofnum); // clear output - - // multiply by Jacobian, for this island - flg_vecunc = 1; - mj_mulM_island(model, data, Mvec_i, vec, i, flg_vecunc); - - // expect corresponding values to match - for (int j=0; j < dofnum; j++) { - EXPECT_THAT(Mvec_i[j], DoubleNear(Mvec[dofind[j]], 1e-12)); - } - - mju_free(vec_i); - mju_free(Mvec_i); - } - - mju_free(Mvec); - mju_free(vec); - mj_deleteData(data); - mj_deleteModel(model); -} - static constexpr char GeomDistanceTestingModel[] = R"( - """)) + """), + backend_impl='jax', + ) def test_implicitfast_fluid_not_implemented(self): with self.assertRaises(NotImplementedError): - mjx.put_model(mujoco.MjModel.from_xml_string(""" + mjx.put_model( + mujoco.MjModel.from_xml_string(""" """)) + """), + backend_impl='jax', + ) def test_wrap_inside(self): m = test_util.load_test_file('tendon/wrap_sidesite.xml') - mx0 = mjx.put_model(m) + mx0 = mjx.put_model(m, backend_impl='jax') np.testing.assert_equal( - mx0.is_wrap_inside, + mx0._impl.is_wrap_inside, np.array([1, 0, 1, 0, 1, 1, 0]), ) m.site_pos[2] = m.site_pos[1] - mx1 = mjx.put_model(m) + mx1 = mjx.put_model(m, backend_impl='jax') np.testing.assert_equal( - mx1.is_wrap_inside, + mx1._impl.is_wrap_inside, np.array([0, 0, 1, 0, 1, 0, 0]), ) @@ -217,11 +238,11 @@ class ModelIOTest(parameterized.TestCase): class DataIOTest(parameterized.TestCase): """IO tests for mjx.Data.""" - def test_make_data(self): + @parameterized.parameters('jax', 'c') + def test_make_data(self, backend_impl: str): """Test that make_data returns the correct shapes.""" - m = mujoco.MjModel.from_xml_string(_MULTIPLE_CONVEX_OBJECTS) - d = mjx.make_data(m) + d = mjx.make_data(m, backend_impl=backend_impl) nq = 22 nbody = 5 @@ -230,7 +251,7 @@ class DataIOTest(parameterized.TestCase): nv = 19 nefc = 185 - self.assertEqual(d.nefc, nefc) + self.assertEqual(d._impl.nefc, nefc) self.assertEqual(d.qpos.shape, (nq,)) self.assertEqual(d.qvel.shape, (nv,)) self.assertEqual(d.act.shape, (0,)) @@ -251,57 +272,68 @@ class DataIOTest(parameterized.TestCase): self.assertEqual(d.geom_xpos.shape, (6, 3)) self.assertEqual(d.geom_xmat.shape, (6, 3, 3)) self.assertEqual(d.subtree_com.shape, (nbody, 3)) - self.assertEqual(d.cdof.shape, (nv, 6)) - self.assertEqual(d.cinert.shape, (nbody, 10)) - self.assertEqual(d.crb.shape, (nbody, 10)) - self.assertEqual(d.actuator_length.shape, (1,)) - self.assertEqual(d.actuator_moment.shape, (1, nv)) - self.assertEqual(d.qM.shape, (nv, nv)) - self.assertEqual(d.qLD.shape, (nv, nv)) - self.assertEqual(d.qLDiagInv.shape, (0,)) - self.assertEqual(d.contact.dist.shape, (ncon,)) - self.assertEqual(d.contact.pos.shape, (ncon, 3)) - self.assertEqual(d.contact.frame.shape, (ncon, 3, 3)) - self.assertEqual(d.contact.solref.shape, (ncon, 2)) - self.assertEqual(d.contact.solimp.shape, (ncon, 5)) - self.assertEqual(d.contact.geom1.shape, (ncon,)) - self.assertEqual(d.contact.geom2.shape, (ncon,)) - self.assertEqual(d.efc_J.shape, (nefc, nv)) - self.assertEqual(d.efc_frictionloss.shape, (nefc,)) - self.assertEqual(d.efc_D.shape, (nefc,)) - self.assertEqual(d.actuator_velocity.shape, (1,)) + self.assertEqual(d._impl.cdof.shape, (nv, 6)) + self.assertEqual(d._impl.cinert.shape, (nbody, 10)) + self.assertEqual(d._impl.crb.shape, (nbody, 10)) + self.assertEqual(d._impl.actuator_length.shape, (1,)) + self.assertEqual(d._impl.actuator_moment.shape, (1, nv)) + self.assertEqual(d._impl.contact.dist.shape, (ncon,)) + self.assertEqual(d._impl.contact.pos.shape, (ncon, 3)) + self.assertEqual(d._impl.contact.frame.shape, (ncon, 3, 3)) + self.assertEqual(d._impl.contact.solref.shape, (ncon, 2)) + self.assertEqual(d._impl.contact.solimp.shape, (ncon, 5)) + self.assertEqual(d._impl.contact.geom1.shape, (ncon,)) + self.assertEqual(d._impl.contact.geom2.shape, (ncon,)) + self.assertEqual(d._impl.efc_J.shape, (nefc, nv)) + self.assertEqual(d._impl.efc_frictionloss.shape, (nefc,)) + self.assertEqual(d._impl.efc_D.shape, (nefc,)) + self.assertEqual(d._impl.actuator_velocity.shape, (1,)) self.assertEqual(d.cvel.shape, (nbody, 6)) - self.assertEqual(d.cdof_dot.shape, (nv, 6)) + self.assertEqual(d._impl.cdof_dot.shape, (nv, 6)) self.assertEqual(d.qfrc_bias.shape, (nv,)) self.assertEqual(d.qfrc_passive.shape, (nv,)) - self.assertEqual(d.efc_aref.shape, (nefc,)) + self.assertEqual(d._impl.efc_aref.shape, (nefc,)) self.assertEqual(d.qfrc_actuator.shape, (nv,)) self.assertEqual(d.qfrc_smooth.shape, (nv,)) self.assertEqual(d.qacc_smooth.shape, (nv,)) self.assertEqual(d.qfrc_constraint.shape, (nv,)) self.assertEqual(d.qfrc_inverse.shape, (nv,)) - self.assertEqual(d.efc_force.shape, (nefc,)) + self.assertEqual(d._impl.efc_force.shape, (nefc,)) + + if backend_impl == 'jax': + self.assertEqual(d._impl.qM.shape, (nv, nv)) + self.assertEqual(d._impl.qLD.shape, (nv, nv)) + self.assertEqual(d._impl.qLDiagInv.shape, (0,)) + elif backend_impl == 'c': + self.assertEqual(d._impl.qM.shape, (nm,)) + self.assertEqual(d._impl.qLD.shape, (nm,)) + self.assertEqual(d._impl.qLDiagInv.shape, (nv,)) # test sparse m.opt.jacobian = mujoco.mjtJacobian.mjJAC_SPARSE - d = mjx.make_data(m) - self.assertEqual(d.qM.shape, (nm,)) - self.assertEqual(d.qLD.shape, (nm,)) - self.assertEqual(d.qLDiagInv.shape, (nv,)) + d = mjx.make_data(m, backend_impl=backend_impl) + self.assertEqual(d._impl.qM.shape, (nm,)) + self.assertEqual(d._impl.qLD.shape, (nm,)) + self.assertEqual(d._impl.qLDiagInv.shape, (nv,)) - def test_put_data(self): + if backend_impl == 'c': + # check C specific fields + self.assertEqual(d._impl.light_xpos.shape, (m.nlight, 3)) + self.assertEqual(d._impl.bvh_active.shape, (m.nbvh,)) + + @parameterized.parameters('jax', 'c') + def test_put_data(self, backend_impl: str): """Test that put_data puts the correct data for dense and sparse.""" - m = mujoco.MjModel.from_xml_string(_MULTIPLE_CONSTRAINTS) d = mujoco.MjData(m) mujoco.mj_step(m, d, 2) - dx = mjx.put_data(m, d) + dx = mjx.put_data(m, d, backend_impl=backend_impl) # check a few fields np.testing.assert_allclose(dx.qpos, d.qpos) np.testing.assert_allclose(dx.xpos, d.xpos) np.testing.assert_allclose(dx.cvel, d.cvel) - np.testing.assert_allclose(dx.cdof_dot, d.cdof_dot) + np.testing.assert_allclose(dx._impl.cdof_dot, d.cdof_dot) # check that there are no weak types self.assertFalse( @@ -312,21 +344,26 @@ class DataIOTest(parameterized.TestCase): ) ) - # check that qM is transformed properly - qm = np.zeros((m.nv, m.nv), dtype=np.float64) - mujoco.mj_fullM(m, qm, d.qM) - np.testing.assert_allclose(qm, mjx.full_m(mjx.put_model(m), dx)) + if backend_impl == 'jax': + # check that qM is transformed properly + qm = np.zeros((m.nv, m.nv), dtype=np.float64) + mujoco.mj_fullM(m, qm, d.qM) + np.testing.assert_allclose(qm, mjx.full_m(mjx.put_model(m), dx)) + elif backend_impl == 'c': + np.testing.assert_allclose(dx._impl.qM, d.qM) + np.testing.assert_allclose(dx._impl.qLD, d.qLD) + np.testing.assert_allclose(dx._impl.qLDiagInv, d.qLDiagInv) # 4 contacts, 2 for each capsule against the plane - self.assertEqual(dx.contact.dist.shape, (4,)) + self.assertEqual(dx._impl.contact.dist.shape, (4,)) self.assertEqual(d.ncon, 1) # however only 1 contact in this step - np.testing.assert_allclose(dx.contact.dist[0], d.contact.dist[0]) - self.assertTrue((dx.contact.dist[1:] > 0).all()) - self.assertEqual(dx.contact.frame.shape, (4, 3, 3)) + np.testing.assert_allclose(dx._impl.contact.dist[0], d.contact.dist[0]) + self.assertTrue((dx._impl.contact.dist[1:] > 0).all()) + self.assertEqual(dx._impl.contact.frame.shape, (4, 3, 3)) np.testing.assert_allclose( - dx.contact.frame[0].reshape(9), d.contact.frame[0] + dx._impl.contact.frame[0].reshape(9), d.contact.frame[0] ) - np.testing.assert_allclose(dx.contact.frame[1:], 0) + np.testing.assert_allclose(dx._impl.contact.frame[1:], 0) # xmat, ximat, geom_xmat are all shape transformed self.assertEqual(dx.xmat.shape, (3, 3, 3)) @@ -339,57 +376,65 @@ class DataIOTest(parameterized.TestCase): np.testing.assert_allclose(dx.site_xmat.reshape((1, 9)), d.site_xmat) # tendon data is correct - np.testing.assert_allclose(dx.ten_length, d.ten_length) - np.testing.assert_equal(dx.ten_wrapadr, np.zeros((1,))) - np.testing.assert_equal(dx.ten_wrapnum, np.zeros((1,))) - np.testing.assert_equal(dx.wrap_obj, np.zeros((2, 2))) - np.testing.assert_equal(dx.wrap_xpos, np.zeros((2, 6))) + np.testing.assert_allclose(dx._impl.ten_length, d.ten_length) + np.testing.assert_equal(dx._impl.ten_wrapadr, np.zeros((1,))) + np.testing.assert_equal(dx._impl.ten_wrapnum, np.zeros((1,))) + np.testing.assert_equal(dx._impl.wrap_obj, np.zeros((2, 2))) + np.testing.assert_equal(dx._impl.wrap_xpos, np.zeros((2, 6))) # efc_ are also shape transformed and padded - self.assertEqual(dx.efc_J.shape, (45, 8)) # nefc, nv + self.assertEqual(dx._impl.efc_J.shape, (45, 8)) # nefc, nv d_efc_j = d.efc_J.reshape((-1, 8)) - np.testing.assert_allclose(dx.efc_J[:3], d_efc_j[:3]) # connect eq - np.testing.assert_allclose(dx.efc_J[3], d_efc_j[3]) # one active limit - np.testing.assert_allclose(dx.efc_J[4], 0) # one inactive limit - np.testing.assert_allclose(dx.efc_J[5:15], d_efc_j[4:14]) # contact - np.testing.assert_allclose(dx.efc_J[15:], 0) # no contact + np.testing.assert_allclose(dx._impl.efc_J[:3], d_efc_j[:3]) # connect eq + np.testing.assert_allclose( + dx._impl.efc_J[3], d_efc_j[3] + ) # one active limit + np.testing.assert_allclose(dx._impl.efc_J[4], 0) # one inactive limit + np.testing.assert_allclose(dx._impl.efc_J[5:15], d_efc_j[4:14]) # contact + np.testing.assert_allclose(dx._impl.efc_J[15:], 0) # no contact # check another efc_ too - self.assertEqual(dx.efc_aref.shape, (45,)) # nefc - np.testing.assert_allclose(dx.efc_aref[:3], d.efc_aref[:3]) - np.testing.assert_allclose(dx.efc_aref[3], d.efc_aref[3]) - np.testing.assert_allclose(dx.efc_aref[4], 0) - np.testing.assert_allclose(dx.efc_aref[5:15], d.efc_aref[4:14]) - np.testing.assert_allclose(dx.efc_aref[15:], 0) + self.assertEqual(dx._impl.efc_aref.shape, (45,)) # nefc + np.testing.assert_allclose(dx._impl.efc_aref[:3], d.efc_aref[:3]) + np.testing.assert_allclose(dx._impl.efc_aref[3], d.efc_aref[3]) + np.testing.assert_allclose(dx._impl.efc_aref[4], 0) + np.testing.assert_allclose(dx._impl.efc_aref[5:15], d.efc_aref[4:14]) + np.testing.assert_allclose(dx._impl.efc_aref[15:], 0) # check sparse transform is correct m.opt.jacobian = mujoco.mjtJacobian.mjJAC_SPARSE d = mujoco.MjData(m) mujoco.mj_step(m, d, 2) - dx_sparse = mjx.put_data(m, d) - np.testing.assert_allclose(dx_sparse.efc_J, dx.efc_J, atol=1e-8) + dx_sparse = mjx.put_data(m, d, backend_impl=backend_impl) + np.testing.assert_allclose(dx_sparse._impl.efc_J, dx._impl.efc_J, atol=1e-8) # check sparse mass matrices are correct - np.testing.assert_allclose(dx_sparse.qM, d.qM, atol=1e-8) - np.testing.assert_allclose(dx_sparse.qLD, d.qLD, atol=1e-8) - np.testing.assert_allclose(dx_sparse.qLDiagInv, d.qLDiagInv, atol=1e-8) + np.testing.assert_allclose(dx_sparse._impl.qM, d.qM, atol=1e-8) + np.testing.assert_allclose(dx_sparse._impl.qLD, d.qLD, atol=1e-8) + np.testing.assert_allclose( + dx_sparse._impl.qLDiagInv, d.qLDiagInv, atol=1e-8 + ) # check dense mass matrices are correct m.opt.jacobian = mujoco.mjtJacobian.mjJAC_DENSE d = mujoco.MjData(m) mujoco.mj_step(m, d, 2) - dx_from_dense = mjx.put_data(m, d) - qm = np.zeros((m.nv, m.nv)) - mujoco.mj_fullM(m, qm, d.qM) - np.testing.assert_allclose(dx_from_dense.qM, qm, atol=1e-8) + dx_from_dense = mjx.put_data(m, d, backend_impl=backend_impl) + if backend_impl == 'jax': + qm = np.zeros((m.nv, m.nv)) + mujoco.mj_fullM(m, qm, d.qM) + np.testing.assert_allclose(dx_from_dense._impl.qM, qm, atol=1e-8) + elif backend_impl == 'c': + np.testing.assert_allclose(dx_from_dense._impl.qM, d.qM, atol=1e-8) - def test_get_data(self): + @parameterized.parameters('jax', 'c') + def test_get_data(self, backend_impl: str): """Test that get_data makes correct MjData.""" m = mujoco.MjModel.from_xml_string(_MULTIPLE_CONSTRAINTS) d = mujoco.MjData(m) mujoco.mj_step(m, d, 2) - dx = mjx.put_data(m, d) + dx = mjx.put_data(m, d, backend_impl=backend_impl) d_2: mujoco.MjData = mjx.get_data(m, dx) # check a few fields @@ -424,6 +469,10 @@ class DataIOTest(parameterized.TestCase): np.testing.assert_allclose(d_2.efc_aref, d.efc_aref) np.testing.assert_allclose(d_2.contact.efc_address, d.contact.efc_address) + if backend_impl == 'c': + # check fields specific to the C implementation + np.testing.assert_allclose(d_2.M_rownnz, d.M_rownnz) + def test_get_data_runs(self): xml = """ @@ -444,13 +493,14 @@ class DataIOTest(parameterized.TestCase): dx = mjx.put_data(m, d) mjx.get_data(m, dx) - def test_get_data_batched(self): + @parameterized.parameters('jax', 'c') + def test_get_data_batched(self, backend_impl): """Test that get_data makes correct List[MjData] for batched Data.""" m = mujoco.MjModel.from_xml_string(_MULTIPLE_CONSTRAINTS) d = mujoco.MjData(m) mujoco.mj_step(m, d, 2) - dx = mjx.put_data(m, d) + dx = mjx.put_data(m, d, backend_impl=backend_impl) # second data in batch has contact dist > 0, disables contact dx_b = jax.tree_util.tree_map(lambda x: jp.stack((x, x + 0.05)), dx) ds = mjx.get_data(m, dx_b) @@ -460,13 +510,14 @@ class DataIOTest(parameterized.TestCase): self.assertEqual(ds[0].ncon, 1) self.assertEqual(ds[1].ncon, 0) - def test_get_data_into(self): + @parameterized.parameters('jax', 'c') + def test_get_data_into(self, backend_impl): """Test that get_data_into correctly populates an MjData.""" m = mujoco.MjModel.from_xml_string(_MULTIPLE_CONSTRAINTS) d = mujoco.MjData(m) mujoco.mj_step(m, d, 2) - dx = mjx.put_data(m, d) + dx = mjx.put_data(m, d, backend_impl=backend_impl) d_2 = mujoco.MjData(m) mjx.get_data_into(d_2, m, dx) @@ -482,32 +533,33 @@ class DataIOTest(parameterized.TestCase): self.assertEqual(d_2.contact.frame.shape, (1, 9)) np.testing.assert_allclose(d_2.contact.frame, d.contact.frame) - def test_get_data_into_wrong_shape(self): + @parameterized.parameters('jax', 'c') + def test_get_data_into_wrong_shape(self, backend_impl): """Tests that get_data_into throwsif input and output shapes don't match.""" m = mujoco.MjModel.from_xml_string(_MULTIPLE_CONSTRAINTS) d = mujoco.MjData(m) mujoco.mj_step(m, d, 2) - dx = mjx.put_data(m, d) + dx = mjx.put_data(m, d, backend_impl=backend_impl) m_2 = mujoco.MjModel.from_xml_string(_MULTIPLE_CONVEX_OBJECTS) d_2 = mujoco.MjData(m_2) with self.assertRaisesRegex(ValueError, r'Input field.*has shape.*'): mjx.get_data_into(d_2, m, dx) - def test_make_matches_put(self): + @parameterized.parameters('jax', 'c') + def test_make_matches_put(self, backend_impl): """Test that make_data produces a pytree that matches put_data.""" - m = mujoco.MjModel.from_xml_string(_MULTIPLE_CONSTRAINTS) d = mujoco.MjData(m) mujoco.mj_step(m, d, 2) - dx = mjx.put_data(m, d) + dx = mjx.put_data(m, d, backend_impl=backend_impl) step_fn = lambda d: d.replace(time=d.time + 1) step_fn_jit = jax.jit(step_fn).lower(dx).compile() # placing an MjData onto device should yield the same treedef mjx.Data as # calling make_data. they should be interchangeable for jax functions: - step_fn_jit(mjx.make_data(m)) + step_fn_jit(mjx.make_data(m, backend_impl=backend_impl)) def test_contact_elliptic_condim1(self): """Test that condim=1 with ConeType.ELLIPTIC is not implemented.""" @@ -556,7 +608,327 @@ class DataIOTest(parameterized.TestCase): """) with self.assertRaises(NotImplementedError): - mjx.put_model(m) + mjx.put_model(m, backend_impl='jax') + + +class FullCompatTest(parameterized.TestCase): + """Tests for the _full_compat flag.""" + + def test_full_compat_deprecated(self): + """Tests that _full_compat is deprecated.""" + xml = """ + + + + + + + + + + + + """ + m = mujoco.MjModel.from_xml_string(xml) + with self.assertWarns(DeprecationWarning): + out = mjx_io.put_model(m, _full_compat=True) + self.assertEqual(out.backend_impl, BackendImpl.C) + with self.assertWarns(DeprecationWarning): + out = mjx_io.make_data(m, _full_compat=True) + self.assertEqual(out.backend_impl, BackendImpl.C) + + +# Test cases for `_resolve_backend_impl_and_device` where the device is +# specified by the user and the device is available. +_DEVICE_TEST_CASES = [ + # Arguments use the following format: + # (device_type_str, backend_impl_str, + # (expected_device, expected_backend_impl))) + # No backend specified. + ('cpu', None, ('cpu', BackendImpl.C)), + ('gpu-notnvidia', None, ('gpu', BackendImpl.JAX)), + ('gpu-nvidia', None, ('gpu', BackendImpl.WARP)), + ('tpu', None, ('tpu', BackendImpl.JAX)), + # JAX backend specified. + ('cpu', 'jax', ('cpu', BackendImpl.JAX)), + ('gpu-notnvidia', 'jax', ('gpu', BackendImpl.JAX)), + ('gpu-nvidia', 'jax', ('gpu', BackendImpl.JAX)), + ('tpu', 'jax', ('tpu', BackendImpl.JAX)), + # WARP backend specified. + ('cpu', 'warp', ('cpu', 'error')), + ('gpu-notnvidia', 'warp', ('cpu', 'error')), + ('gpu-nvidia', 'warp', ('gpu', BackendImpl.WARP)), + ('tpu', 'warp', ('tpu', 'error')), + # C backend specified. + ('cpu', 'c', ('cpu', BackendImpl.C)), + ('gpu-notnvidia', 'c', ('cpu', 'error')), + ('gpu-nvidia', 'c', ('cpu', 'error')), + ('tpu', 'c', ('tpu', 'error')), +] + +# Test cases for `_resolve_backend_impl_and_device` where the user does NOT +# specify a device. We mock the JAX default device. +_DEFAULT_DEVICE_TEST_CASES = [ + # Arguments use the following format: + # (jax.default_device, backend_impl_str, + # (expected_device, expected_backend_impl)) + # No backend impl specified. + ('cpu', None, ('cpu', BackendImpl.C)), + ('gpu-notnvidia', None, ('gpu', BackendImpl.JAX)), + ('gpu-nvidia', None, ('gpu', BackendImpl.WARP)), + ('tpu', None, ('tpu', BackendImpl.JAX)), + # JAX backend impl specified. + ('cpu', 'jax', ('cpu', BackendImpl.JAX)), + ('gpu-notnvidia', 'jax', ('gpu', BackendImpl.JAX)), + ('gpu-nvidia', 'jax', ('gpu', BackendImpl.JAX)), + ('tpu', 'jax', ('tpu', BackendImpl.JAX)), + # WARP backend impl specified. + ('cpu', 'warp', ('cpu', 'error')), + ('gpu-notnvidia', 'warp', ('cpu', 'error')), + ('gpu-nvidia', 'warp', ('gpu', BackendImpl.WARP)), + ('tpu', 'warp', ('tpu', 'error')), + # C backend impl specified, CPU should always be available. + ('cpu', 'c', ('cpu', BackendImpl.C)), + ('gpu-notnvidia', 'c', ('cpu', BackendImpl.C)), + ('gpu-nvidia', 'c', ('cpu', BackendImpl.C)), + ('tpu', 'c', ('cpu', BackendImpl.C)), +] + + +class ResolveBackendImplAndDeviceTest(parameterized.TestCase): + """Tests for the _resolve_backend_impl_and_device function.""" + + def setUp(self): + super().setUp() + + # Create mock devices + self.mock_cpu = mock.Mock(spec=jax.Device) + self.mock_cpu.platform = 'cpu' + self.mock_cpu.device_kind = 'Mock CPU' + self.mock_cpu.id = 0 + + self.mock_nvidia_gpu = mock.Mock(spec=jax.Device) + self.mock_nvidia_gpu.platform = 'gpu' + self.mock_nvidia_gpu.device_kind = 'NVIDIA Mocked GPU' + self.mock_nvidia_gpu.id = 0 + + self.mock_other_gpu = mock.Mock(spec=jax.Device) + self.mock_other_gpu.platform = 'gpu' + self.mock_other_gpu.device_kind = 'Other Mocked GPU' + self.mock_other_gpu.id = 1 + + self.mock_tpu = mock.Mock(spec=jax.Device) + self.mock_tpu.platform = 'tpu' + self.mock_tpu.device_kind = 'Mock TPU' + self.mock_tpu.id = 0 + + # Patch jax.devices for the entire test class using enter_context + self.mock_jax_devices = self.enter_context(mock.patch('jax.devices')) + self.mock_default_backend = self.enter_context( + mock.patch('jax.default_backend') + ) + + @parameterized.named_parameters( + (f'{str(args[0])}_{str(args[1])}', *args) for args in _DEVICE_TEST_CASES + ) + @mock.patch.dict( + os.environ, {'MJX_WARP_ENABLED': 'true', 'MJX_C_DEFAULT_ENABLED': 'true'} + ) + def test_resolve_with_device( + self, + device_type_str, + backend_impl_str, + expected, + ): + """Tests various combinations of device and backend impls.""" + input_device = { + 'cpu': self.mock_cpu, + 'gpu-nvidia': self.mock_nvidia_gpu, + 'gpu-notnvidia': self.mock_other_gpu, + 'tpu': self.mock_tpu, + }[device_type_str] + + def devices_side_effect(backend=None): + # assume the user-specified device is always available + if backend == 'cpu': + return [self.mock_cpu] + elif backend == 'gpu': + if 'nvidia' in device_type_str: + return [self.mock_nvidia_gpu] + return [self.mock_other_gpu] + elif backend == 'tpu': + return [self.mock_tpu] + elif backend == 'cuda': + return [self.mock_nvidia_gpu] + + raise AssertionError('Should not be called.') + + self.mock_jax_devices.side_effect = devices_side_effect + + expected_device, expected_backend_impl = expected + if expected_backend_impl == 'error': + with self.assertRaises(AssertionError): + mjx_io._resolve_backend_impl_and_device( + backend_impl=backend_impl_str, device=input_device + ) + return + + actual_backend_impl, actual_device = ( + mjx_io._resolve_backend_impl_and_device( + backend_impl=backend_impl_str, device=input_device + ) + ) + + self.assertEqual(actual_backend_impl, expected_backend_impl) + self.assertIsNotNone(actual_device) + self.assertEqual(actual_device.platform, expected_device) + + @parameterized.named_parameters( + (f'{str(args[0])}_{str(args[1])}', *args) + for args in _DEFAULT_DEVICE_TEST_CASES + ) + @mock.patch.dict( + os.environ, {'MJX_WARP_ENABLED': 'true', 'MJX_C_DEFAULT_ENABLED': 'true'} + ) + def test_resolve_without_device( + self, + default_device_str, + backend_impl_str, + expected, + ): + """Tests various combinations of jax.default_device and backend impls.""" + default_devices = { + 'cpu': [self.mock_cpu], + 'gpu-nvidia': [self.mock_nvidia_gpu, self.mock_cpu], + 'gpu-notnvidia': [self.mock_other_gpu, self.mock_cpu], + 'tpu': [self.mock_tpu, self.mock_cpu], + }[default_device_str] + + def devices_side_effect(backend=None): + if backend == 'cpu': + return [self.mock_cpu] # CPU is always available + if backend == 'gpu' and default_device_str == 'gpu-notnvidia': + return [self.mock_other_gpu] + if backend == 'gpu' and default_device_str == 'gpu-nvidia': + return [self.mock_nvidia_gpu] + if backend == 'cuda' and default_device_str == 'gpu-nvidia': + return [self.mock_nvidia_gpu] + if backend == 'tpu' and default_device_str == 'tpu': + return [self.mock_tpu] + if backend is None: + return default_devices + if backend == 'cuda': + raise RuntimeError('cuda backend not supported') + raise AssertionError('jax.devices error') + + self.mock_jax_devices.side_effect = devices_side_effect + default_device_side_effect_str = { + 'cpu': 'cpu', + 'gpu-nvidia': 'gpu', + 'gpu-notnvidia': 'gpu', + 'tpu': 'tpu', + }[default_device_str] + self.mock_default_backend.side_effect = ( + lambda: default_device_side_effect_str + ) + + expected_device, expected_backend_impl = expected + if ( + expected_backend_impl == 'error' + and default_device_str != 'gpu-nvidia' + and backend_impl_str == 'warp' + ): + with self.assertRaisesRegex(RuntimeError, 'cuda backend not supported'): + mjx_io._resolve_backend_impl_and_device( + backend_impl=backend_impl_str, device=None + ) + return + + if expected_backend_impl == 'error': + with self.assertRaises(AssertionError): + mjx_io._resolve_backend_impl_and_device( + backend_impl=backend_impl_str, device=None + ) + return + + actual_backend_impl, actual_device = ( + mjx_io._resolve_backend_impl_and_device( + backend_impl=backend_impl_str, device=None + ) + ) + + self.assertEqual(actual_backend_impl, expected_backend_impl) + self.assertIsNotNone(actual_device) + self.assertEqual(actual_device.platform, expected_device) + + @mock.patch.dict(os.environ, {'MJX_WARP_ENABLED': 'false'}) + def test_resolve_warp_disabled(self): + """Tests behavior when MJX_WARP_ENABLED is false.""" + self.mock_jax_devices.side_effect = lambda backend=None: ( + [self.mock_nvidia_gpu, self.mock_cpu] + if backend is None + else ([self.mock_nvidia_gpu] if backend == 'gpu' else [self.mock_cpu]) + ) + self.mock_default_backend.side_effect = lambda: 'gpu' + + # Default to JAX instead of WARP on NVIDIA GPU. + backend_impl, device = mjx_io._resolve_backend_impl_and_device( + backend_impl=None, device=None + ) + self.assertEqual(backend_impl, BackendImpl.JAX) + self.assertEqual(device.platform, 'gpu') + + # Specifying an NVIDIA GPU should still choose JAX. + backend_impl, device = mjx_io._resolve_backend_impl_and_device( + backend_impl=None, device=self.mock_nvidia_gpu + ) + self.assertEqual(backend_impl, BackendImpl.JAX) + self.assertEqual(device.platform, 'gpu') + + # Requesting warp explicitly should fail since it is disabled. + with self.assertRaises(AssertionError): + mjx_io._resolve_backend_impl_and_device( + backend_impl='warp', device=self.mock_nvidia_gpu + ) + with self.assertRaises(AssertionError): + mjx_io._resolve_backend_impl_and_device(backend_impl='warp', device=None) + + @mock.patch.dict(os.environ, {'MJX_C_DEFAULT_ENABLED': 'false'}) + def test_resolve_c_disabled(self): + """Tests behavior when MJX_C_DEFAULT_ENABLED is false.""" + # Users expect that CPU defaults to the JAX impl. But in the future, it will + # default to the C backend implementation. This test checks that + # MJX_C_DEFAULT_ENABLED=false defaults to the old behavior, until the + # migration to MJEP-15 is complete. + self.mock_jax_devices.side_effect = lambda backend=None: ([self.mock_cpu]) + self.mock_default_backend.side_effect = lambda: 'cpu' + + # Default to JAX instead of C on CPU. + backend_impl, device = mjx_io._resolve_backend_impl_and_device( + backend_impl=None, device=None + ) + self.assertEqual(backend_impl, BackendImpl.JAX) + self.assertEqual(device.platform, 'cpu') + + # Specifing CPU should still choose JAX. + backend_impl, device = mjx_io._resolve_backend_impl_and_device( + backend_impl=None, device=self.mock_cpu + ) + self.assertEqual(backend_impl, BackendImpl.JAX) + self.assertEqual(device.platform, 'cpu') + + # Specifying C should choose C! + backend_impl, device = mjx_io._resolve_backend_impl_and_device( + backend_impl='c', device=None + ) + self.assertEqual(backend_impl, BackendImpl.C) + self.assertEqual(device.platform, 'cpu') + + backend_impl, device = mjx_io._resolve_backend_impl_and_device( + backend_impl='c', device=self.mock_cpu + ) + self.assertEqual(backend_impl, BackendImpl.C) + self.assertEqual(device.platform, 'cpu') if __name__ == '__main__': diff --git a/mjx/mujoco/mjx/_src/passive.py b/mjx/mujoco/mjx/_src/passive.py index c289690f..b4a8b674 100644 --- a/mjx/mujoco/mjx/_src/passive.py +++ b/mjx/mujoco/mjx/_src/passive.py @@ -23,9 +23,12 @@ from mujoco.mjx._src import scan from mujoco.mjx._src import support # pylint: disable=g-importing-member from mujoco.mjx._src.types import Data +from mujoco.mjx._src.types import DataJAX from mujoco.mjx._src.types import DisableBit from mujoco.mjx._src.types import JointType from mujoco.mjx._src.types import Model +from mujoco.mjx._src.types import ModelJAX +from mujoco.mjx._src.types import OptionJAX # pylint: enable=g-importing-member @@ -72,11 +75,11 @@ def _spring_damper(m: Model, d: Data) -> jax.Array: qfrc -= m.dof_damping * d.qvel # tendon-level spring-dampers - below, above = m.tendon_lengthspring.T - d.ten_length + below, above = m.tendon_lengthspring.T - d._impl.ten_length frc_spring = jp.where(below > 0, m.tendon_stiffness * below, 0) frc_spring = jp.where(above < 0, m.tendon_stiffness * above, frc_spring) - frc_damper = -m.tendon_damping * d.ten_velocity - qfrc += d.ten_J.T @ (frc_spring + frc_damper) + frc_damper = -m.tendon_damping * d._impl.ten_velocity + qfrc += d._impl.ten_J.T @ (frc_spring + frc_damper) return qfrc @@ -113,6 +116,9 @@ def _fluid(m: Model, d: Data) -> jax.Array: def passive(m: Model, d: Data) -> Data: """Adds all passive forces.""" + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('passive requires JAX backend implementation.') + if m.opt.disableflags & DisableBit.PASSIVE: return d.replace(qfrc_passive=jp.zeros(m.nv), qfrc_gravcomp=jp.zeros(m.nv)) @@ -124,7 +130,7 @@ def passive(m: Model, d: Data) -> Data: # add gravcomp unless added via actuators qfrc_passive += qfrc_gravcomp * (1 - m.jnt_actgravcomp[m.dof_jntid]) - if m.opt.has_fluid_params: + if m.opt.has_fluid_params: # pytype: disable=attribute-error qfrc_passive += _fluid(m, d) d = d.replace(qfrc_passive=qfrc_passive, qfrc_gravcomp=qfrc_gravcomp) diff --git a/mjx/mujoco/mjx/_src/sensor.py b/mjx/mujoco/mjx/_src/sensor.py index 24471ccc..41751527 100644 --- a/mjx/mujoco/mjx/_src/sensor.py +++ b/mjx/mujoco/mjx/_src/sensor.py @@ -22,9 +22,12 @@ from mujoco.mjx._src import math from mujoco.mjx._src import ray from mujoco.mjx._src import smooth from mujoco.mjx._src import support +from mujoco.mjx._src.types import BackendImpl from mujoco.mjx._src.types import Data +from mujoco.mjx._src.types import DataJAX from mujoco.mjx._src.types import DisableBit from mujoco.mjx._src.types import Model +from mujoco.mjx._src.types import ModelJAX from mujoco.mjx._src.types import ObjType from mujoco.mjx._src.types import SensorType from mujoco.mjx._src.types import TrnType @@ -32,7 +35,7 @@ from mujoco.mjx._src.types import TrnType import numpy as np -def apply_cutoff( +def _apply_cutoff( sensor: jax.Array, cutoff: jax.Array, data_type: int ) -> jax.Array: """Clip sensor to cutoff value.""" @@ -51,6 +54,8 @@ def apply_cutoff( def sensor_pos(m: Model, d: Data) -> Data: """Compute position-dependent sensors values.""" + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('sensor_pos requires JAX backend implementation.') if m.opt.disableflags & DisableBit.SENSOR: return d @@ -163,15 +168,15 @@ def sensor_pos(m: Model, d: Data) -> Data: sensor, _ = jax.vmap( ray.ray, in_axes=(None, None, 0, 0, None, None, None) )(m, d, site_xpos, site_mat, (), True, sid) - sensors.append(apply_cutoff(sensor, cutoffs, data_type[0])) + sensors.append(_apply_cutoff(sensor, cutoffs, data_type[0])) adrs.append(adr[idxs]) continue # avoid adding to sensors/adrs list a second time elif sensor_type == SensorType.JOINTPOS: sensor = d.qpos[m.jnt_qposadr[objid]] elif sensor_type == SensorType.TENDONPOS: - sensor = d.ten_length[objid] + sensor = d._impl.ten_length[objid] elif sensor_type == SensorType.ACTUATORPOS: - sensor = d.actuator_length[objid] + sensor = d._impl.actuator_length[objid] elif sensor_type == SensorType.BALLQUAT: jnt_qposadr = m.jnt_qposadr[objid, None] + np.arange(4)[None] quat = d.qpos[jnt_qposadr] @@ -194,7 +199,7 @@ def sensor_pos(m: Model, d: Data) -> Data: cutofft = cutoff[idxt] sensor = jax.vmap(_framepos)(xpos, xpos_ref, xmat_ref, refidt) adrt = adr[idxt, None] + np.arange(3)[None] - sensors.append(apply_cutoff(sensor, cutofft, data_type[0]).reshape(-1)) + sensors.append(_apply_cutoff(sensor, cutofft, data_type[0]).reshape(-1)) adrs.append(adrt.reshape(-1)) continue # avoid adding to sensors/adrs list a second time elif sensor_type in frame_axis: @@ -214,7 +219,7 @@ def sensor_pos(m: Model, d: Data) -> Data: cutofft = cutoff[idxt] sensor = jax.vmap(_frameaxis)(xmat, xmat_ref, refidt) adrt = adr[idxt, None] + np.arange(3)[None] - sensors.append(apply_cutoff(sensor, cutofft, data_type[0]).reshape(-1)) + sensors.append(_apply_cutoff(sensor, cutofft, data_type[0]).reshape(-1)) adrs.append(adrt.reshape(-1)) continue # avoid adding to sensors/adrs list a second time elif sensor_type == SensorType.FRAMEQUAT: @@ -255,7 +260,7 @@ def sensor_pos(m: Model, d: Data) -> Data: ) )(quat, refquat, refidt) adrt = adr[idxt, None] + np.arange(4)[None] - sensors.append(apply_cutoff(sensor, cutofft, data_type[0]).reshape(-1)) + sensors.append(_apply_cutoff(sensor, cutofft, data_type[0]).reshape(-1)) adrs.append(adrt.reshape(-1)) continue # avoid adding to sensors/adrs list a second time elif sensor_type == SensorType.SUBTREECOM: @@ -267,7 +272,7 @@ def sensor_pos(m: Model, d: Data) -> Data: # TODO(taylorhowell): raise error after adding sensor check to io.py continue # unsupported sensor type - sensors.append(apply_cutoff(sensor, cutoff, data_type[0]).reshape(-1)) + sensors.append(_apply_cutoff(sensor, cutoff, data_type[0]).reshape(-1)) adrs.append(adr) if not adrs: @@ -282,6 +287,8 @@ def sensor_pos(m: Model, d: Data) -> Data: def sensor_vel(m: Model, d: Data) -> Data: """Compute velocity-dependent sensors values.""" + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('sensor_vel requires JAX backend implementation.') if m.opt.disableflags & DisableBit.SENSOR: return d @@ -333,9 +340,9 @@ def sensor_vel(m: Model, d: Data) -> Data: elif sensor_type == SensorType.JOINTVEL: sensor = d.qvel[m.jnt_dofadr[objid]] elif sensor_type == SensorType.TENDONVEL: - sensor = d.ten_velocity[objid] + sensor = d._impl.ten_velocity[objid] elif sensor_type == SensorType.ACTUATORVEL: - sensor = d.actuator_velocity[objid] + sensor = d._impl.actuator_velocity[objid] elif sensor_type == SensorType.BALLANGVEL: jnt_dotadr = m.jnt_dofadr[objid, None] + np.arange(3)[None] sensor = d.qvel[jnt_dotadr] @@ -393,20 +400,20 @@ def sensor_vel(m: Model, d: Data) -> Data: adrt = adr[idxt, None] + np.arange(3)[None] - sensors.append(apply_cutoff(sensor, cutofft, data_type[0]).reshape(-1)) + sensors.append(_apply_cutoff(sensor, cutofft, data_type[0]).reshape(-1)) adrs.append(adrt.reshape(-1)) continue # avoid adding to sensors/adrs list a second time elif sensor_type == SensorType.SUBTREELINVEL: - sensor = d.subtree_linvel[objid] + sensor = d._impl.subtree_linvel[objid] adr = (adr[:, None] + np.arange(3)[None]).reshape(-1) elif sensor_type == SensorType.SUBTREEANGMOM: - sensor = d.subtree_angmom[objid] + sensor = d._impl.subtree_angmom[objid] adr = (adr[:, None] + np.arange(3)[None]).reshape(-1) else: # TODO(taylorhowell): raise error after adding sensor check to io.py continue # unsupported sensor type - sensors.append(apply_cutoff(sensor, cutoff, data_type[0]).reshape(-1)) + sensors.append(_apply_cutoff(sensor, cutoff, data_type[0]).reshape(-1)) adrs.append(adr) if not adrs: @@ -421,6 +428,8 @@ def sensor_vel(m: Model, d: Data) -> Data: def sensor_acc(m: Model, d: Data) -> Data: """Compute acceleration/force-dependent sensors values.""" + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('sensor_acc requires JAX backend implementation.') if m.opt.disableflags & DisableBit.SENSOR: return d @@ -460,14 +469,14 @@ def sensor_acc(m: Model, d: Data) -> Data: # compute contact forces forces = [] condim_ids = [] - for dim in set(d.contact.dim): + for dim in set(d._impl.contact.dim): force, condim_id = support.contact_force_dim(m, d, dim) forces.append(force) condim_ids.append(condim_id) forces = jp.concatenate(forces)[np.argsort(np.concatenate(condim_ids))] # get bodies of contact geoms - conbody = jp.array(m.geom_bodyid)[d.contact.geom] + conbody = jp.array(m.geom_bodyid)[d._impl.contact.geom] # get site information site_bodyid = m.site_bodyid[objid] @@ -477,12 +486,14 @@ def sensor_acc(m: Model, d: Data) -> Data: site_type = m.site_type[objid] conbody0 = site_bodyid[:, None] == conbody[:, 0] conbody1 = site_bodyid[:, None] == conbody[:, 1] - contacts = (d.contact.efc_address >= 0)[None] & (conbody0 | conbody1) + contacts = (d._impl.contact.efc_address >= 0)[None] & ( + conbody0 | conbody1 + ) # compute conray, flip if second body conray = jax.vmap( lambda frame, force: math.normalize(frame[0] * force[0]) - )(d.contact.frame, forces) + )(d._impl.contact.frame, forces) conray = jp.where(conbody1[..., None], -conray, conray) # compute distance, mapping over sites and contacts @@ -504,7 +515,7 @@ def sensor_acc(m: Model, d: Data) -> Data: site_xpos[dist_id_site], site_xmat[dist_id_site], st, - d.contact.pos, + d._impl.contact.pos, conray[dist_id_site], ) dist.append(jp.where(jp.isinf(dist_site), 0, dist_site)) @@ -526,14 +537,14 @@ def sensor_acc(m: Model, d: Data) -> Data: bodyid = m.site_bodyid[objid] rot = d.site_xmat[objid] cvel = d.cvel[bodyid] - cacc = d.cacc[bodyid] + cacc = d._impl.cacc[bodyid] dif = d.site_xpos[objid] - d.subtree_com[m.body_rootid[bodyid]] sensor = _accelerometer(cvel, cacc, dif, rot) adr = (adr[:, None] + np.arange(3)[None]).reshape(-1) elif sensor_type == SensorType.FORCE: bodyid = m.site_bodyid[objid] - cfrc_int = d.cfrc_int[bodyid] + cfrc_int = d._impl.cfrc_int[bodyid] site_xmat = d.site_xmat[objid] sensor = jax.vmap(lambda mat, vec: mat.T @ vec)( site_xmat, cfrc_int[:, 3:] @@ -542,7 +553,7 @@ def sensor_acc(m: Model, d: Data) -> Data: elif sensor_type == SensorType.TORQUE: bodyid = m.site_bodyid[objid] rootid = m.body_rootid[bodyid] - cfrc_int = d.cfrc_int[bodyid] + cfrc_int = d._impl.cfrc_int[bodyid] site_xmat = d.site_xmat[objid] dif = d.site_xpos[objid] - d.subtree_com[rootid] sensor = jax.vmap( @@ -571,7 +582,7 @@ def sensor_acc(m: Model, d: Data) -> Data: pos, bodyid = objtype_data[ot] pos = pos[objidt] bodyid = bodyid[objidt] - cacc = d.cacc[bodyid] + cacc = d._impl.cacc[bodyid] if sensor_type == SensorType.FRAMELINACC: @@ -601,7 +612,7 @@ def sensor_acc(m: Model, d: Data) -> Data: # TODO(taylorhowell): raise error after adding sensor check to io.py continue # unsupported sensor type - sensors.append(apply_cutoff(sensor, cutoff, data_type[0]).reshape(-1)) + sensors.append(_apply_cutoff(sensor, cutoff, data_type[0]).reshape(-1)) adrs.append(adr) if not adrs: diff --git a/mjx/mujoco/mjx/_src/sensor_test.py b/mjx/mujoco/mjx/_src/sensor_test.py index fce6225c..cab50779 100644 --- a/mjx/mujoco/mjx/_src/sensor_test.py +++ b/mjx/mujoco/mjx/_src/sensor_test.py @@ -64,14 +64,14 @@ class SensorTest(parameterized.TestCase): mujoco.mj_forward(m, d) mx = mjx.put_model(m) - dx = mjx.put_data(m, d).replace( - sensordata=jp.zeros_like(d.sensordata), - subtree_linvel=jp.zeros_like(d.subtree_linvel), - subtree_angmom=jp.zeros_like(d.subtree_angmom), - cacc=jp.zeros_like(d.cacc), - cfrc_int=jp.zeros_like(d.cfrc_int), - cfrc_ext=jp.zeros_like(d.cfrc_ext), - ) + dx = mjx.put_data(m, d).tree_replace({ + 'sensordata': jp.zeros_like(d.sensordata), + '_impl.subtree_linvel': jp.zeros_like(d.subtree_linvel), + '_impl.subtree_angmom': jp.zeros_like(d.subtree_angmom), + '_impl.cacc': jp.zeros_like(d.cacc), + '_impl.cfrc_int': jp.zeros_like(d.cfrc_int), + '_impl.cfrc_ext': jp.zeros_like(d.cfrc_ext), + }) dx = jax.jit(mjx.sensor_pos)(mx, dx) dx = jax.jit(mjx.sensor_vel)(mx, dx) dx = jax.jit(mjx.sensor_acc)(mx, dx) diff --git a/mjx/mujoco/mjx/_src/smooth.py b/mjx/mujoco/mjx/_src/smooth.py index 92e46702..f87c2ed3 100644 --- a/mjx/mujoco/mjx/_src/smooth.py +++ b/mjx/mujoco/mjx/_src/smooth.py @@ -23,10 +23,12 @@ from mujoco.mjx._src import support # pylint: disable=g-importing-member from mujoco.mjx._src.types import CamLightType from mujoco.mjx._src.types import Data +from mujoco.mjx._src.types import DataJAX from mujoco.mjx._src.types import DisableBit from mujoco.mjx._src.types import EqType from mujoco.mjx._src.types import JointType from mujoco.mjx._src.types import Model +from mujoco.mjx._src.types import ModelJAX from mujoco.mjx._src.types import TrnType from mujoco.mjx._src.types import WrapType # pylint: enable=g-importing-member @@ -35,7 +37,6 @@ import numpy as np def kinematics(m: Model, d: Data) -> Data: """Converts position/velocity from generalized coordinates to maximal.""" - def fn(carry, jnt_typs, jnt_pos, jnt_axis, qpos, qpos0, pos, quat): # calculate joint anchors, axes, body pos and quat in global frame # also normalize qpos while we're at it @@ -131,6 +132,8 @@ def kinematics(m: Model, d: Data) -> Data: def com_pos(m: Model, d: Data) -> Data: """Maps inertias and motion dofs to global frame centered at subtree-CoM.""" + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('com_pos requires JAX backend implementation.') # calculate center of mass of each subtree def subtree_sum(carry, xipos, body_mass): @@ -162,7 +165,7 @@ def com_pos(m: Model, d: Data) -> Data: root_com = subtree_com[m.body_rootid] offset = d.xipos - root_com cinert = inert_com(m.body_inertia, d.ximat, offset, m.body_mass) - d = d.replace(cinert=cinert) + d = d.tree_replace({'_impl.cinert': cinert}) # map motion dofs to global frame centered at subtree_com def cdof_fn(jnt_typs, root_com, xmat, xanchor, xaxis): @@ -201,13 +204,16 @@ def com_pos(m: Model, d: Data) -> Data: d.xanchor, d.xaxis, ) - d = d.replace(cdof=cdof) + d = d.tree_replace({'_impl.cdof': cdof}) return d def camlight(m: Model, d: Data) -> Data: """Computes camera and light positions and orientations.""" + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('camlight requires JAX backend implementation.') + if m.ncam == 0: return d.replace(cam_xpos=jp.zeros((0, 3)), cam_xmat=jp.zeros((0, 3, 3))) @@ -278,32 +284,33 @@ def camlight(m: Model, d: Data) -> Data: def crb(m: Model, d: Data) -> Data: """Runs composite rigid body inertia algorithm.""" + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('crb requires JAX backend implementation.') def crb_fn(crb_child, crb_body): if crb_child is not None: crb_body += crb_child return crb_body - crb_body = scan.body_tree(m, crb_fn, 'b', 'b', d.cinert, reverse=True) + crb_body = scan.body_tree(m, crb_fn, 'b', 'b', d._impl.cinert, reverse=True) crb_body = crb_body.at[0].set(0.0) - d = d.replace(crb=crb_body) + d = d.tree_replace({'_impl.crb': crb_body}) crb_dof = jp.take(crb_body, jp.array(m.dof_bodyid), axis=0) - crb_cdof = jax.vmap(math.inert_mul)(crb_dof, d.cdof) - qm = support.make_m(m, crb_cdof, d.cdof, m.dof_armature) - d = d.replace(qM=qm) - if support.is_sparse(m) and d._qM_sparse.size > 0: # pylint: disable=protected-access - d = d.replace(_qM_sparse=qm) - + crb_cdof = jax.vmap(math.inert_mul)(crb_dof, d._impl.cdof) + qm = support.make_m(m, crb_cdof, d._impl.cdof, m.dof_armature) + d = d.tree_replace({'_impl.qM': qm}) return d def factor_m(m: Model, d: Data) -> Data: """Gets factorizaton of inertia-like matrix M, assumed spd.""" + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('factor_m requires JAX backend implementation.') if not support.is_sparse(m): - qh, _ = jax.scipy.linalg.cho_factor(d.qM) - d = d.replace(qLD=qh) + qh, _ = jax.scipy.linalg.cho_factor(d._impl.qM) + d = d.tree_replace({'_impl.qLD': qh}) return d # build up indices for where we will do backwards updates over qLD @@ -325,7 +332,7 @@ def factor_m(m: Model, d: Data) -> Data: (out_beg, out_end, madr_d, madr_ij) ) - qld = d.qM + qld = d._impl.qM for _, updates in sorted(updates.items(), reverse=True): # combine the updates into one update batch (per depth level) @@ -353,20 +360,17 @@ def factor_m(m: Model, d: Data) -> Data: qld_diag = qld[m.dof_Madr] qld = (qld / qld[jp.array(madr_ds)]).at[m.dof_Madr].set(qld_diag) - d = d.replace(qLD=qld, qLDiagInv=1 / qld_diag) - if d._qLD_sparse.size > 0: # pylint: disable=protected-access - d = d.replace(_qLD_sparse=d.qLD) - if d._qLDiagInv_sparse.size > 0: # pylint: disable=protected-access - d = d.replace(_qLDiagInv_sparse=d.qLDiagInv) - + d = d.tree_replace({'_impl.qLD': qld, '_impl.qLDiagInv': 1 / qld_diag}) return d def solve_m(m: Model, d: Data, x: jax.Array) -> jax.Array: """Computes sparse backsubstitution: x = inv(L'*D*L)*y .""" + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('solve_m requires JAX backend implementation.') if not support.is_sparse(m): - return jax.scipy.linalg.cho_solve((d.qLD, False), x) + return jax.scipy.linalg.cho_solve((d._impl.qLD, False), x) depth = [] for i in range(m.nv): @@ -385,21 +389,23 @@ def solve_m(m: Model, d: Data, x: jax.Array) -> jax.Array: # x <- inv(L') * x for _, vals in sorted(updates_j.items(), reverse=True): j, madr_ij, i = np.array(vals).T - x = x.at[j].add(-d.qLD[madr_ij] * x[i]) + x = x.at[j].add(-d._impl.qLD[madr_ij] * x[i]) # x <- inv(D) * x - x = x * d.qLDiagInv + x = x * d._impl.qLDiagInv # x <- inv(L) * x for _, vals in sorted(updates_i.items()): i, madr_ij, j = np.array(vals).T - x = x.at[i].add(-d.qLD[madr_ij] * x[j]) + x = x.at[i].add(-d._impl.qLD[madr_ij] * x[j]) return x def com_vel(m: Model, d: Data) -> Data: """Computes cvel, cdof_dot.""" + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('com_vel requires JAX backend implementation.') # forward scan down tree: accumulate link center of mass velocity def fn(parent, jnt_typs, cdof, qvel): @@ -431,17 +437,19 @@ def com_vel(m: Model, d: Data) -> Data: 'jvv', 'bv', m.jnt_type, - d.cdof, + d._impl.cdof, d.qvel, ) - d = d.replace(cvel=cvel, cdof_dot=cdof_dot) + d = d.tree_replace({'cvel': cvel, '_impl.cdof_dot': cdof_dot}) return d def subtree_vel(m: Model, d: Data) -> Data: """Subtree linear velocity and angular momentum.""" + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('subtree_vel requires JAX backend implementation.') # bodywise quantities def _forward(cvel, xipos, ximat, subtree_com_root, mass, inertia): @@ -529,7 +537,10 @@ def subtree_vel(m: Model, d: Data) -> Data: reverse=True, ) - return d.replace(subtree_linvel=subtree_linvel, subtree_angmom=subtree_angmom) + return d.tree_replace({ + '_impl.subtree_linvel': subtree_linvel, + '_impl.subtree_angmom': subtree_angmom, + }) def rne(m: Model, d: Data, flg_acc: bool = False) -> Data: @@ -537,6 +548,8 @@ def rne(m: Model, d: Data, flg_acc: bool = False) -> Data: flg_acc=False removes inertial term. """ + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('rne requires JAX backend implementation.') # forward scan over tree: accumulate link center of mass acceleration def cacc_fn(cacc, cdof_dot, qvel, cdof, qacc): @@ -555,7 +568,7 @@ def rne(m: Model, d: Data, flg_acc: bool = False) -> Data: return cacc cacc = scan.body_tree( - m, cacc_fn, 'vvvv', 'b', d.cdof_dot, d.qvel, d.cdof, d.qacc + m, cacc_fn, 'vvvv', 'b', d._impl.cdof_dot, d.qvel, d._impl.cdof, d.qacc ) def frc(cinert, cacc, cvel): @@ -564,7 +577,7 @@ def rne(m: Model, d: Data, flg_acc: bool = False) -> Data: return frc - loc_cfrc = jax.vmap(frc)(d.cinert, cacc, d.cvel) + loc_cfrc = jax.vmap(frc)(d._impl.cinert, cacc, d.cvel) # backward scan up tree: accumulate body forces def cfrc_fn(cfrc_child, cfrc): @@ -573,7 +586,7 @@ def rne(m: Model, d: Data, flg_acc: bool = False) -> Data: return cfrc cfrc = scan.body_tree(m, cfrc_fn, 'b', 'b', loc_cfrc, reverse=True) - qfrc_bias = jax.vmap(jp.dot)(d.cdof, cfrc[jp.array(m.dof_bodyid)]) + qfrc_bias = jax.vmap(jp.dot)(d._impl.cdof, cfrc[jp.array(m.dof_bodyid)]) d = d.replace(qfrc_bias=qfrc_bias) @@ -582,6 +595,8 @@ def rne(m: Model, d: Data, flg_acc: bool = False) -> Data: def rne_postconstraint(m: Model, d: Data) -> Data: """RNE with complete data: compute cacc, cfrc_ext, cfrc_int.""" + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('rne_postconstraint requires JAX backend implementation.') def _transform_force(frc, offset): force, torque = jp.split(frc, 2) @@ -602,7 +617,7 @@ def rne_postconstraint(m: Model, d: Data) -> Data: # compute contact forces for each condim forces = [] condim_idx = [] - for dim in set(d.contact.dim): + for dim in set(d._impl.contact.dim): force, idx = support.contact_force_dim(m, d, dim) forces.append(force) condim_idx.append(idx) @@ -629,10 +644,10 @@ def rne_postconstraint(m: Model, d: Data) -> Data: ) condim_idx = jp.concatenate(condim_idx) - frame = d.contact.frame[condim_idx] - pos = d.contact.pos[condim_idx] - id1 = jp.array(m.geom_bodyid)[d.contact.geom[condim_idx, 0]] - id2 = jp.array(m.geom_bodyid)[d.contact.geom[condim_idx, 1]] + frame = d._impl.contact.frame[condim_idx] + pos = d._impl.contact.pos[condim_idx] + id1 = jp.array(m.geom_bodyid)[d._impl.contact.geom[condim_idx, 0]] + id2 = jp.array(m.geom_bodyid)[d._impl.contact.geom[condim_idx, 1]] com1 = d.subtree_com[jp.array(m.body_rootid)][id1] com2 = d.subtree_com[jp.array(m.body_rootid)][id2] @@ -668,8 +683,8 @@ def rne_postconstraint(m: Model, d: Data) -> Data: ) # cacc = cacc_parent + cdofdot * qvel + cdof * qacc - cacc_vel = d.cdof_dot.T @ (mask * d.qvel) - cacc_acc = d.cdof.T @ (mask * d.qacc) + cacc_vel = d._impl.cdof_dot.T @ (mask * d.qvel) + cacc_acc = d._impl.cdof.T @ (mask * d.qacc) cacc = cacc_parent + cacc_vel + cacc_acc # cfrc_body = cinert * cacc + cvel x (cinert * cvel) @@ -687,7 +702,7 @@ def rne_postconstraint(m: Model, d: Data) -> Data: 'bbbbb', 'bb', cfrc_ext, - d.cinert, + d._impl.cinert, d.cvel, jp.array(m.body_dofadr), jp.array(m.body_dofnum), @@ -704,11 +719,18 @@ def rne_postconstraint(m: Model, d: Data) -> Data: ) # update data - return d.replace(cacc=cacc, cfrc_int=cfrc_int, cfrc_ext=cfrc_ext) + return d.tree_replace({ + '_impl.cacc': cacc, + '_impl.cfrc_int': cfrc_int, + '_impl.cfrc_ext': cfrc_ext, + }) def tendon(m: Model, d: Data) -> Data: """Computes tendon lengths and moments.""" + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('tendon requires JAX backend implementation.') + if not m.ntendon: return d @@ -836,8 +858,8 @@ def tendon(m: Model, d: Data) -> Data: # wrap inside # TODO(taylorhowell): check that is_wrap_inside is consistent with # site and geom relative positions - (wrap_inside_id,) = np.nonzero(m.is_wrap_inside) - (wrap_outside_id,) = np.nonzero(~m.is_wrap_inside) + (wrap_inside_id,) = np.nonzero(m._impl.is_wrap_inside) + (wrap_outside_id,) = np.nonzero(~m._impl.is_wrap_inside) # compute geom wrap length and connect points (if wrap occurs) v_wrap = jax.vmap( @@ -853,9 +875,9 @@ def tendon(m: Model, d: Data) -> Data: has_sidesite[wrap_inside_id], is_sphere[wrap_inside_id], True, - m.wrap_inside_maxiter, - m.wrap_inside_tolerance, - m.wrap_inside_z_init, + m._impl.wrap_inside_maxiter, + m._impl.wrap_inside_tolerance, + m._impl.wrap_inside_z_init, ) lengths_outside, pnt0_outside, pnt1_outside = v_wrap( @@ -868,9 +890,9 @@ def tendon(m: Model, d: Data) -> Data: has_sidesite[wrap_outside_id], is_sphere[wrap_outside_id], False, - m.wrap_inside_maxiter, - m.wrap_inside_tolerance, - m.wrap_inside_z_init, + m._impl.wrap_inside_maxiter, + m._impl.wrap_inside_tolerance, + m._impl.wrap_inside_z_init, ) wrap_id = np.argsort(np.concatenate([wrap_inside_id, wrap_outside_id])) @@ -952,12 +974,14 @@ def tendon(m: Model, d: Data) -> Data: ) # assemble length and moment - ten_length = jp.zeros_like(d.ten_length).at[tendon_id_jnt].set(length_jnt) + ten_length = ( + jp.zeros_like(d._impl.ten_length).at[tendon_id_jnt].set(length_jnt) + ) ten_length = ten_length.at[tendon_id_site].add(length_site) ten_length = ten_length.at[tendon_id_geom].add(length_geom) ten_moment = ( - jp.zeros_like(d.ten_J) + jp.zeros_like(d._impl.ten_J) .at[adr_moment_jnt, dofadr_moment_jnt] .set(moment_jnt) ) @@ -1020,14 +1044,14 @@ def tendon(m: Model, d: Data) -> Data: [wrap_obj[sort], jp.zeros(2 * m.nwrap - count, dtype=int)] ).reshape((m.nwrap, 2)) - return d.replace( - ten_length=ten_length, - ten_J=ten_moment, - ten_wrapadr=jp.array(ten_wrapadr, dtype=int), - ten_wrapnum=jp.array(ten_wrapnum, dtype=int), - wrap_xpos=wrap_xpos, - wrap_obj=jp.array(wrap_obj, dtype=int), - ) + return d.tree_replace({ + '_impl.ten_length': ten_length, + '_impl.ten_J': ten_moment, + '_impl.ten_wrapadr': jp.array(ten_wrapadr, dtype=int), + '_impl.ten_wrapnum': jp.array(ten_wrapnum, dtype=int), + '_impl.wrap_xpos': wrap_xpos, + '_impl.wrap_obj': jp.array(wrap_obj, dtype=int), + }) def _site_dof_mask(m: Model) -> np.ndarray: @@ -1061,6 +1085,9 @@ def _site_dof_mask(m: Model) -> np.ndarray: def transmission(m: Model, d: Data) -> Data: """Computes actuator/transmission lengths and moments.""" + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('transmission requires JAX backend implementation.') + if not m.nu: return d @@ -1120,8 +1147,8 @@ def transmission(m: Model, d: Data) -> Data: wrench = jp.concatenate((frame_xmat @ gear[:3], frame_xmat @ gear[3:])) moment = jac @ wrench elif trntype == TrnType.TENDON: - length = d.ten_length[trnid[0]] * gear[:1] - moment = d.ten_J[trnid[0]] * gear[0] + length = d._impl.ten_length[trnid[0]] * gear[:1] + moment = d._impl.ten_J[trnid[0]] * gear[0] else: raise RuntimeError(f'unrecognized trntype: {TrnType(trntype)}') @@ -1153,5 +1180,7 @@ def transmission(m: Model, d: Data) -> Data: length = length.reshape((m.nu,)) moment = moment.reshape((m.nu, m.nv)) - d = d.replace(actuator_length=length, actuator_moment=moment) + d = d.tree_replace( + {'_impl.actuator_length': length, '_impl.actuator_moment': moment} + ) return d diff --git a/mjx/mujoco/mjx/_src/smooth_test.py b/mjx/mujoco/mjx/_src/smooth_test.py index eca05068..4f182a75 100644 --- a/mjx/mujoco/mjx/_src/smooth_test.py +++ b/mjx/mujoco/mjx/_src/smooth_test.py @@ -20,7 +20,7 @@ import jax import mujoco from mujoco import mjx from mujoco.mjx._src import test_util -from mujoco.mjx._src.types import ConeType +from mujoco.mjx._src.types import ConeType # pylint: disable=g-importing-member import numpy as np # tolerance for difference between MuJoCo and MJX smooth calculations - mostly @@ -78,30 +78,27 @@ class SmoothTest(absltest.TestCase): # com_pos dx = jax.jit(mjx.com_pos)(mx, mjx.put_data(m, d)) _assert_attr_eq(d, dx, 'subtree_com') - _assert_attr_eq(d, dx, 'cinert') - _assert_attr_eq(d, dx, 'cdof') + _assert_attr_eq(d, dx._impl, 'cinert') + _assert_attr_eq(d, dx._impl, 'cdof') # camlight dx = jax.jit(mjx.camlight)(mx, mjx.put_data(m, d)) _assert_attr_eq(d, dx, 'cam_xpos') _assert_eq(d.cam_xmat.reshape((-1, 3, 3)), dx.cam_xmat, 'cam_xmat') # crb dx = jax.jit(mjx.crb)(mx, mjx.put_data(m, d)) - _assert_attr_eq(d, dx, 'crb') - _assert_attr_eq(d, dx, 'qM') - _assert_eq(dx._qM_sparse, np.zeros(0), '_qM_sparse') + _assert_attr_eq(d, dx._impl, 'crb') + _assert_attr_eq(d, dx._impl, 'qM') # factor_m dx = jax.jit(mjx.factor_m)(mx, mjx.put_data(m, d)) qLDLegacy = np.zeros(mx.nM) # pylint:disable=invalid-name for i in range(m.nM): qLDLegacy[d.mapM2M[i]] = d.qLD[i] - _assert_eq(qLDLegacy, dx.qLD, 'qLD') - _assert_attr_eq(d, dx, 'qLDiagInv') - _assert_eq(dx._qLD_sparse, np.zeros(0), '_qLD_sparse') - _assert_eq(dx._qLDiagInv_sparse, np.zeros(0), '_qLDiagInv_sparse') + _assert_eq(qLDLegacy, dx._impl.qLD, 'qLD') + _assert_attr_eq(d, dx._impl, 'qLDiagInv') # com_vel dx = jax.jit(mjx.com_vel)(mx, mjx.put_data(m, d)) _assert_attr_eq(d, dx, 'cvel') - _assert_attr_eq(d, dx, 'cdof_dot') + _assert_attr_eq(d, dx._impl, 'cdof_dot') # rne dx = jax.jit(mjx.rne)(mx, mjx.put_data(m, d)) _assert_attr_eq(d, dx, 'qfrc_bias') @@ -122,11 +119,11 @@ class SmoothTest(absltest.TestCase): mujoco.mj_forward(m, d) # tendon dx = jax.jit(mjx.tendon)(mx, mjx.put_data(m, d)) - _assert_attr_eq(d, dx, 'ten_J') - _assert_attr_eq(d, dx, 'ten_length') + _assert_attr_eq(d, dx._impl, 'ten_J') + _assert_attr_eq(d, dx._impl, 'ten_length') # transmission dx = jax.jit(mjx.transmission)(mx, dx) - _assert_attr_eq(d, dx, 'actuator_length') + _assert_attr_eq(d, dx._impl, 'actuator_length') # convert sparse actuator_moment to dense representation moment = np.zeros((m.nu, m.nv)) @@ -137,7 +134,7 @@ class SmoothTest(absltest.TestCase): d.moment_rowadr, d.moment_colind, ) - _assert_eq(moment, dx.actuator_moment, 'actuator_moment') + _assert_eq(moment, dx._impl.actuator_moment, 'actuator_moment') def test_disable_gravity(self): m = mujoco.MjModel.from_xml_string(""" @@ -197,7 +194,7 @@ class SmoothTest(absltest.TestCase): mujoco.mj_transmission(m, d) dx = jax.jit(mjx.transmission)(mx, dx) - _assert_attr_eq(d, dx, 'actuator_length') + _assert_attr_eq(d, dx._impl, 'actuator_length') # convert sparse actuator_moment to dense representation moment = np.zeros((m.nu, m.nv)) @@ -208,7 +205,7 @@ class SmoothTest(absltest.TestCase): d.moment_rowadr, d.moment_colind, ) - _assert_eq(moment, dx.actuator_moment, 'actuator_moment') + _assert_eq(moment, dx._impl.actuator_moment, 'actuator_moment') def test_subtree_vel(self): """Tests MJX subtree_vel function matches MuJoCo mj_subtreeVel.""" @@ -226,8 +223,8 @@ class SmoothTest(absltest.TestCase): mujoco.mj_subtreeVel(m, d) dx = jax.jit(mjx.subtree_vel)(mx, dx) - _assert_attr_eq(d, dx, 'subtree_linvel') - _assert_attr_eq(d, dx, 'subtree_angmom') + _assert_attr_eq(d, dx._impl, 'subtree_linvel') + _assert_attr_eq(d, dx._impl, 'subtree_angmom') class RnePostConstraintTest(parameterized.TestCase): @@ -278,9 +275,9 @@ class RnePostConstraintTest(parameterized.TestCase): mujoco.mj_rnePostConstraint(m, d) dx = jax.jit(mjx.rne_postconstraint)(mx, dx) - _assert_eq(d.cacc, dx.cacc, 'cacc') - _assert_eq(d.cfrc_ext, dx.cfrc_ext, 'cfrc_ext') - _assert_eq(d.cfrc_int, dx.cfrc_int, 'cfrc_int') + _assert_eq(d.cacc, dx._impl.cacc, 'cacc') + _assert_eq(d.cfrc_ext, dx._impl.cfrc_ext, 'cfrc_ext') + _assert_eq(d.cfrc_int, dx._impl.cfrc_int, 'cfrc_int') class TendonTest(parameterized.TestCase): @@ -312,12 +309,12 @@ class TendonTest(parameterized.TestCase): mujoco.mj_forward(m, d) dx = jax.jit(mjx.forward)(mx, dx) - _assert_eq(d.ten_length, dx.ten_length, 'ten_length') - _assert_eq(d.ten_J, dx.ten_J, 'ten_J') - _assert_eq(d.ten_wrapnum, dx.ten_wrapnum, 'ten_wrapnum') - _assert_eq(d.ten_wrapadr, dx.ten_wrapadr, 'ten_wrapadr') - _assert_eq(d.wrap_obj, dx.wrap_obj, 'wrap_obj') - _assert_eq(d.wrap_xpos, dx.wrap_xpos, 'wrap_xpos') + _assert_eq(d.ten_length, dx._impl.ten_length, 'ten_length') + _assert_eq(d.ten_J, dx._impl.ten_J, 'ten_J') + _assert_eq(d.ten_wrapnum, dx._impl.ten_wrapnum, 'ten_wrapnum') + _assert_eq(d.ten_wrapadr, dx._impl.ten_wrapadr, 'ten_wrapadr') + _assert_eq(d.wrap_obj, dx._impl.wrap_obj, 'wrap_obj') + _assert_eq(d.wrap_xpos, dx._impl.wrap_xpos, 'wrap_xpos') if __name__ == '__main__': diff --git a/mjx/mujoco/mjx/_src/solver.py b/mjx/mujoco/mjx/_src/solver.py index efb58c41..98ec3d27 100644 --- a/mjx/mujoco/mjx/_src/solver.py +++ b/mjx/mujoco/mjx/_src/solver.py @@ -24,8 +24,10 @@ from mujoco.mjx._src import support from mujoco.mjx._src.dataclasses import PyTreeNode from mujoco.mjx._src.types import ConeType from mujoco.mjx._src.types import Data +from mujoco.mjx._src.types import DataJAX from mujoco.mjx._src.types import DisableBit from mujoco.mjx._src.types import Model +from mujoco.mjx._src.types import ModelJAX from mujoco.mjx._src.types import SolverType # pylint: enable=g-importing-member @@ -73,14 +75,19 @@ class Context(PyTreeNode): @classmethod def create(cls, m: Model, d: Data, grad: bool = True) -> 'Context': - jaref = d.efc_J @ d.qacc - d.efc_aref + if not isinstance(d._impl, DataJAX): + raise ValueError( + 'Constraint context requires JAX backend implementation.' + ) + + jaref = d._impl.efc_J @ d.qacc - d._impl.efc_aref # TODO(robotics-team): determine nv at which sparse mul is faster ma = support.mul_m(m, d, d.qacc) nv_0 = jp.zeros(m.nv) fri = 0.0 if m.opt.cone == ConeType.ELLIPTIC: - friction = d.contact.friction[d.contact.dim > 1] - dim = d.contact.dim[d.contact.dim > 1] + friction = d._impl.contact.friction[d._impl.contact.dim > 1] + dim = d._impl.contact.dim[d._impl.contact.dim > 1] mu = friction[:, 0] / jp.sqrt(m.opt.impratio) fri = jp.concatenate((mu[:, None], friction), axis=1) for condim in (3, 4, 6): @@ -90,7 +97,7 @@ class Context(PyTreeNode): qacc=d.qacc, qfrc_constraint=d.qfrc_constraint, Jaref=jaref, - efc_force=d.efc_force, + efc_force=d._impl.efc_force, Ma=ma, grad=nv_0, Mgrad=nv_0, @@ -145,18 +152,20 @@ class _LSPoint(PyTreeNode): ) -> '_LSPoint': """Creates a linesearch point with first and second derivatives.""" # roughly corresponds to CGEval in mujoco/src/engine/engine_solver.c + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('LSPoint requires JAX backend implementation.') cost, deriv_0, deriv_1 = 0.0, 0.0, 0.0 quad_total = quad_gauss x = ctx.Jaref + alpha * jv - active = (x < 0).at[: d.ne + d.nf].set(True) + active = (x < 0).at[: d._impl.ne + d._impl.nf].set(True) - dof_fl, ten_fl = m.dof_hasfrictionloss, m.tendon_hasfrictionloss + dof_fl, ten_fl = m._impl.dof_hasfrictionloss, m._impl.tendon_hasfrictionloss if (dof_fl.any() or ten_fl.any()) and not ( m.opt.disableflags & DisableBit.FRICTIONLOSS ): - f = d.efc_frictionloss - r = 1.0 / (d.efc_D + (d.efc_D == 0.0) * mujoco.mjMINVAL) + f = d._impl.efc_frictionloss + r = 1.0 / (d._impl.efc_D + (d._impl.efc_D == 0.0) * mujoco.mjMINVAL) rf, z = r * f, jp.zeros_like(f) linear_neg = (x <= -rf)[:, None] linear_pos = (x >= rf)[:, None] @@ -174,13 +183,13 @@ class _LSPoint(PyTreeNode): middle_zone = (tsqr > 0) & (n < (mu * t)) & ((mu * n + t) > 0) # quadratic cost for equality, friction, limits, frictionless contacts - dim1 = d.contact.efc_address[d.contact.dim == 1] - nefl = d.ne + d.nf + d.nl + dim1 = d._impl.contact.efc_address[d._impl.contact.dim == 1] + nefl = d._impl.ne + d._impl.nf + d._impl.nl active = active.at[nefl:].set(False).at[dim1].set(active[dim1]) quad_efld = jax.vmap(jp.multiply)(quad, active) quad_total += jp.sum(quad_efld, axis=0) # elliptic bottom zone: quadratic cost - efc_elliptic = d.contact.efc_address[d.contact.dim > 1] + efc_elliptic = d._impl.contact.efc_address[d._impl.contact.dim > 1] quad_c = jax.vmap(jp.multiply)(quad[efc_elliptic], bottom_zone) quad_total += jp.sum(quad_c, axis=0) # elliptic middle zone @@ -254,17 +263,20 @@ def _update_constraint(m: Model, d: Data, ctx: Context) -> Context: Returns: context with new constraint force and costs """ + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('_update_constraint requires JAX backend implementation.') + # ne constraints are always active, nf are conditionally active, others are # non-negative constraints. - active = (ctx.Jaref < 0).at[: d.ne + d.nf].set(True) + active = (ctx.Jaref < 0).at[: d._impl.ne + d._impl.nf].set(True) - floss_force, floss_cost = jp.zeros(d.nefc), 0.0 - dof_fl, ten_fl = m.dof_hasfrictionloss, m.tendon_hasfrictionloss + floss_force, floss_cost = jp.zeros(d._impl.nefc), 0.0 + dof_fl, ten_fl = m._impl.dof_hasfrictionloss, m._impl.tendon_hasfrictionloss if (dof_fl.any() or ten_fl.any()) and not ( m.opt.disableflags & DisableBit.FRICTIONLOSS ): - f = d.efc_frictionloss - r = 1.0 / (d.efc_D + (d.efc_D == 0.0) * mujoco.mjMINVAL) + f = d._impl.efc_frictionloss + r = 1.0 / (d._impl.efc_D + (d._impl.efc_D == 0.0) * mujoco.mjMINVAL) linear_neg = (ctx.Jaref <= -r * f) * (f > 0) linear_pos = (ctx.Jaref >= r * f) * (f > 0) active = active & ~linear_neg & ~linear_pos @@ -274,13 +286,13 @@ def _update_constraint(m: Model, d: Data, ctx: Context) -> Context: floss_cost = floss_cost.sum() if m.opt.cone == ConeType.PYRAMIDAL: - efc_force = d.efc_D * -ctx.Jaref * active + floss_force - cost = 0.5 * jp.sum(d.efc_D * ctx.Jaref * ctx.Jaref * active) + efc_force = d._impl.efc_D * -ctx.Jaref * active + floss_force + cost = 0.5 * jp.sum(d._impl.efc_D * ctx.Jaref * ctx.Jaref * active) dm, u, h = 0.0, 0.0, 0.0 elif m.opt.cone == ConeType.ELLIPTIC: - friction = d.contact.friction[d.contact.dim > 1] - efc_address = d.contact.efc_address[d.contact.dim > 1] - dim = d.contact.dim[d.contact.dim > 1] + friction = d._impl.contact.friction[d._impl.contact.dim > 1] + efc_address = d._impl.contact.efc_address[d._impl.contact.dim > 1] + dim = d._impl.contact.dim[d._impl.contact.dim > 1] # to prevent out of range append zeros to ctx.Jaref slice_fn = jax.vmap( lambda x: jax.lax.dynamic_slice( @@ -297,12 +309,12 @@ def _update_constraint(m: Model, d: Data, ctx: Context) -> Context: adr_i.extend(range(addr, addr + condim)) adr_j.extend([i] * condim) active = active.at[jp.array(adr_i)].set(bottom_zone[jp.array(adr_j)]) - efc_force = d.efc_D * -ctx.Jaref * active + floss_force - cost = 0.5 * jp.sum(d.efc_D * ctx.Jaref * ctx.Jaref * active) + efc_force = d._impl.efc_D * -ctx.Jaref * active + floss_force + cost = 0.5 * jp.sum(d._impl.efc_D * ctx.Jaref * ctx.Jaref * active) # middle zone: cone middle_zone = (t > 0) & (n < (mu * t)) & ((mu * n + t) > 0) - dm = d.efc_D[efc_address] / jp.maximum( + dm = d._impl.efc_D[efc_address] / jp.maximum( mu * mu * (1 + mu * mu), mujoco.mjMINVAL ) nmt = n - mu * t @@ -339,7 +351,7 @@ def _update_constraint(m: Model, d: Data, ctx: Context) -> Context: else: raise NotImplementedError(f'unsupported cone type: {m.opt.cone}') - qfrc_constraint = d.efc_J.T @ efc_force + qfrc_constraint = d._impl.efc_J.T @ efc_force gauss = 0.5 * jp.dot(ctx.Ma - d.qfrc_smooth, ctx.qacc - d.qacc_smooth) ctx = ctx.replace( qfrc_constraint=qfrc_constraint, @@ -371,6 +383,8 @@ def _update_gradient(m: Model, d: Data, ctx: Context) -> Context: Raises: NotImplementedError: for unsupported solver type """ + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('_update_gradient requires JAX backend implementation.') grad = ctx.Ma - d.qfrc_smooth - ctx.qfrc_constraint @@ -378,16 +392,16 @@ def _update_gradient(m: Model, d: Data, ctx: Context) -> Context: mgrad = smooth.solve_m(m, d, grad) elif m.opt.solver == SolverType.NEWTON: if m.opt.cone == ConeType.ELLIPTIC: - cm = jp.diag(d.efc_D * ctx.active) - efc_address = d.contact.efc_address[d.contact.dim > 1] - dim = d.contact.dim[d.contact.dim > 1] + cm = jp.diag(d._impl.efc_D * ctx.active) + efc_address = d._impl.contact.efc_address[d._impl.contact.dim > 1] + dim = d._impl.contact.dim[d._impl.contact.dim > 1] # set efc of cone H along diagonal for i, (condim, addr) in enumerate(zip(dim, efc_address)): h_cone = ctx.h[i, :condim, :condim] cm = cm.at[addr : addr + condim, addr : addr + condim].add(h_cone) - h = d.efc_J.T @ cm @ d.efc_J + h = d._impl.efc_J.T @ cm @ d._impl.efc_J else: - h = (d.efc_J.T * d.efc_D * ctx.active) @ d.efc_J + h = (d._impl.efc_J.T * d._impl.efc_D * ctx.active) @ d._impl.efc_J h = support.full_m(m, d) + h h_ = jax.scipy.linalg.cho_factor(h) mgrad = jax.scipy.linalg.cho_solve(h_, grad) @@ -414,12 +428,15 @@ def _linesearch(m: Model, d: Data, ctx: Context) -> Context: Returns: updated context with new qacc, Ma, Jaref """ + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('_lineasearch requires JAX backend implementation.') + smag = math.norm(ctx.search) * m.stat.meaninertia * max(1, m.nv) gtol = m.opt.tolerance * m.opt.ls_tolerance * smag # compute Mv, Jv mv = support.mul_m(m, d, ctx.search) - jv = d.efc_J @ ctx.search + jv = d._impl.efc_J @ ctx.search # prepare quadratics quad_gauss = jp.stack(( @@ -428,13 +445,15 @@ def _linesearch(m: Model, d: Data, ctx: Context) -> Context: 0.5 * jp.dot(ctx.search, mv), )) quad = jp.stack((0.5 * ctx.Jaref * ctx.Jaref, jv * ctx.Jaref, 0.5 * jv * jv)) - quad = (quad * d.efc_D).T + quad = (quad * d._impl.efc_D).T uu, v0, uv, vv = 0.0, 0.0, 0.0, 0.0 if m.opt.cone == ConeType.ELLIPTIC: - mask = d.contact.dim > 1 + mask = d._impl.contact.dim > 1 # complete vector quadratic (for bottom zone) efc_con, efc_fri = [], [] - for condim, addr in zip(d.contact.dim[mask], d.contact.efc_address[mask]): + for condim, addr in zip( + d._impl.contact.dim[mask], d._impl.contact.efc_address[mask] + ): efc_con.extend([addr] * (condim - 1)) efc_fri.extend(range(addr + 1, addr + condim)) quad = quad.at[jp.array(efc_con)].add(quad[jp.array(efc_fri)]) @@ -446,7 +465,7 @@ def _linesearch(m: Model, d: Data, ctx: Context) -> Context: jp.concatenate((jv, jp.zeros(3))), (x,), (6,) ) ) - efc_elliptic = d.contact.efc_address[mask] + efc_elliptic = d._impl.contact.efc_address[mask] v = jv_fn(efc_elliptic) * ctx.fri uu = jp.sum(ctx.u[:, 1:] * ctx.u[:, 1:], axis=1) v0 = v[:, 0] @@ -571,11 +590,11 @@ def solve(m: Model, d: Data) -> Data: else: ctx = jax.lax.while_loop(cond, body, ctx) - d = d.replace( - qacc_warmstart=ctx.qacc, - qacc=ctx.qacc, - qfrc_constraint=ctx.qfrc_constraint, - efc_force=ctx.efc_force, - ) + d = d.tree_replace({ + 'qacc_warmstart': ctx.qacc, + 'qfrc_constraint': ctx.qfrc_constraint, + 'qacc': ctx.qacc, + '_impl.efc_force': ctx.efc_force, + }) return d diff --git a/mjx/mujoco/mjx/_src/solver_test.py b/mjx/mujoco/mjx/_src/solver_test.py index 6da06526..0b1b3b5a 100644 --- a/mjx/mujoco/mjx/_src/solver_test.py +++ b/mjx/mujoco/mjx/_src/solver_test.py @@ -87,8 +87,8 @@ class SolverTest(parameterized.TestCase): # MJX finds very similar solutions with the newton solver if solver_ == mujoco.mjtSolver.mjSOL_NEWTON: - nnz = dx.efc_J.any(axis=1) - _assert_eq(d.efc_force, dx.efc_force[nnz], 'efc_force') + nnz = dx._impl.efc_J.any(axis=1) + _assert_eq(d.efc_force, dx._impl.efc_force[nnz], 'efc_force') _assert_attr_eq(d, dx, 'qfrc_constraint') _assert_attr_eq(d, dx, 'qacc') @@ -108,9 +108,9 @@ class SolverTest(parameterized.TestCase): mujoco.mj_forward(m, d) mx = mjx.put_model(m) dx = jax.jit(mjx.solve)(mx, mjx.put_data(m, d)) - nnz = dx.efc_J.any(axis=1) + nnz = dx._impl.efc_J.any(axis=1) # even without warmstart, newton converges quickly - _assert_eq(d.efc_force, dx.efc_force[nnz], 'efc_force', tol=2e-4) + _assert_eq(d.efc_force, dx._impl.efc_force[nnz], 'efc_force', tol=2e-4) def test_sparse(self): """Test solver works with sparse mass matrices.""" @@ -130,8 +130,8 @@ class SolverTest(parameterized.TestCase): _assert_attr_eq(d, dx, 'qacc') _assert_attr_eq(d, dx, 'qfrc_constraint') - nnz = dx.efc_J.any(axis=1) - _assert_eq(d.efc_force, dx.efc_force[nnz], 'efc_force') + nnz = dx._impl.efc_J.any(axis=1) + _assert_eq(d.efc_force, dx._impl.efc_force[nnz], 'efc_force') def test_quad_frictionloss(self): """Test a case with quadratic frictionloss constraints.""" @@ -144,8 +144,8 @@ class SolverTest(parameterized.TestCase): _assert_attr_eq(d, dx, 'qacc') _assert_attr_eq(d, dx, 'qfrc_constraint') - nnz = dx.efc_J.any(axis=1) - _assert_eq(d.efc_force, dx.efc_force[nnz], 'efc_force') + nnz = dx._impl.efc_J.any(axis=1) + _assert_eq(d.efc_force, dx._impl.efc_force[nnz], 'efc_force') # TODO(taylorhowell): condim=1 with ConeType.ELLIPTIC @parameterized.product(condim=(3, 4, 6), cone=tuple(ConeType)) diff --git a/mjx/mujoco/mjx/_src/support.py b/mjx/mujoco/mjx/_src/support.py index 90c517ef..d68c0407 100644 --- a/mjx/mujoco/mjx/_src/support.py +++ b/mjx/mujoco/mjx/_src/support.py @@ -13,6 +13,7 @@ # limitations under the License. # ============================================================================== """Engine support functions.""" + from collections.abc import Iterable, Sequence from typing import Optional, Tuple, Union @@ -92,7 +93,7 @@ def full_m(m: Model, d: Data) -> jax.Array: """Reconstitute dense mass matrix from qM.""" if not is_sparse(m): - return d.qM + return d._impl.qM # pytype: disable=attribute-error ij = [] for i in range(m.nv): @@ -103,7 +104,7 @@ def full_m(m: Model, d: Data) -> jax.Array: i, j = (jp.array(x) for x in zip(*ij)) - mat = jp.zeros((m.nv, m.nv)).at[(i, j)].set(d.qM) + mat = jp.zeros((m.nv, m.nv)).at[(i, j)].set(d._impl.qM) # pytype: disable=attribute-error # also set upper triangular mat = mat + jp.tril(mat, -1).T @@ -115,9 +116,9 @@ def mul_m(m: Model, d: Data, vec: jax.Array) -> jax.Array: """Multiply vector by inertia matrix.""" if not is_sparse(m): - return d.qM @ vec + return d._impl.qM @ vec # pytype: disable=attribute-error - diag_mul = d.qM[jp.array(m.dof_Madr)] * vec + diag_mul = d._impl.qM[jp.array(m.dof_Madr)] * vec # pytype: disable=attribute-error is_, js, madr_ijs = [], [], [] for i in range(m.nv): @@ -131,8 +132,8 @@ def mul_m(m: Model, d: Data, vec: jax.Array) -> jax.Array: i, j, madr_ij = (jp.array(x, dtype=jp.int32) for x in (is_, js, madr_ijs)) - out = diag_mul.at[i].add(d.qM[madr_ij] * vec[j]) - out = out.at[j].add(d.qM[madr_ij] * vec[i]) + out = diag_mul.at[i].add(d._impl.qM[madr_ij] * vec[j]) # pytype: disable=attribute-error + out = out.at[j].add(d._impl.qM[madr_ij] * vec[i]) # pytype: disable=attribute-error return out @@ -147,9 +148,9 @@ def jac( mask = mask[jp.array(m.dof_bodyid)] > 0 offset = point - d.subtree_com[jp.array(m.body_rootid)[body_id]] - jacp = jax.vmap(lambda a, b=offset: a[3:] + jp.cross(a[:3], b))(d.cdof) + jacp = jax.vmap(lambda a, b=offset: a[3:] + jp.cross(a[:3], b))(d._impl.cdof) # pytype: disable=attribute-error jacp = jax.vmap(jp.multiply)(jacp, mask) - jacr = jax.vmap(jp.multiply)(d.cdof[:, :3], mask) + jacr = jax.vmap(jp.multiply)(d._impl.cdof[:, :3], mask) # pytype: disable=attribute-error return jacp, jacr @@ -548,20 +549,20 @@ def contact_force( m: Model, d: Data, contact_id: int, to_world_frame: bool = False ) -> jax.Array: """Extract 6D force:torque for one contact, in contact frame by default.""" - efc_address = d.contact.efc_address[contact_id] - condim = d.contact.dim[contact_id] + efc_address = d._impl.contact.efc_address[contact_id] # pytype: disable=attribute-error + condim = d._impl.contact.dim[contact_id] # pytype: disable=attribute-error if m.opt.cone == ConeType.PYRAMIDAL: force = _decode_pyramid( - d.efc_force[efc_address:], d.contact.friction[contact_id], condim + d._impl.efc_force[efc_address:], d._impl.contact.friction[contact_id], condim # pytype: disable=attribute-error ) elif m.opt.cone == ConeType.ELLIPTIC: - force = d.efc_force[efc_address : efc_address + condim] + force = d._impl.efc_force[efc_address : efc_address + condim] # pytype: disable=attribute-error force = jp.concatenate([force, jp.zeros((6 - condim))]) else: raise ValueError(f'Unknown cone type: {m.opt.cone}') if to_world_frame: - force = force.reshape((-1, 3)) @ d.contact.frame[contact_id] + force = force.reshape((-1, 3)) @ d._impl.contact.frame[contact_id] # pytype: disable=attribute-error force = force.reshape(-1) return force * (efc_address >= 0) @@ -572,21 +573,21 @@ def contact_force_dim( ) -> Tuple[jax.Array, np.ndarray]: """Extract 6D force:torque for contacts with dimension dim.""" # valid contact and condim indices - idx_dim = (d.contact.efc_address >= 0) & (d.contact.dim == dim) + idx_dim = (d._impl.contact.efc_address >= 0) & (d._impl.contact.dim == dim) # pytype: disable=attribute-error # contact force from efc if m.opt.cone == ConeType.PYRAMIDAL: efc_address = ( - d.contact.efc_address[idx_dim, None] + d._impl.contact.efc_address[idx_dim, None] # pytype: disable=attribute-error + np.arange(np.where(dim == 1, 1, 2 * (dim - 1)))[None] ) - efc_force = d.efc_force[efc_address] + efc_force = d._impl.efc_force[efc_address] # pytype: disable=attribute-error force = jax.vmap(_decode_pyramid, in_axes=(0, 0, None))( - efc_force, d.contact.friction[idx_dim], dim + efc_force, d._impl.contact.friction[idx_dim], dim # pytype: disable=attribute-error ) elif m.opt.cone == ConeType.ELLIPTIC: - efc_address = d.contact.efc_address[idx_dim, None] + np.arange(dim)[None] - force = d.efc_force[efc_address] + efc_address = d._impl.contact.efc_address[idx_dim, None] + np.arange(dim)[None] # pytype: disable=attribute-error + force = d._impl.efc_force[efc_address] # pytype: disable=attribute-error force = jp.hstack([force, jp.zeros((force.shape[0], 6 - dim))]) else: raise ValueError(f'Unknown cone type: {m.opt.cone}.') diff --git a/mjx/mujoco/mjx/_src/support_test.py b/mjx/mujoco/mjx/_src/support_test.py index 1a0c904c..5f060bc2 100644 --- a/mjx/mujoco/mjx/_src/support_test.py +++ b/mjx/mujoco/mjx/_src/support_test.py @@ -385,7 +385,7 @@ class SupportTest(parameterized.TestCase): # map MJX contacts to MJ ones def _find(g): - val = (g == dx.contact.geom).sum(axis=1) + val = (g == dx._impl.contact.geom).sum(axis=1) return np.where(val == 2)[0][0] contact_id_map = {i: _find(d.contact.geom[i]) for i in range(d.ncon)} @@ -399,7 +399,7 @@ class SupportTest(parameterized.TestCase): np.testing.assert_allclose(result, force, rtol=1e-5, atol=2) # check for zeros after first condim elements - condim = dx.contact.dim[j] + condim = dx._impl.contact.dim[j] if condim < 6: np.testing.assert_allclose(force[condim:], 0, rtol=1e-5, atol=1e-5) @@ -412,8 +412,8 @@ class SupportTest(parameterized.TestCase): ), )(mx, dx, j, True) # back to contact frame - force = force.at[:3].set(dx.contact.frame[j] @ force[:3]) - force = force.at[3:].set(dx.contact.frame[j] @ force[3:]) + force = force.at[:3].set(dx._impl.contact.frame[j] @ force[:3]) + force = force.at[3:].set(dx._impl.contact.frame[j] @ force[3:]) np.testing.assert_allclose(result, force, rtol=1e-5, atol=2) def test_wrap_inside(self): diff --git a/mjx/mujoco/mjx/_src/test_util.py b/mjx/mujoco/mjx/_src/test_util.py index f91ee9af..142a2eaa 100644 --- a/mjx/mujoco/mjx/_src/test_util.py +++ b/mjx/mujoco/mjx/_src/test_util.py @@ -106,20 +106,20 @@ def benchmark( def efc_order(m: mujoco.MjModel, d: mujoco.MjData, dx: Data) -> np.ndarray: - """Returns a sort order such that dx.efc_*[order][:d.nefc] == d.efc_*.""" + """Returns a sort order such that dx.efc_*[order][:d._impl.nefc] == d.efc_*.""" # pytype: disable=attribute-error # reorder efc rows to skip inactive constraints and match contact order - efl = dx.ne + dx.nf + dx.nl + efl = dx._impl.ne + dx._impl.nf + dx._impl.nl # pytype: disable=attribute-error order = np.arange(efl) - order[(dx.efc_J[:efl] == 0).all(axis=1)] = 2**16 # move empty rows to end - for i in range(dx.ncon): - num_rows = dx.contact.dim[i] - if dx.contact.dim[i] > 1 and m.opt.cone == mujoco.mjtCone.mjCONE_PYRAMIDAL: - num_rows = (dx.contact.dim[i] - 1) * 2 - if dx.contact.dist[i] > 0: # move empty contacts to end + order[(dx._impl.efc_J[:efl] == 0).all(axis=1)] = 2**16 # move empty rows to end # pytype: disable=attribute-error + for i in range(dx._impl.ncon): # pytype: disable=attribute-error + num_rows = dx._impl.contact.dim[i] # pytype: disable=attribute-error + if dx._impl.contact.dim[i] > 1 and m.opt.cone == mujoco.mjtCone.mjCONE_PYRAMIDAL: # pytype: disable=attribute-error + num_rows = (dx._impl.contact.dim[i] - 1) * 2 # pytype: disable=attribute-error + if dx._impl.contact.dist[i] > 0: # move empty contacts to end # pytype: disable=attribute-error order = np.append(order, np.repeat(2**16, num_rows)) continue - contact_match = (d.contact.geom == dx.contact.geom[i]).all(axis=-1) - contact_match &= (d.contact.pos == dx.contact.pos[i]).all(axis=-1) + contact_match = (d.contact.geom == dx._impl.contact.geom[i]).all(axis=-1) # pytype: disable=attribute-error + contact_match &= (d.contact.pos == dx._impl.contact.pos[i]).all(axis=-1) # pytype: disable=attribute-error assert contact_match.any(), f'contact {i} not found' contact_id = np.nonzero(contact_match)[0][0] order = np.append(order, np.repeat(efl + contact_id, num_rows)) diff --git a/mjx/mujoco/mjx/_src/types.py b/mjx/mujoco/mjx/_src/types.py index 657cd524..c70d71a2 100644 --- a/mjx/mujoco/mjx/_src/types.py +++ b/mjx/mujoco/mjx/_src/types.py @@ -14,20 +14,33 @@ # ============================================================================== """Base types used in MJX.""" -import dataclasses import enum -from typing import Tuple +from typing import Tuple, Union +import warnings + import jax import mujoco from mujoco.mjx._src.dataclasses import PyTreeNode # pylint: disable=g-importing-member import numpy as np -def _restricted_to(platform: str): - """Specifies whether a field exists in only MuJoCo or MJX.""" - if platform not in ('mujoco', 'mjx'): - raise ValueError(f'unknown platform: {platform}') - return dataclasses.field(metadata={'restricted_to': platform}) +class BackendImpl(enum.Enum): + """Backend implementation to use.""" + + C = 'c' + JAX = 'jax' + WARP = 'warp' + + @classmethod + def _missing_(cls, value): + # This method is called only when lookup by value fails + # (e.g., BackendImpl('JAX') fails initially because 'JAX' != 'jax') + if not isinstance(value, str): + return None + for member in cls: + if member.value == value.lower(): + return member + return None class DisableBit(enum.IntFlag): @@ -430,77 +443,6 @@ class ObjType(PyTreeNode): CAMERA = mujoco.mjtObj.mjOBJ_CAMERA -class Option(PyTreeNode): - """Physics options. - - Attributes: - timestep: timestep - apirate: update rate for remote API (Hz) (not used) - impratio: ratio of friction-to-normal contact impedance - tolerance: main solver tolerance - ls_tolerance: CG/Newton linesearch tolerance - noslip_tolerance: noslip solver tolerance (not used) - ccd_tolerance: CCD solver tolerance (not used) - gravity: gravitational acceleration (3,) - wind: wind (for lift, drag and viscosity) - magnetic: global magnetic flux (not used) - density: density of medium - viscosity: viscosity of medium - o_margin: contact solver override: margin (not used) - o_solref: contact solver override: solref (not used) - o_solimp: contact solver override: solimp (not used) - o_friction[5]: contact solver override: friction (not used) - has_fluid_params: automatically set by mjx if wind/density/viscosity are - nonzero. Not used by mj - integrator: integration mode - cone: type of friction cone - jacobian: matrix layout for mass matrices (dense or sparse) - (note that this is different from MuJoCo, where jacobian - specifies whether efc_J and its accompanying matrices - are dense or sparse. - solver: solver algorithm - iterations: number of main solver iterations - ls_iterations: maximum number of CG/Newton linesearch iterations - noslip_iterations: maximum number of noslip solver iterations (not used) - ccd_iterations: maximum number of CCD solver iterations (not used) - disableflags: bit flags for disabling standard features - enableflags: bit flags for enabling optional features (not used) - disableactuator: bit flags for disabling actuators by group id (not used) - sdf_initpoints: number of starting points for gradient descent (not used) - sdf_iterations: max number of iterations for gradient descent (not used) - """ # fmt: skip - timestep: jax.Array - apirate: jax.Array = _restricted_to('mujoco') - impratio: jax.Array - tolerance: jax.Array - ls_tolerance: jax.Array - noslip_tolerance: jax.Array = _restricted_to('mujoco') - ccd_tolerance: jax.Array = _restricted_to('mujoco') - gravity: jax.Array - wind: jax.Array - magnetic: jax.Array - density: jax.Array - viscosity: jax.Array - o_margin: jax.Array - o_solref: jax.Array - o_solimp: jax.Array - o_friction: jax.Array - has_fluid_params: bool = _restricted_to('mjx') - integrator: IntegratorType - cone: ConeType - jacobian: JacobianType - solver: SolverType - iterations: int - ls_iterations: int - noslip_iterations: int = _restricted_to('mujoco') - ccd_iterations: int = _restricted_to('mujoco') - disableflags: DisableBit - enableflags: int - disableactuator: int - sdf_initpoints: int = _restricted_to('mujoco') - sdf_iterations: int = _restricted_to('mujoco') - - class Statistic(PyTreeNode): """Model statistics (in qpos0). @@ -519,371 +461,142 @@ class Statistic(PyTreeNode): center: jax.Array -class Model(PyTreeNode): - """Static model of the scene that remains unchanged with each physics step. +class Option(PyTreeNode): + """Physics options.""" # fmt: skip + timestep: jax.Array + impratio: jax.Array + tolerance: jax.Array + ls_tolerance: jax.Array + gravity: jax.Array + wind: jax.Array + magnetic: jax.Array + density: jax.Array + viscosity: jax.Array + o_margin: jax.Array + o_solref: jax.Array + o_solimp: jax.Array + o_friction: jax.Array + integrator: IntegratorType + cone: ConeType + jacobian: JacobianType + solver: SolverType + iterations: int + ls_iterations: int + disableflags: DisableBit + enableflags: int + disableactuator: int - Attributes: - nq: number of generalized coordinates = dim(qpos) - nv: number of degrees of freedom = dim(qvel) - nu: number of actuators/controls = dim(ctrl) - na: number of activation states = dim(act) - nbody: number of bodies - nbvh: number of total bounding volumes in all bodies - nbvhstatic: number of static bounding volumes (aabb stored in mjModel) - nbvhdynamic: number of dynamic bounding volumes (aabb stored in mjData) - njnt: number of joints - ngeom: number of geoms - nsite: number of sites - ncam: number of cameras - nlight: number of lights - nflex: number of flexes - nflexvert: number of vertices in all flexes - nflexedge: number of edges in all flexes - nflexelem: number of elements in all flexes - nflexelemdata: number of element vertex ids in all flexes - nflexshelldata: number of shell fragment vertex ids in all flexes - nflexevpair: number of element-vertex pairs in all flexes - nflextexcoord: number of vertices with texture coordinates - nmesh: number of meshes - nmeshvert: number of vertices in all meshes - nmeshnormal: number of normals in all meshes - nmeshtexcoord: number of texcoords in all meshes - nmeshface: number of triangular faces in all meshes - nmeshgraph: number of ints in mesh auxiliary data - nhfield: number of heightfields - nhfielddata: number of data points in all heightfields - ntex: number of textures - ntexdata: number of bytes in texture rgb data - nmat: number of materials - npair: number of predefined geom pairs - nexclude: number of excluded geom pairs - neq: number of equality constraints - ntendon: number of tendons - nwrap: number of wrap objects in all tendon paths - nsensor: number of sensors - nnumeric: number of numeric custom fields - ntuple: number of tuple custom fields - nkey: number of keyframes - nmocap: number of mocap bodies - nM: number of non-zeros in sparse inertia matrix - nD: number of non-zeros in sparse dof-dof matrix - nB: number of non-zeros in sparse body-dof matrix - nC: number of non-zeros in sparse reduced dof-dof matrix - nD: number of non-zeros in sparse dof-dof matrix - nJmom: number of non-zeros in sparse actuator_moment matrix - ntree: number of kinematic trees under world body - ngravcomp: number of bodies with nonzero gravcomp - nuserdata: size of userdata array - nsensordata: number of mjtNums in sensor data vector - narena: number of bytes in the mjData arena (inclusive of stack) - opt: physics options - stat: model statistics - qpos0: qpos values at default pose (nq,) - qpos_spring: reference pose for springs (nq,) - body_parentid: id of body's parent (nbody,) - body_rootid: id of root above body (nbody,) - body_weldid: id of body that this body is welded to (nbody,) - body_jntnum: number of joints for this body (nbody,) - body_jntadr: start addr of joints; -1: no joints (nbody,) - body_dofnum: number of motion degrees of freedom (nbody,) - body_dofadr: start addr of dofs; -1: no dofs (nbody,) - body_treeid: id of body's kinematic tree; -1: static (nbody,) - body_geomnum: number of geoms (nbody,) - body_geomadr: start addr of geoms; -1: no geoms (nbody,) - body_simple: 1: diag M; 2: diag M, sliders only (nbody,) - body_pos: position offset rel. to parent body (nbody, 3) - body_quat: orientation offset rel. to parent body (nbody, 4) - body_ipos: local position of center of mass (nbody, 3) - body_iquat: local orientation of inertia ellipsoid (nbody, 4) - body_mass: mass (nbody,) - body_subtreemass: mass of subtree starting at this body (nbody,) - body_inertia: diagonal inertia in ipos/iquat frame (nbody, 3) - body_gravcomp: antigravity force, units of body weight (nbody,) - body_margin: MAX over all geom margins (nbody,) - body_contype: OR over all geom contypes (nbody,) - body_conaffinity: OR over all geom conaffinities (nbody,) - body_bvhadr: address of bvh root (nbody,) - body_bvhnum: number of bounding volumes (nbody,) - bvh_child: left and right children in tree (nbvh, 2) - bvh_nodeid: geom or elem id of node; -1: non-leaf (nbvh,) - bvh_aabb: local bounding box (center, size) (nbvhstatic, 6) - body_invweight0: mean inv inert in qpos0 (trn, rot) (nbody, 2) - jnt_type: type of joint (mjtJoint) (njnt,) - jnt_qposadr: start addr in 'qpos' for joint's data (njnt,) - jnt_dofadr: start addr in 'qvel' for joint's data (njnt,) - jnt_bodyid: id of joint's body (njnt,) - jnt_group: group for visibility (njnt,) - jnt_limited: does joint have limits (njnt,) - jnt_actfrclimited: does joint have actuator force limits (njnt,) - jnt_actgravcomp: is gravcomp force applied via actuators (njnt,) - jnt_solref: constraint solver reference: limit (njnt, mjNREF) - jnt_solimp: constraint solver impedance: limit (njnt, mjNIMP) - jnt_pos: local anchor position (njnt, 3) - jnt_axis: local joint axis (njnt, 3) - jnt_stiffness: stiffness coefficient (njnt,) - jnt_range: joint limits (njnt, 2) - jnt_actfrcrange: range of total actuator force (njnt, 2) - jnt_margin: min distance for limit detection (njnt,) - dof_bodyid: id of dof's body (nv,) - dof_jntid: id of dof's joint (nv,) - dof_parentid: id of dof's parent; -1: none (nv,) - dof_treeid: id of dof's kinematic tree (nv,) - dof_Madr: dof address in M-diagonal (nv,) - dof_simplenum: number of consecutive simple dofs (nv,) - dof_solref: constraint solver reference:frictionloss (nv, mjNREF) - dof_solimp: constraint solver impedance:frictionloss (nv, mjNIMP) - dof_frictionloss: dof friction loss (nv,) - dof_hasfrictionloss: dof has >0 frictionloss (MJX) (nv,) - dof_armature: dof armature inertia/mass (nv,) - dof_damping: damping coefficient (nv,) - dof_invweight0: diag. inverse inertia in qpos0 (nv,) - dof_M0: diag. inertia in qpos0 (nv,) - geom_type: geometric type (mjtGeom) (ngeom,) - geom_contype: geom contact type (ngeom,) - geom_conaffinity: geom contact affinity (ngeom,) - geom_condim: contact dimensionality (1, 3, 4, 6) (ngeom,) - geom_bodyid: id of geom's body (ngeom,) - geom_dataid: id of geom's mesh/hfield; -1: none (ngeom,) - geom_group: group for visibility (ngeom,) - geom_matid: material id for rendering (ngeom,) - geom_priority: geom contact priority (ngeom,) - geom_solmix: mixing coef for solref/imp in geom pair (ngeom,) - geom_solref: constraint solver reference: contact (ngeom, mjNREF) - geom_solimp: constraint solver impedance: contact (ngeom, mjNIMP) - geom_size: geom-specific size parameters (ngeom, 3) - geom_aabb: bounding box, (center, size) (ngeom, 6) - geom_rbound: radius of bounding sphere (ngeom,) - geom_rbound_hfield: static rbound for hfield grid bounds (ngeom,) - geom_pos: local position offset rel. to body (ngeom, 3) - geom_quat: local orientation offset rel. to body (ngeom, 4) - geom_friction: friction for (slide, spin, roll) (ngeom, 3) - geom_margin: include in solver if dist0 frictionloss (MJX) (ntendon,) - wrap_type: wrap object type (mjtWrap) (nwrap,) - wrap_objid: object id: geom, site, joint (nwrap,) - wrap_prm: divisor, joint coef, or site id (nwrap,) - wrap_inside_maxiter: maximum iterations for wrap_inside - wrap_inside_tolerance: tolerance for wrap_inside - wrap_inside_z_init: initialization for wrap_inside - is_wrap_inside: spatial tendon sidesite inside geom (nwrapinside,) - actuator_trntype: transmission type (mjtTrn) (nu,) - actuator_dyntype: dynamics type (mjtDyn) (nu,) - actuator_gaintype: gain type (mjtGain) (nu,) - actuator_biastype: bias type (mjtBias) (nu,) - actuator_trnid: transmission id: joint, tendon, site (nu, 2) - actuator_actadr: first activation address; -1: stateless (nu,) - actuator_actnum: number of activation variables (nu,) - actuator_group: group for visibility (nu,) - actuator_ctrllimited: is control limited (nu,) - actuator_forcelimited: is force limited (nu,) - actuator_actlimited: is activation limited (nu,) - actuator_dynprm: dynamics parameters (nu, mjNDYN) - actuator_gainprm: gain parameters (nu, mjNGAIN) - actuator_biasprm: bias parameters (nu, mjNBIAS) - actuator_actearly: step activation before force (nu,) - actuator_ctrlrange: range of controls (nu, 2) - actuator_forcerange: range of forces (nu, 2) - actuator_actrange: range of activations (nu, 2) - actuator_gear: scale length and transmitted force (nu, 6) - actuator_cranklength: crank length for slider-crank (nu,) - actuator_acc0: acceleration from unit force in qpos0 (nu,) - actuator_lengthrange: feasible actuator length range (nu, 2) - sensor_type: sensor type (mjtSensor) (nsensor,) - sensor_datatype: numeric data type (mjtDataType) (nsensor,) - sensor_needstage: required compute stage (mjtStage) (nsensor,) - sensor_objtype: type of sensorized object (mjtObj) (nsensor,) - sensor_objid: id of sensorized object (nsensor,) - sensor_reftype: type of reference frame (mjtObj) (nsensor,) - sensor_refid: id of reference frame; -1: global frame (nsensor,) - sensor_dim: number of scalar outputs (nsensor,) - sensor_adr: address in sensor array (nsensor,) - sensor_cutoff: cutoff for real and positive; 0: ignore (nsensor,) - numeric_adr: address of field in numeric_data (nnumeric,) - numeric_data: array of all numeric fields (nnumericdata,) - tuple_adr: address of text in text_data (ntuple,) - tuple_size: number of objects in tuple (ntuple,) - tuple_objtype: array of object types in all tuples (ntupledata,) - tuple_objid: array of object ids in all tuples (ntupledata,) - tuple_objprm: array of object params in all tuples (ntupledata,) - key_time: key time (nkey,) - key_qpos: key position (nkey, nq) - key_qvel: key velocity (nkey, nv) - key_act: key activation (nkey, na) - key_mpos: key mocap position (nkey, nmocap, 3) - key_mquat: key mocap quaternion (nkey, nmocap, 4) - key_ctrl: key control (nkey, nu) - name_bodyadr: body name pointers (nbody,) - name_jntadr: joint name pointers (njnt,) - name_geomadr: geom name pointers (ngeom,) - name_siteadr: site name pointers (nsite,) - name_camadr: camera name pointers (ncam,) - name_meshadr: mesh name pointers (nmesh,) - name_pairadr: geom pair name pointers (npair,) - name_eqadr: equality constraint name pointers (neq,) - name_tendonadr: tendon name pointers (ntendon,) - name_actuatoradr: actuator name pointers (nu,) - name_sensoradr: sensor name pointers (nsensor,) - name_numericadr: numeric name pointers (nnumeric,) - name_tupleadr: tuple name pointers (ntuple,) - name_keyadr: keyframe name pointers (nkey,) - names: names of all objects, 0-terminated (nnames,) - signature: compilation signature - """ + +class OptionC(Option): + """C-specific option.""" + + apirate: jax.Array + noslip_tolerance: jax.Array + ccd_tolerance: jax.Array + noslip_iterations: int + ccd_iterations: int + sdf_initpoints: int + sdf_iterations: int + + +class OptionJAX(Option): + """JAX-specific option.""" + + has_fluid_params: bool + + +class ModelC(PyTreeNode): + """CPU-specific model data.""" + + nbvh: jax.Array + nbvhstatic: jax.Array + nbvhdynamic: jax.Array + nflex: jax.Array + nflexvert: jax.Array + nflexedge: jax.Array + nflexelem: jax.Array + nflexelemdata: jax.Array + nflexshelldata: jax.Array + nflexevpair: jax.Array + nflextexcoord: jax.Array + ntree: jax.Array + narena: jax.Array + body_bvhadr: jax.Array + body_bvhnum: jax.Array + bvh_child: jax.Array + bvh_nodeid: jax.Array + bvh_aabb: jax.Array + light_bodyid: jax.Array + light_targetbodyid: jax.Array + flex_contype: jax.Array + flex_conaffinity: jax.Array + flex_condim: jax.Array + flex_priority: jax.Array + flex_solmix: jax.Array + flex_solref: jax.Array + flex_solimp: jax.Array + flex_friction: jax.Array + flex_margin: jax.Array + flex_gap: jax.Array + flex_internal: jax.Array + flex_selfcollide: jax.Array + flex_activelayers: jax.Array + flex_dim: jax.Array + flex_vertadr: jax.Array + flex_vertnum: jax.Array + flex_edgeadr: jax.Array + flex_edgenum: jax.Array + flex_elemadr: jax.Array + flex_elemnum: jax.Array + flex_elemdataadr: jax.Array + flex_evpairadr: jax.Array + flex_evpairnum: jax.Array + flex_vertbodyid: jax.Array + flex_edge: jax.Array + flex_elem: jax.Array + flex_elemlayer: jax.Array + flex_evpair: jax.Array + flex_vert: jax.Array + flexedge_length0: jax.Array + flexedge_invweight0: jax.Array + flex_radius: jax.Array + flex_edgestiffness: jax.Array + flex_edgedamping: jax.Array + flex_edgeequality: jax.Array + flex_rigid: jax.Array + flexedge_rigid: jax.Array + flex_centered: jax.Array + flex_bvhadr: jax.Array + flex_bvhnum: jax.Array + actuator_plugin: jax.Array + + +class ModelJAX(PyTreeNode): + """JAX-specific model data.""" + + dof_hasfrictionloss: np.ndarray + geom_rbound_hfield: np.ndarray + mesh_convex: Tuple[ConvexMesh, ...] + tendon_hasfrictionloss: np.ndarray + wrap_inside_maxiter: int + wrap_inside_tolerance: float + wrap_inside_z_init: float + is_wrap_inside: np.ndarray + + +class Model(PyTreeNode): + """Static model of the scene that remains unchanged with each physics step.""" nq: int nv: int nu: int na: int nbody: int - nbvh: int = _restricted_to('mujoco') - nbvhstatic: int = _restricted_to('mujoco') - nbvhdynamic: int = _restricted_to('mujoco') njnt: int ngeom: int nsite: int ncam: int nlight: int - nflex: int = _restricted_to('mujoco') - nflexvert: int = _restricted_to('mujoco') - nflexedge: int = _restricted_to('mujoco') - nflexelem: int = _restricted_to('mujoco') - nflexelemdata: int = _restricted_to('mujoco') - nflexshelldata: int = _restricted_to('mujoco') - nflexevpair: int = _restricted_to('mujoco') - nflextexcoord: int = _restricted_to('mujoco') nmesh: int nmeshvert: int nmeshnormal: int @@ -910,11 +623,9 @@ class Model(PyTreeNode): nC: int # pylint:disable=invalid-name nD: int # pylint:disable=invalid-name nJmom: int # pylint:disable=invalid-name - ntree: int = _restricted_to('mujoco') ngravcomp: int nuserdata: int nsensordata: int - narena: int = _restricted_to('mujoco') opt: Option stat: Statistic qpos0: jax.Array @@ -943,11 +654,6 @@ class Model(PyTreeNode): body_margin: np.ndarray body_contype: np.ndarray body_conaffinity: np.ndarray - body_bvhadr: np.ndarray = _restricted_to('mujoco') - body_bvhnum: np.ndarray = _restricted_to('mujoco') - bvh_child: np.ndarray = _restricted_to('mujoco') - bvh_nodeid: np.ndarray = _restricted_to('mujoco') - bvh_aabb: np.ndarray = _restricted_to('mujoco') body_invweight0: jax.Array jnt_type: np.ndarray jnt_qposadr: np.ndarray @@ -973,7 +679,6 @@ class Model(PyTreeNode): dof_solref: jax.Array dof_solimp: jax.Array dof_frictionloss: jax.Array - dof_hasfrictionloss: np.ndarray = _restricted_to('mjx') dof_armature: jax.Array dof_damping: jax.Array dof_invweight0: jax.Array @@ -994,7 +699,6 @@ class Model(PyTreeNode): geom_size: jax.Array geom_aabb: np.ndarray geom_rbound: jax.Array - geom_rbound_hfield: np.ndarray = _restricted_to('mjx') geom_pos: jax.Array geom_quat: jax.Array geom_friction: jax.Array @@ -1021,8 +725,6 @@ class Model(PyTreeNode): cam_sensorsize: np.ndarray cam_intrinsic: np.ndarray light_mode: np.ndarray - light_bodyid: np.ndarray = _restricted_to('mujoco') - light_targetbodyid: np.ndarray = _restricted_to('mujoco') light_directional: jax.Array light_castshadow: jax.Array light_pos: jax.Array @@ -1031,46 +733,6 @@ class Model(PyTreeNode): light_pos0: np.ndarray light_dir0: np.ndarray light_cutoff: jax.Array - flex_contype: np.ndarray = _restricted_to('mujoco') - flex_conaffinity: np.ndarray = _restricted_to('mujoco') - flex_condim: np.ndarray = _restricted_to('mujoco') - flex_priority: np.ndarray = _restricted_to('mujoco') - flex_solmix: np.ndarray = _restricted_to('mujoco') - flex_solref: np.ndarray = _restricted_to('mujoco') - flex_solimp: np.ndarray = _restricted_to('mujoco') - flex_friction: np.ndarray = _restricted_to('mujoco') - flex_margin: np.ndarray = _restricted_to('mujoco') - flex_gap: np.ndarray = _restricted_to('mujoco') - flex_internal: np.ndarray = _restricted_to('mujoco') - flex_selfcollide: np.ndarray = _restricted_to('mujoco') - flex_activelayers: np.ndarray = _restricted_to('mujoco') - flex_dim: np.ndarray = _restricted_to('mujoco') - flex_vertadr: np.ndarray = _restricted_to('mujoco') - flex_vertnum: np.ndarray = _restricted_to('mujoco') - flex_edgeadr: np.ndarray = _restricted_to('mujoco') - flex_edgenum: np.ndarray = _restricted_to('mujoco') - flex_elemadr: np.ndarray = _restricted_to('mujoco') - flex_elemnum: np.ndarray = _restricted_to('mujoco') - flex_elemdataadr: np.ndarray = _restricted_to('mujoco') - flex_evpairadr: np.ndarray = _restricted_to('mujoco') - flex_evpairnum: np.ndarray = _restricted_to('mujoco') - flex_vertbodyid: np.ndarray = _restricted_to('mujoco') - flex_edge: np.ndarray = _restricted_to('mujoco') - flex_elem: np.ndarray = _restricted_to('mujoco') - flex_elemlayer: np.ndarray = _restricted_to('mujoco') - flex_evpair: np.ndarray = _restricted_to('mujoco') - flex_vert: np.ndarray = _restricted_to('mujoco') - flexedge_length0: np.ndarray = _restricted_to('mujoco') - flexedge_invweight0: np.ndarray = _restricted_to('mujoco') - flex_radius: np.ndarray = _restricted_to('mujoco') - flex_edgestiffness: np.ndarray = _restricted_to('mujoco') - flex_edgedamping: np.ndarray = _restricted_to('mujoco') - flex_edgeequality: np.ndarray = _restricted_to('mujoco') - flex_rigid: np.ndarray = _restricted_to('mujoco') - flexedge_rigid: np.ndarray = _restricted_to('mujoco') - flex_centered: np.ndarray = _restricted_to('mujoco') - flex_bvhadr: np.ndarray = _restricted_to('mujoco') - flex_bvhnum: np.ndarray = _restricted_to('mujoco') mesh_vertadr: np.ndarray mesh_vertnum: np.ndarray mesh_faceadr: np.ndarray @@ -1082,7 +744,6 @@ class Model(PyTreeNode): mesh_graph: np.ndarray mesh_pos: np.ndarray mesh_quat: np.ndarray - mesh_convex: Tuple[ConvexMesh, ...] = _restricted_to('mjx') mesh_texcoordadr: np.ndarray mesh_texcoordnum: np.ndarray mesh_texcoord: np.ndarray @@ -1136,14 +797,9 @@ class Model(PyTreeNode): tendon_lengthspring: jax.Array tendon_length0: jax.Array tendon_invweight0: jax.Array - tendon_hasfrictionloss: np.ndarray = _restricted_to('mjx') wrap_type: np.ndarray wrap_objid: np.ndarray wrap_prm: np.ndarray - wrap_inside_maxiter: int = _restricted_to('mjx') - wrap_inside_tolerance: float = _restricted_to('mjx') - wrap_inside_z_init: float = _restricted_to('mjx') - is_wrap_inside: np.ndarray = _restricted_to('mjx') actuator_trntype: np.ndarray actuator_dyntype: np.ndarray actuator_gaintype: np.ndarray @@ -1166,7 +822,6 @@ class Model(PyTreeNode): actuator_cranklength: np.ndarray actuator_acc0: jax.Array actuator_lengthrange: np.ndarray - actuator_plugin: np.ndarray = _restricted_to('mujoco') sensor_type: np.ndarray sensor_datatype: np.ndarray sensor_needstage: np.ndarray @@ -1209,6 +864,36 @@ class Model(PyTreeNode): names: bytes signature: np.uint64 _sizes: jax.Array + _impl: Union[ModelC, ModelJAX] + + @property + def backend_impl(self) -> BackendImpl: + return { + ModelC: BackendImpl.C, + ModelJAX: BackendImpl.JAX, + }[type(self._impl)] + + def __getattr__(self, name: str): + if name == 'value': + # Special case for NNX, the value attribute may not exist on the parent + # PyTreeNode, before it exists on the child PyTreeNode. Thanks NNX. + return object.__getattribute__(self, 'value') + + try: + impl_instsance = object.__getattribute__(self, '_impl') + val = getattr(impl_instsance, name) + warnings.warn( + f'Accessing `{name}` directly from `Model` is deprecated. ' + f'Access it via `model._impl.{name}` instead.', + DeprecationWarning, + stacklevel=2, + ) + except AttributeError: + # raise the standard exception + raise AttributeError( # pylint: disable=raise-missing-from + f"'{type(self).__name__}' object has no attribute '{name}'" + ) + return val class Contact(PyTreeNode): @@ -1246,144 +931,141 @@ class Contact(PyTreeNode): efc_address: np.ndarray -class Data(PyTreeNode): - r"""\Dynamic state that updates each step. +class DataC(PyTreeNode): + """C-specific data.""" - Attributes: - ne: number of equality constraints - nf: number of friction constraints - nl: number of limit constraints - nefc: number of constraints - ncon: number of contacts - solver_niter: number of solver iterations - time: simulation time - qpos: position (nq,) - qvel: velocity (nv,) - act: actuator activation (na,) - qacc_warmstart: acceleration used for warmstart (nv,) - ctrl: control (nu,) - qfrc_applied: applied generalized force (nv,) - xfrc_applied: applied Cartesian force/torque (nbody, 6) - eq_active: enable/disable constraints (neq,) - mocap_pos: positions of mocap bodies (nmocap x 3) - mocap_quat: orientations of mocap bodies (nmocap x 4) - qacc: acceleration (nv,) - act_dot: time-derivative of actuator activation (na,) - userdata: user data, not touched by engine (nuserdata,) - sensordata: sensor data array (nsensordata,) - xpos: Cartesian position of body frame (nbody, 3) - xquat: Cartesian orientation of body frame (nbody, 4) - xmat: Cartesian orientation of body frame (nbody, 3, 3) - xipos: Cartesian position of body com (nbody, 3) - ximat: Cartesian orientation of body inertia (nbody, 3, 3) - xanchor: Cartesian position of joint anchor (njnt, 3) - xaxis: Cartesian joint axis (njnt, 3) - geom_xpos: Cartesian geom position (ngeom, 3) - geom_xmat: Cartesian geom orientation (ngeom, 3, 3) - site_xpos: Cartesian site position (nsite, 3) - site_xmat: Cartesian site orientation (nsite, 3, 3) - cam_xpos: Cartesian camera position (ncam, 3) - cam_xmat: Cartesian camera orientation (ncam, 3, 3) - light_xpos: Cartesian light position (nlight, 3) - light_xdir: Cartesian light direction (nlight, 3) - subtree_com: center of mass of each subtree (nbody, 3) - cdof: com-based motion axis of each dof (nv, 6) - cinert: com-based body inertia and mass (nbody, 10) - flexvert_xpos: Cartesian flex vertex positions (nflexvert, 3) - flexelem_aabb: flex element bounding boxes (center, size) (nflexelem, 6) - flexedge_J_rownnz: number of non-zeros in Jacobian row (nflexedge,) - flexedge_J_rowadr: row start address in colind array (nflexedge,) - flexedge_J_colind: column indices in sparse Jacobian (nflexedge, nv) - flexedge_J: flex edge Jacobian (nflexedge, nv) - flexedge_length: flex edge lengths (nflexedge,) - ten_wrapadr: start address of tendon's path (ntendon,) - ten_wrapnum: number of wrap points in path (ntendon,) - ten_J_rownnz: number of non-zeros in Jacobian row (ntendon,) - ten_J_rowadr: row start address in colind array (ntendon,) - ten_J_colind: column indices in sparse Jacobian (ntendon, nv) - ten_J: tendon Jacobian (ntendon, nv) - ten_length: tendon lengths (ntendon,) - wrap_obj: geom id; -1: site; -2: pulley (nwrap*2,) - wrap_xpos: Cartesian 3D points in all path (nwrap*2, 3) - actuator_length: actuator lengths (nu,) - moment_rownnz: number of non-zeros in actuator_moment row (nu,) - moment_rowadr: row start address in colind array (nu,) - moment_colind: column indices in sparse Jacobian (nJmom,) - actuator_moment: actuator moments (nJmom,) - crb: com-based composite inertia and mass (nbody, 10) - qM: total inertia if sparse: (nM,) - if dense: (nv, nv) - qLD: L'*D*L (or Cholesky) factorization of M. if sparse: (nM,) - if dense: (nv, nv) - qLDiagInv: 1/diag(D) if sparse: (nv,) - if dense: (0,) - bvh_aabb_dyn: global bounding box (center, size) (nbvhdynamic, 6) - bvh_active: volume has been added to collisions (nbvh,) - flexedge_velocity: flex edge velocities (nflexedge,) - ten_velocity: tendon velocities (ntendon,) - actuator_velocity: actuator velocities (nu,) - cvel: com-based velocity [3D rot; 3D tran] (nbody, 6) - cdof_dot: time-derivative of cdof (nv, 6) - qfrc_bias: C(qpos,qvel) (nv,) - qfrc_spring: passive spring force (nv,) - qfrc_damper: passive damper force (nv,) - qfrc_gravcomp: passive gravity compensation force (nv,) - qfrc_fluid: passive fluid force (nv,) - qfrc_passive: total passive force (nv,) - subtree_linvel: linear velocity of subtree com (nbody, 3) - subtree_angmom: angular momentum about subtree com (nbody, 3) - qH: L'*D*L factorization of modified M (nM,) - qHDiagInv: 1/diag(D) of modified M (nv,) - B_rownnz: body-dof: non-zeros in each row (nbody,) - B_rowadr: body-dof: address of each row in B_colind (nbody,) - B_colind: body-dof: column indices of non-zeros (nB,) - M_rownnz: inertia: non-zeros in each row (nv,) - M_rowadr: inertia: address of each row in M_colind (nv,) - M_colind: inertia: column indices of non-zeros (nM,) - mapM2M: index mapping from M (legacy) to M (CSR) (nM,) - C_rownnz: reduced dof-dof: non-zeros in each row (nv,) - C_rowadr: reduced dof-dof: address of each row in C_colind (nv,) - C_colind: reduced dof-dof: column indices of non-zeros (nC,) - mapM2C: index mapping from M to C (nC,) - D_rownnz: dof-dof: non-zeros in each row (nv,) - D_rowadr: dof-dof: address of each row in D_colind (nv,) - D_diag: dof-dof: index of diagonal element (nv,) - D_colind: dof-dof: column indices of non-zeros (nD,) - mapM2D: index mapping from M to D (nD,) - mapD2M: index mapping from D to M (nM,) - qDeriv: d (passive + actuator - bias) / d qvel (nD,) - qLU: sparse LU of (qM - dt*qDeriv) (nD,) - actuator_force: actuator force in actuation space (nu,) - qfrc_actuator: actuator force (nv,) - qfrc_smooth: net unconstrained force (nv,) - qacc_smooth: unconstrained acceleration (nv,) - qfrc_constraint: constraint force (nv,) - qfrc_inverse: net external force; should equal: (nv,) - qfrc_applied + J'*xfrc_applied + qfrc_actuator - cacc: com-based acceleration (nbody, 6) - cfrc_int: com-based interaction force with parent (nbody, 6) - cfrc_ext: com-based external force on body (nbody, 6) - contact: all detected contacts (ncon,) - efc_type: constraint type (nefc,) - efc_J: constraint Jacobian (nefc, nv) - efc_pos: constraint position (equality, contact) (nefc,) - efc_margin: inclusion margin (contact) (nefc,) - efc_frictionloss: frictionloss (friction) (nefc,) - efc_D: constraint mass (nefc,) - efc_aref: reference pseudo-acceleration (nefc,) - efc_force: constraint force in constraint space (nefc,) - _qM_sparse: qM in sparse representation (nM,) - _qLD_sparse: qLD in sparse representation (nM,) - _qLDiagInv_sparse: qLDiagInv in sparse representation (nv,) - """ # fmt: skip # constant sizes: + # TODO(stunya): make these sizes jax.Array? ne: int nf: int nl: int nefc: int ncon: int - # solver statistics: + # TODO(stunya): remove most of these fields solver_niter: jax.Array + cdof: jax.Array + cinert: jax.Array + light_xpos: jax.Array + light_xdir: jax.Array + flexvert_xpos: jax.Array + flexelem_aabb: jax.Array + flexedge_J_rownnz: jax.Array # pylint:disable=invalid-name + flexedge_J_rowadr: jax.Array # pylint:disable=invalid-name + flexedge_J_colind: jax.Array # pylint:disable=invalid-name + flexedge_J: jax.Array # pylint:disable=invalid-name + flexedge_length: jax.Array + ten_wrapadr: jax.Array + ten_wrapnum: jax.Array + ten_J_rownnz: jax.Array # pylint:disable=invalid-name + ten_J_rowadr: jax.Array # pylint:disable=invalid-name + ten_J_colind: jax.Array # pylint:disable=invalid-name + ten_J: jax.Array # pylint:disable=invalid-name + ten_length: jax.Array + wrap_obj: jax.Array + wrap_xpos: jax.Array + actuator_length: jax.Array + moment_rownnz: jax.Array # pylint:disable=invalid-name + moment_rowadr: jax.Array # pylint:disable=invalid-name + moment_colind: jax.Array # pylint:disable=invalid-name + actuator_moment: jax.Array + crb: jax.Array + qM: jax.Array # pylint:disable=invalid-name + qLD: jax.Array # pylint:disable=invalid-name + qLDiagInv: jax.Array # pylint:disable=invalid-name + bvh_aabb_dyn: jax.Array + bvh_active: jax.Array + # position, velocity dependent: + flexedge_velocity: jax.Array + ten_velocity: jax.Array + actuator_velocity: jax.Array + cdof_dot: jax.Array + qH: jax.Array # pylint:disable=invalid-name + qHDiagInv: jax.Array # pylint:disable=invalid-name + B_rownnz: jax.Array # pylint:disable=invalid-name + B_rowadr: jax.Array # pylint:disable=invalid-name + B_colind: jax.Array # pylint:disable=invalid-name + M_rownnz: jax.Array # pylint:disable=invalid-name + M_rowadr: jax.Array # pylint:disable=invalid-name + M_colind: jax.Array # pylint:disable=invalid-name + mapM2M: jax.Array # pylint:disable=invalid-name + C_rownnz: jax.Array # pylint:disable=invalid-name + C_rowadr: jax.Array # pylint:disable=invalid-name + C_colind: jax.Array # pylint:disable=invalid-name + mapM2C: jax.Array # pylint:disable=invalid-name + D_rownnz: jax.Array # pylint:disable=invalid-name + D_rowadr: jax.Array # pylint:disable=invalid-name + D_diag: jax.Array # pylint:disable=invalid-name + D_colind: jax.Array # pylint:disable=invalid-name + mapM2D: jax.Array # pylint:disable=invalid-name + mapD2M: jax.Array # pylint:disable=invalid-name + qDeriv: jax.Array # pylint:disable=invalid-name + qLU: jax.Array # pylint:disable=invalid-name + qfrc_spring: jax.Array + qfrc_damper: jax.Array + cacc: jax.Array + cfrc_int: jax.Array + cfrc_ext: jax.Array + subtree_linvel: jax.Array + subtree_angmom: jax.Array + # dynamically sized arrays which are made static for the frontend JAX API + # TODO(stunya): remove these dynamic fields entirely + contact: Contact + efc_type: jax.Array + efc_J: jax.Array # pylint:disable=invalid-name + efc_pos: jax.Array + efc_margin: jax.Array + efc_frictionloss: jax.Array + efc_D: jax.Array # pylint:disable=invalid-name + efc_aref: jax.Array + efc_force: jax.Array + + +class DataJAX(PyTreeNode): + """JAX-specific data.""" + + ne: int + nf: int + nl: int + nefc: int + ncon: int + solver_niter: jax.Array + cdof: jax.Array + cinert: jax.Array + ten_wrapadr: jax.Array + ten_wrapnum: jax.Array + ten_J: jax.Array # pylint:disable=invalid-name + ten_length: jax.Array + wrap_obj: jax.Array + wrap_xpos: jax.Array + actuator_length: jax.Array + actuator_moment: jax.Array + crb: jax.Array + qM: jax.Array # pylint:disable=invalid-name + qLD: jax.Array # pylint:disable=invalid-name + qLDiagInv: jax.Array # pylint:disable=invalid-name + ten_velocity: jax.Array + actuator_velocity: jax.Array + cdof_dot: jax.Array + cacc: jax.Array + cfrc_int: jax.Array + cfrc_ext: jax.Array + subtree_linvel: jax.Array + subtree_angmom: jax.Array + # dynamically sized data which are made static due to JAX limitations + contact: Contact + efc_type: jax.Array + efc_J: jax.Array # pylint:disable=invalid-name + efc_pos: jax.Array + efc_margin: jax.Array + efc_frictionloss: jax.Array + efc_D: jax.Array # pylint:disable=invalid-name + efc_aref: jax.Array + efc_force: jax.Array + + +class Data(PyTreeNode): + """Dynamic state that updates each step.""" + # global properties: time: jax.Array # state: @@ -1419,98 +1101,45 @@ class Data(PyTreeNode): site_xmat: jax.Array cam_xpos: jax.Array cam_xmat: jax.Array - light_xpos: jax.Array = _restricted_to('mujoco') - light_xdir: jax.Array = _restricted_to('mujoco') subtree_com: jax.Array - cdof: jax.Array - cinert: jax.Array - flexvert_xpos: jax.Array = _restricted_to('mujoco') - flexelem_aabb: jax.Array - flexedge_J_rownnz: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - flexedge_J_rowadr: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - flexedge_J_colind: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - flexedge_J: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - flexedge_length: jax.Array = _restricted_to('mujoco') - ten_wrapadr: jax.Array - ten_wrapnum: jax.Array - ten_J_rownnz: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - ten_J_rowadr: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - ten_J_colind: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - ten_J: jax.Array # pylint:disable=invalid-name - ten_length: jax.Array - wrap_obj: jax.Array - wrap_xpos: jax.Array - actuator_length: jax.Array - moment_rownnz: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - moment_rowadr: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - moment_colind: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - actuator_moment: jax.Array - crb: jax.Array - qM: jax.Array # pylint:disable=invalid-name - qLD: jax.Array # pylint:disable=invalid-name - qLDiagInv: jax.Array # pylint:disable=invalid-name - bvh_aabb_dyn: jax.Array = _restricted_to('mujoco') - bvh_active: jax.Array = _restricted_to('mujoco') - # position, velocity dependent: - flexedge_velocity: jax.Array = _restricted_to('mujoco') - ten_velocity: jax.Array - actuator_velocity: jax.Array cvel: jax.Array - cdof_dot: jax.Array qfrc_bias: jax.Array - qfrc_spring: jax.Array = _restricted_to('mujoco') - qfrc_damper: jax.Array = _restricted_to('mujoco') qfrc_gravcomp: jax.Array qfrc_fluid: jax.Array qfrc_passive: jax.Array - subtree_linvel: jax.Array - subtree_angmom: jax.Array - qH: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - qHDiagInv: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - B_rownnz: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - B_rowadr: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - B_colind: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - M_rownnz: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - M_rowadr: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - M_colind: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - mapM2M: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - C_rownnz: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - C_rowadr: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - C_colind: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - mapM2C: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - D_rownnz: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - D_rowadr: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - D_diag: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - D_colind: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - mapM2D: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - mapD2M: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - qDeriv: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - qLU: jax.Array = _restricted_to('mujoco') # pylint:disable=invalid-name - # position, velocity, control & acceleration dependent: qfrc_actuator: jax.Array actuator_force: jax.Array qfrc_smooth: jax.Array qacc_smooth: jax.Array qfrc_constraint: jax.Array qfrc_inverse: jax.Array - cacc: jax.Array - cfrc_int: jax.Array - cfrc_ext: jax.Array - # dynamically sized - contact: Contact - # dynamically sized - position dependent: - efc_type: jax.Array - efc_J: jax.Array # pylint:disable=invalid-name - efc_pos: jax.Array - efc_margin: jax.Array - efc_frictionloss: jax.Array - efc_D: jax.Array # pylint:disable=invalid-name - # dynamically sized - position & velocity dependent: - efc_aref: jax.Array - # dynamically sized - position, velocity, control & acceleration dependent: - efc_force: jax.Array - # sparse representation of qM, qLD, qLDiagInv, for compatibility with MuJoCo - # when in dense mode - _qM_sparse: jax.Array = _restricted_to('mjx') # pylint:disable=invalid-name - _qLD_sparse: jax.Array = _restricted_to('mjx') # pylint:disable=invalid-name - _qLDiagInv_sparse: jax.Array = _restricted_to('mjx') # pylint:disable=invalid-name + _impl: Union[DataC, DataJAX] + + @property + def backend_impl(self) -> BackendImpl: + return { + DataC: BackendImpl.C, + DataJAX: BackendImpl.JAX, + }[type(self._impl)] + + def __getattr__(self, name: str): + if name == 'value': + # Special case for NNX, the value attribute may not exist on the parent + # PyTreeNode, before it exists on the child PyTreeNode. Thanks NNX. + return object.__getattribute__(self, 'value') + + try: + impl_instsance = object.__getattribute__(self, '_impl') + val = getattr(impl_instsance, name) + warnings.warn( + f'Accessing `{name}` directly from `Data` is deprecated. ' + f'Access it via `data._impl.{name}` instead.', + DeprecationWarning, + stacklevel=2, + ) + except AttributeError: + # raise the standard exception + raise AttributeError( # pylint: disable=raise-missing-from + f"'{type(self).__name__}' object has no attribute '{name}'" + ) + return val From f4774a544938383343fe911cde8b94e1dae02336 Mon Sep 17 00:00:00 2001 From: Kyle Bayes Date: Thu, 8 May 2025 02:41:06 -0700 Subject: [PATCH 100/191] Fix bug in cached meshes where convex hull was missing. Previously if a mesh was copied from the cache it would not have its convex hull if the original mesh that was cached didn't previously compute it. Fixes #2609 PiperOrigin-RevId: 756223337 Change-Id: I54705456636c270607a17340b249047e0df75d0b --- src/user/user_cache.cc | 6 ++-- src/user/user_cache.h | 8 ++--- src/user/user_mesh.cc | 65 +++++++++++++----------------------- src/user/user_objects.cc | 19 +++++++---- test/user/user_cache_test.cc | 1 + test/user/user_mesh_test.cc | 46 +++++++++++++++++++++++-- 6 files changed, 88 insertions(+), 57 deletions(-) diff --git a/src/user/user_cache.cc b/src/user/user_cache.cc index 715e8e6a..ee22d7eb 100644 --- a/src/user/user_cache.cc +++ b/src/user/user_cache.cc @@ -102,7 +102,8 @@ bool mjCCache::Insert(const std::string& modelname, const mjResource *resource, -// populate data from the cache into the given function +// populate data from the cache into the given function, return true if data was +// copied bool mjCCache::PopulateData(const mjResource* resource, mjCDataFunc fn) { std::lock_guard lock(mutex_); auto it = lookup_.find(resource->name); @@ -121,8 +122,7 @@ bool mjCCache::PopulateData(const mjResource* resource, mjCDataFunc fn) { entries_.erase(asset); entries_.insert(asset); - asset->PopulateData(fn); - return true; + return asset->PopulateData(fn); } diff --git a/src/user/user_cache.h b/src/user/user_cache.h index a2d5577f..4b0e9288 100644 --- a/src/user/user_cache.h +++ b/src/user/user_cache.h @@ -28,7 +28,7 @@ #include -typedef std::function mjCDataFunc; +typedef std::function mjCDataFunc; typedef void (*mjCDeallocFunc)(const void*); // A class container for a thread-safe asset cache @@ -57,9 +57,9 @@ class mjCAsset { std::size_t InsertNum() const { return insert_num_; } std::size_t AccessCount() const { return access_count_; } - // pass data in the cache to the given function - void PopulateData(mjCDataFunc fn) const { - fn(data_.get()); + // pass data in the cache to the given function, return true if data was copied + bool PopulateData(mjCDataFunc fn) const { + return fn(data_.get()); } private: diff --git a/src/user/user_mesh.cc b/src/user/user_mesh.cc index f999fc9a..e03896ed 100644 --- a/src/user/user_mesh.cc +++ b/src/user/user_mesh.cc @@ -1012,30 +1012,28 @@ void mjCMesh::LoadOBJ(mjResource* resource, bool remove_repeated) { // load mesh from cached asset, return true on success bool mjCMesh::LoadCachedMesh(mjCCache *cache, const mjResource* resource) { - // save previous mesh properties (in case different from cached mesh) - int maxhullvert = maxhullvert_; - mjtMeshInertia old_inertia = inertia; - double old_scale[3] = {scale[0], scale[1], scale[2]}; - auto process_mesh = [&](const void* data) { const mjCMesh* mesh = static_cast(data); // check if maxhullvert is different - maxhullvert_ = mesh->maxhullvert_; - if (maxhullvert != mesh->maxhullvert_) { - return; + if (maxhullvert_ != mesh->maxhullvert_) { + return false; } // check if inertia is different - inertia = mesh->inertia; - if (old_inertia != mesh->inertia) { - return; + if (inertia != mesh->inertia) { + return false; } // check if scale is different - memcpy(scale, mesh->scale, 3*sizeof(double)); - if (old_scale[0] != mesh->scale[0] || old_scale[1] != mesh->scale[1] || - old_scale[2] != mesh->scale[2]) { - return; + if (scale[0] != mesh->scale[0] || + scale[1] != mesh->scale[1] || + scale[2] != mesh->scale[2]) { + return false; + } + + // check if need hull + if (needhull_ && !mesh->szgraph_) { + return false; } processed_ = mesh->processed_; @@ -1047,11 +1045,14 @@ bool mjCMesh::LoadCachedMesh(mjCCache *cache, const mjResource* resource) { facetexcoord_ = mesh->facetexcoord_; halfedge_ = mesh->halfedge_; - szgraph_ = mesh->szgraph_; - graph_ = nullptr; - if (szgraph_) { - graph_ = (int*)mju_malloc(szgraph_*sizeof(int)); - std::copy(mesh->graph_, mesh->graph_ + szgraph_, graph_); + // only copy graph if needed + if (needhull_ || mesh->face_.empty()) { + szgraph_ = mesh->szgraph_; + graph_ = nullptr; + if (szgraph_) { + graph_ = (int*)mju_malloc(szgraph_*sizeof(int)); + std::copy(mesh->graph_, mesh->graph_ + szgraph_, graph_); + } } polygons_ = mesh->polygons_; @@ -1072,29 +1073,11 @@ bool mjCMesh::LoadCachedMesh(mjCCache *cache, const mjResource* resource) { } tree_ = mesh->tree_; face_aabb_ = mesh->face_aabb_; + return true; }; - // check that cached asset has all data, make sure no metadata has changed - if (!cache->PopulateData(resource, process_mesh)) { - return false; - } - - if (maxhullvert != maxhullvert_) { - maxhullvert_ = maxhullvert; - return false; - } - if (inertia != old_inertia) { - inertia = old_inertia; - return false; - } - if (scale[0] != old_scale[0] || scale[1] != old_scale[1] || - scale[2] != old_scale[2]) { - scale[0] = old_scale[0]; - scale[1] = old_scale[1]; - scale[2] = old_scale[2]; - return false; - } - return true; + // check that cached asset has all data + return cache->PopulateData(resource, process_mesh); } // load STL binary mesh diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index cff97b72..82853307 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -75,14 +75,19 @@ PNGImage PNGImage::Load(const mjCBase* obj, mjResource* resource, image.color_type_ = color_type; mjCCache *cache = reinterpret_cast(mj_globalCache()); + // cache callback + auto callback = [&image](const void* data) { + const PNGImage *cached_image = static_cast(data); + if (cached_image->color_type_ == image.color_type_) { + image = *cached_image; + return true; + } + return false; + }; + // try loading from cache - if (cache && cache->PopulateData(resource, [&image](const void* data) { - const PNGImage *cached_image = static_cast(data); - if (cached_image->color_type_ == image.color_type_) { - image = *cached_image; - } - })) { - if (!image.data_.empty()) return image; + if (cache && cache->PopulateData(resource, callback)) { + return image; } // open PNG resource diff --git a/test/user/user_cache_test.cc b/test/user/user_cache_test.cc index f226827b..82d0f57b 100644 --- a/test/user/user_cache_test.cc +++ b/test/user/user_cache_test.cc @@ -65,6 +65,7 @@ GetCachedText(mjCCache& cache, const std::string& model, bool inserted = cache.PopulateData(resource, [&cached_text](const void* data) { cached_text = *(static_cast(data)); + return true; }); mju_closeResource(resource); mj_deleteVFS(&vfs); diff --git a/test/user/user_mesh_test.cc b/test/user/user_mesh_test.cc index b9c1f3af..759561b2 100644 --- a/test/user/user_mesh_test.cc +++ b/test/user/user_mesh_test.cc @@ -42,14 +42,14 @@ static const char* const kDuplicateVerticesPath = "user/testdata/duplicate_vertices.xml"; static const char* const kCubePath = "user/testdata/cube.xml"; +static const char* const kCubeCompletePath = + "user/testdata/cube_complete.obj"; static const char* const kTorusPath = "user/testdata/torus.xml"; static const char* const kTorusMaxhullVertPath = "user/testdata/torus_maxhullvert.xml"; static const char* const kTorusDefaultMaxhullVertPath = "user/testdata/torus_maxhullvert_default.xml"; -static const char* const kTorusShellPath = - "user/testdata/torus_shell.xml"; static const char* const kCompareInertiaPath = "user/testdata/inertia_compare.xml"; static const char* const kConvexInertiaPath = @@ -1207,6 +1207,48 @@ TEST_F(MjCMeshTest, InvalidIndexInFace) { mj_deleteModel(model); } +TEST_F(MjCMeshTest, QhullCache) { + static constexpr char xml1[] = R"( + + + + + + + + + )"; + + static constexpr char xml2[] = R"( + + + + + + + + + )"; + + mjVFS vfs; + mj_defaultVFS(&vfs); + mj_addFileVFS(&vfs, "", GetTestDataFilePath(kCubeCompletePath).c_str()); + + std::array error; + mjModel* model = LoadModelFromString(xml1, error.data(), error.size(), &vfs); + ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error.data(); + EXPECT_THAT(model->mesh_graphadr[0], -1); + + mj_deleteModel(model); + + model = LoadModelFromString(xml2, error.data(), error.size(), &vfs); + ASSERT_THAT(model, NotNull()) << "Failed to load model: " << error.data(); + EXPECT_GT(model->mesh_graphadr[0], -1); + + mj_deleteModel(model); + mj_deleteVFS(&vfs); +} + TEST_F(MjCMeshTest, LoadSkin) { const std::string xml_path = GetTestDataFilePath(kCubeSkinPath); std::array error; From ad80a4625b3d7fd916709de3fab88d5e687b81e7 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Thu, 8 May 2025 09:45:43 -0700 Subject: [PATCH 101/191] Always generate a BVH in flex. The BVH is used also for computing normalized vertex coordinates in the trilinear interpolation, so we need to compute it also when the flex cannot collide. PiperOrigin-RevId: 756349244 Change-Id: Ib397c472e31251df5a7ea85850d83a62f3b2c949 --- src/user/user_mesh.cc | 3 ++- test/user/user_flex_test.cc | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/user/user_mesh.cc b/src/user/user_mesh.cc index e03896ed..93c10004 100644 --- a/src/user/user_mesh.cc +++ b/src/user/user_mesh.cc @@ -3624,8 +3624,9 @@ void mjCFlex::CreateBVH() { elemaabb_[6*e+5] = 0.5*(xmax[2]-xmin[2]) + radius; // add bounding volume for this element + // contype and conaffinity are set to nonzero to force bvh generation const double* aabb = elemaabb_.data() + 6*e; - tree.AddBoundingVolume(e, contype, conaffinity, aabb, nullptr, aabb); + tree.AddBoundingVolume(e, 1, 1, aabb, nullptr, aabb); nbvh++; } diff --git a/test/user/user_flex_test.cc b/test/user/user_flex_test.cc index a43b93f0..492c4170 100644 --- a/test/user/user_flex_test.cc +++ b/test/user/user_flex_test.cc @@ -243,6 +243,27 @@ TEST_F(UserFlexTest, RigidFlex) { mj_deleteModel(m); mj_deleteData(d); } + +TEST_F(UserFlexTest, FlexNotCollide) { + static constexpr char xml[] = R"( + + + + + + + + )"; + std::array error; + mjModel* m = LoadModelFromString(xml, error.data(), error.size()); + ASSERT_THAT(m, NotNull()) << error.data(); + mjData* d = mj_makeData(m); + mj_step(m, d); + mj_deleteModel(m); + mj_deleteData(d); +} + TEST_F(UserFlexTest, BoundingBoxCoordinates) { static constexpr char xml[] = R"( From baf84265b8627e6f868bc92ea6422e4e78dacb9c Mon Sep 17 00:00:00 2001 From: Taylor Howell Date: Fri, 9 May 2025 04:12:17 -0700 Subject: [PATCH 102/191] Improve wrap_circle. PiperOrigin-RevId: 756704385 Change-Id: Id44ba317c6072790763a958f4227a76278d3ab74 --- src/engine/engine_util_misc.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/engine/engine_util_misc.c b/src/engine/engine_util_misc.c index b7f06601..03435f48 100644 --- a/src/engine/engine_util_misc.c +++ b/src/engine/engine_util_misc.c @@ -107,12 +107,12 @@ static mjtNum wrap_circle(mjtNum pnt[4], const mjtNum end[4], const mjtNum* side return -1; } + mjtNum sqrt0 = mju_sqrt(sqlen0 - sqrad); + mjtNum sqrt1 = mju_sqrt(sqlen1 - sqrad); + // construct the two solutions, compute goodness mjtNum sol[2][2][2], good[2]; for (int i=0; i < 2; i++) { - mjtNum sqrt0 = mju_sqrt(sqlen0 - sqrad); - mjtNum sqrt1 = mju_sqrt(sqlen1 - sqrad); - int sgn = (i == 0 ? 1 : -1); sol[i][0][0] = (end[0]*sqrad + sgn*radius*end[1]*sqrt0)/sqlen0; From 924ee3070ae8addb16a3a00e037113601be2111d Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Fri, 9 May 2025 11:02:09 -0700 Subject: [PATCH 103/191] Modify user values in mujoco to allow users to provide cleanup functions to avoid memory leaks. For the MJCF -> USD plugin, and I suspect other usecases for user values, it's necessary to give ownership of the object to Mujoco. However since they get type erased, we can't clean up the data automatically for the user. Instead this allows c++ clients to provide a cleanup function with their data. PiperOrigin-RevId: 756832010 Change-Id: I80b8e7822e1a0e399a0d19dcaa57ee69b3ccc16a --- doc/APIreference/functions.rst | 11 +++++ doc/includes/references.h | 3 ++ include/mujoco/mujoco.h | 7 ++++ .../introspect/codegen/generate_functions.py | 9 +++- src/user/user_api.cc | 14 ++++--- src/user/user_api.h | 5 +++ src/user/user_objects.cc | 9 ++-- src/user/user_objects.h | 41 ++++++++++++++++++- test/user/user_api_test.cc | 8 ++++ 9 files changed, 92 insertions(+), 15 deletions(-) diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index 9606e94d..e13ad19d 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -4459,6 +4459,17 @@ Transform body into a frame. Set user payload, overriding the existing value for the specified key if present. +.. _mjs_setUserValueWithCleanup: + +`mjs_setUserValueWithCleanup <#mjs_setUserValueWithCleanup>`__ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. mujoco-include:: mjs_setUserValueWithCleanup + +Set user payload, overriding the existing value for the specified key if +present. This version differs from mjs_setUserValue in that it takes a +cleanup function that will be called when the user payload is deleted. + .. _mjs_getUserValue: `mjs_getUserValue <#mjs_getUserValue>`__ diff --git a/doc/includes/references.h b/doc/includes/references.h index 6ebc759a..0689e150 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -3732,6 +3732,9 @@ const char* mjs_resolveOrientation(double quat[4], mjtByte degree, const char* s const mjsOrientation* orientation); mjsFrame* mjs_bodyToFrame(mjsBody** body); void mjs_setUserValue(mjsElement* element, const char* key, const void* data); +void mjs_setUserValueWithCleanup(mjsElement* element, const char* key, + const void* data, + void (*cleanup)(const void*)); const void* mjs_getUserValue(mjsElement* element, const char* key); void mjs_deleteUserValue(mjsElement* element, const char* key); void mjs_defaultSpec(mjSpec* spec); diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index 63bfafcc..5bca71ea 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -1644,6 +1644,13 @@ MJAPI mjsFrame* mjs_bodyToFrame(mjsBody** body); // Set user payload, overriding the existing value for the specified key if present. MJAPI void mjs_setUserValue(mjsElement* element, const char* key, const void* data); +// Set user payload, overriding the existing value for the specified key if +// present. This version differs from mjs_setUserValue in that it takes a +// cleanup function that will be called when the user payload is deleted. +MJAPI void mjs_setUserValueWithCleanup(mjsElement* element, const char* key, + const void* data, + void (*cleanup)(const void*)); + // Return user payload or NULL if none found. MJAPI const void* mjs_getUserValue(mjsElement* element, const char* key); diff --git a/python/mujoco/introspect/codegen/generate_functions.py b/python/mujoco/introspect/codegen/generate_functions.py index 1966e328..06beeaf2 100644 --- a/python/mujoco/introspect/codegen/generate_functions.py +++ b/python/mujoco/introspect/codegen/generate_functions.py @@ -99,8 +99,13 @@ class MjFunctionVisitor: return ''.join(strings) def visit(self, node: ClangJsonNode) -> None: - if (node.get('kind') == 'FunctionDecl' and - node.get('name', '').startswith('mj')): + # Skip mjs_setUserValueWithCleanup as it's only useful for heap allocated + # objects and doesn't need a python wrapper. + if ( + node.get('kind') == 'FunctionDecl' + and node.get('name', '').startswith('mj') + and node.get('name', '') != 'mjs_setUserValueWithCleanup' + ): func_decl = self._make_function(node) self._functions[func_decl.name] = func_decl diff --git a/src/user/user_api.cc b/src/user/user_api.cc index e103aa8e..41cba2d2 100644 --- a/src/user/user_api.cc +++ b/src/user/user_api.cc @@ -860,15 +860,17 @@ mjsFrame* mjs_bodyToFrame(mjsBody** body) { return &frameC->spec; } - - -// set user payload void mjs_setUserValue(mjsElement* element, const char* key, const void* data) { - mjCBase* baseC = static_cast(element); - baseC->SetUserValue(key, data); + mjs_setUserValueWithCleanup(element, key, data, nullptr); } - +// set user payload +void mjs_setUserValueWithCleanup(mjsElement* element, const char* key, + const void* data, + void (*cleanup)(const void*)) { + mjCBase* baseC = static_cast(element); + baseC->SetUserValue(key, data, cleanup); +} // return user payload or NULL if none found const void* mjs_getUserValue(mjsElement* element, const char* key) { diff --git a/src/user/user_api.h b/src/user/user_api.h index e1b73d86..cd228147 100644 --- a/src/user/user_api.h +++ b/src/user/user_api.h @@ -374,6 +374,11 @@ MJAPI mjsFrame* mjs_bodyToFrame(mjsBody** body); // Set user payload. MJAPI void mjs_setUserValue(mjsElement* element, const char* key, const void* data); +// Set user payload. +MJAPI void mjs_setUserValueWithCleanup(mjsElement* element, const char* key, + const void* data, + void (*cleanup)(const void*)); + // Return user payload or NULL if none found. MJAPI const void* mjs_getUserValue(mjsElement* element, const char* key); diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 82853307..51c0f63a 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -784,15 +784,14 @@ void mjCBase::SetFrame(mjCFrame* _frame) { frame = _frame; } - -void mjCBase::SetUserValue(std::string_view key, const void* data) { - user_payload_[std::string(key)] = data; +void mjCBase::SetUserValue(std::string_view key, const void* data, + void (*cleanup)(const void*)) { + user_payload_[std::string(key)] = UserValue(data, cleanup); } - const void* mjCBase::GetUserValue(std::string_view key) { auto found = user_payload_.find(std::string(key)); - return found != user_payload_.end() ? found->second : nullptr; + return found != user_payload_.end() ? found->second.value : nullptr; } diff --git a/src/user/user_objects.h b/src/user/user_objects.h index bfec0797..9cdae417 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -281,7 +281,8 @@ class mjCBase : public mjCBase_ { } // Set and get user payload - void SetUserValue(std::string_view key, const void* data); + void SetUserValue(std::string_view key, const void* data, + void (*cleanup)(const void*)); const void* GetUserValue(std::string_view key); void DeleteUserValue(std::string_view key); @@ -292,8 +293,44 @@ class mjCBase : public mjCBase_ { // reference count for allowing deleting an attached object int refcount = 1; + // Arbitrary user value that cleans up the data when destroyed. + struct UserValue { + const void* value = nullptr; + void (*cleanup)(const void*) = nullptr; + + UserValue() {} + UserValue(const void* value, void (*cleanup)(const void*)) + : value(value), cleanup(cleanup) {} + UserValue(const UserValue& other) = delete; + UserValue& operator=(const UserValue& other) = delete; + + UserValue(UserValue&& other) : value(other.value), cleanup(other.cleanup) { + other.value = nullptr; + other.cleanup = nullptr; + } + + UserValue& operator=(UserValue&& other) { + if (this != &other) { + if (cleanup && value) { + cleanup(value); + } + value = other.value; + cleanup = other.cleanup; + other.value = nullptr; + other.cleanup = nullptr; + } + return *this; + } + + ~UserValue() { + if (cleanup && value) { + cleanup(value); + } + } + }; + // user payload - std::unordered_map user_payload_; + std::unordered_map user_payload_; }; diff --git a/test/user/user_api_test.cc b/test/user/user_api_test.cc index 4db9f25b..1669ea20 100644 --- a/test/user/user_api_test.cc +++ b/test/user/user_api_test.cc @@ -2845,6 +2845,14 @@ TEST_F(MujocoTest, UserValue) { EXPECT_STREQ(static_cast(payload), data.c_str()); mjs_deleteUserValue(body->element, "key"); EXPECT_THAT(mjs_getUserValue(body->element, "key"), IsNull()); + + std::string* heap_data = new std::string("heap_data"); + mjs_setUserValueWithCleanup( + body->element, "key", heap_data, + [](const void* data) { delete static_cast(data); }); + payload = mjs_getUserValue(body->element, "key"); + EXPECT_STREQ(static_cast(payload)->c_str(), + heap_data->c_str()); mj_deleteSpec(spec); } From b0b2c48190d4c2d73277e95196d73284028819cd Mon Sep 17 00:00:00 2001 From: Taylor Howell Date: Mon, 12 May 2025 02:44:42 -0700 Subject: [PATCH 104/191] Update comment in mj_energyPos. PiperOrigin-RevId: 757670899 Change-Id: I3827089f4298b3800c20ed64ca0ccfcf3ed83d92 --- src/engine/engine_sensor.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/engine_sensor.c b/src/engine/engine_sensor.c index 936dc085..0ff91a09 100644 --- a/src/engine/engine_sensor.c +++ b/src/engine/engine_sensor.c @@ -948,7 +948,7 @@ void mj_energyPos(const mjModel* m, mjData* d) { mjFALLTHROUGH; case mjJNT_BALL: - // covert quatertion difference into angular "velocity" + // convert quaternion difference into angular "velocity" mju_copy4(quat, d->qpos+padr); mju_normalize4(quat); mju_subQuat(dif, d->qpos + padr, m->qpos_spring + padr); From 9accc7ae51764445658b594112e591ee37f258d8 Mon Sep 17 00:00:00 2001 From: Taylor Howell Date: Mon, 12 May 2025 03:12:10 -0700 Subject: [PATCH 105/191] Update mj_energyPos. PiperOrigin-RevId: 757678945 Change-Id: If236b9e8f376a2673abc87e818326e9188f6c15a --- src/engine/engine_passive.c | 2 +- src/engine/engine_sensor.c | 4 +--- test/engine/engine_sensor_test.cc | 27 +++++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/engine/engine_passive.c b/src/engine/engine_passive.c index 40296906..3c025804 100644 --- a/src/engine/engine_passive.c +++ b/src/engine/engine_passive.c @@ -84,7 +84,7 @@ static void mj_springdamper(const mjModel* m, mjData* d) { case mjJNT_BALL: { - // convert quatertion difference into angular "velocity" + // convert quaternion difference into angular "velocity" mjtNum dif[3], quat[4]; mju_copy4(quat, d->qpos+padr); mju_normalize4(quat); diff --git a/src/engine/engine_sensor.c b/src/engine/engine_sensor.c index 0ff91a09..485474c1 100644 --- a/src/engine/engine_sensor.c +++ b/src/engine/engine_sensor.c @@ -938,9 +938,7 @@ void mj_energyPos(const mjModel* m, mjData* d) { switch ((mjtJoint) m->jnt_type[i]) { case mjJNT_FREE: - mju_copy4(quat, d->qpos+padr); - mju_normalize4(quat); - mju_sub3(dif, quat, m->qpos_spring+padr); + mju_sub3(dif, d->qpos+padr, m->qpos_spring+padr); d->energy[0] += 0.5*stiffness*mju_dot3(dif, dif); // continue with rotations diff --git a/test/engine/engine_sensor_test.cc b/test/engine/engine_sensor_test.cc index 5aa2aa5b..b5f0125a 100644 --- a/test/engine/engine_sensor_test.cc +++ b/test/engine/engine_sensor_test.cc @@ -454,6 +454,33 @@ TEST_F(SensorTest, PotentialEnergy) { mj_deleteModel(model); } +TEST_F(SensorTest, PotentialEnergyFreeJointSpring) { + constexpr char xml[] = R"( + + + )"; + mjModel* model = LoadModelFromString(xml); + mjData* data = mj_makeData(model); + data->qpos[0] = 1; + data->qpos[1] = 2; + data->qpos[2] = 3; + mj_forward(model, data); + EXPECT_EQ(data->sensordata[0], 0.5*2*14); + + mj_deleteData(data); + mj_deleteModel(model); +} + TEST_F(SensorTest, KineticEnergy) { constexpr char xml[] = R"( From 85dd78d6f34ef976ab99eebd2a36f329fd54f555 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 12 May 2025 03:51:31 -0700 Subject: [PATCH 106/191] Add private function `mj_makeM`. PiperOrigin-RevId: 757689756 Change-Id: Ie8bb49d9f18a95da2e3fd8622e30f9266a1ced98 --- src/engine/engine_core_smooth.c | 11 ++++++++--- src/engine/engine_core_smooth.h | 3 +++ src/engine/engine_forward.c | 17 +++++++++-------- src/engine/engine_inverse.c | 3 +-- src/engine/engine_setconst.c | 3 +-- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/engine/engine_core_smooth.c b/src/engine/engine_core_smooth.c index efd74614..b2bdc8b5 100644 --- a/src/engine/engine_core_smooth.c +++ b/src/engine/engine_core_smooth.c @@ -1471,7 +1471,6 @@ void mj_transmission(const mjModel* m, mjData* d) { // add tendon armature to qM void mj_tendonArmature(const mjModel* m, mjData* d) { - TM_START; int nv = m->nv, ntendon = m->ntendon, issparse = mj_isSparse(m); for (int k=0; k < ntendon; k++) { @@ -1521,14 +1520,12 @@ void mj_tendonArmature(const mjModel* m, mjData* d) { } } } - TM_END(mjTIMER_POS_INERTIA); } // composite rigid body inertia algorithm void mj_crb(const mjModel* m, mjData* d) { - TM_START; mjtNum buf[6]; mjtNum* crb = d->crb; int last_body = m->nbody - 1, nv = m->nv; @@ -1574,6 +1571,14 @@ void mj_crb(const mjModel* m, mjData* d) { d->qM[Madr_ij++] += mju_dot(d->cdof+6*j, buf, 6); } } +} + + + +void mj_makeM(const mjModel* m, mjData* d) { + TM_START; + mj_crb(m, d); + mj_tendonArmature(m, d); TM_END(mjTIMER_POS_INERTIA); } diff --git a/src/engine/engine_core_smooth.h b/src/engine/engine_core_smooth.h index 38decdca..a2db11cf 100644 --- a/src/engine/engine_core_smooth.h +++ b/src/engine/engine_core_smooth.h @@ -54,6 +54,9 @@ MJAPI void mj_crb(const mjModel* m, mjData* d); // add tendon armature to qM MJAPI void mj_tendonArmature(const mjModel* m, mjData* d); +// make inertia matrix +void mj_makeM(const mjModel* m, mjData* d); + // sparse L'*D*L factorizaton of inertia-like matrix M, assumed spd (legacy implementation) MJAPI void mj_factorI_legacy(const mjModel* m, mjData* d, const mjtNum* M, mjtNum* qLD, mjtNum* qLDiagInv); diff --git a/src/engine/engine_forward.c b/src/engine/engine_forward.c index b409e1ea..b48b5804 100644 --- a/src/engine/engine_forward.c +++ b/src/engine/engine_forward.c @@ -114,16 +114,15 @@ typedef struct mjFwdPositionArgs_ mjFwdPositionArgs; // wrapper for mj_crb and mj_factorM void* mj_inertialThreaded(void* args) { mjFwdPositionArgs* forward_args = (mjFwdPositionArgs*) args; - mj_crb(forward_args->m, forward_args->d); // timed internally (POS_INERTIA) - mj_tendonArmature(forward_args->m, forward_args->d); // timed internally (POS_INERTIA) - mj_factorM(forward_args->m, forward_args->d); // timed internally (POS_INERTIA) + mj_makeM(forward_args->m, forward_args->d); + mj_factorM(forward_args->m, forward_args->d); return NULL; } // wrapper for mj_collision void* mj_collisionThreaded(void* args) { mjFwdPositionArgs* forward_args = (mjFwdPositionArgs*) args; - mj_collision(forward_args->m, forward_args->d); // timed internally (POS_COLLISION) + mj_collision(forward_args->m, forward_args->d); return NULL; } @@ -143,10 +142,12 @@ void mj_fwdPosition(const mjModel* m, mjData* d) { // no threadpool: inertia and collision on main thread if (!d->threadpool) { - mj_crb(m, d); // timed internally (POS_INERTIA) - mj_tendonArmature(m, d); // timed internally (POS_INERTIA) - mj_factorM(m, d); // timed internally (POS_INERTIA) - mj_collision(m, d); // timed internally (POS_COLLISION) + // inertia, timed internally (POS_INERTIA) + mj_makeM(m, d); + mj_factorM(m, d); + + // collision, timed internally (POS_COLLISION) + mj_collision(m, d); } // have threadpool: inertia and collision on separate threads diff --git a/src/engine/engine_inverse.c b/src/engine/engine_inverse.c index 57005f1e..de8b9a52 100644 --- a/src/engine/engine_inverse.c +++ b/src/engine/engine_inverse.c @@ -46,8 +46,7 @@ void mj_invPosition(const mjModel* m, mjData* d) { mj_tendon(m, d); TM_END(mjTIMER_POS_KINEMATICS); - mj_crb(m, d); // timed internally (POS_INERTIA) - mj_tendonArmature(m, d); // timed internally (POS_INERTIA) + mj_makeM(m, d); // timed internally (POS_INERTIA) mj_factorM(m, d); // timed internally (POS_INERTIA) mj_collision(m, d); // timed internally (POS_COLLISION) diff --git a/src/engine/engine_setconst.c b/src/engine/engine_setconst.c index eece506c..f737688c 100644 --- a/src/engine/engine_setconst.c +++ b/src/engine/engine_setconst.c @@ -103,8 +103,7 @@ static void set0(mjModel* m, mjData* d) { // run remaining computations mj_tendon(m, d); - mj_crb(m, d); - mj_tendonArmature(m, d); + mj_makeM(m, d); mj_factorM(m, d); mj_flex(m, d); mj_transmission(m, d); From af088adaee0b6e4354bbd8bbff5b1dbb70fdc8a3 Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Mon, 12 May 2025 04:57:14 -0700 Subject: [PATCH 107/191] Add mjPhysics schemas, starting with MjcPhysicsSceneAPI and MjcPhysicsSiteAPI PiperOrigin-RevId: 757710745 Change-Id: Ia920abecdf8c6a05f61dd0fd3a8e6bc001ccc7d0 --- src/experimental/usd/mjcPhysics/schema.usda | 524 ++++++++++++++++++++ 1 file changed, 524 insertions(+) create mode 100644 src/experimental/usd/mjcPhysics/schema.usda diff --git a/src/experimental/usd/mjcPhysics/schema.usda b/src/experimental/usd/mjcPhysics/schema.usda new file mode 100644 index 00000000..1a13a089 --- /dev/null +++ b/src/experimental/usd/mjcPhysics/schema.usda @@ -0,0 +1,524 @@ +#usda 1.0 +( + subLayers = [ + @usd/schema.usda@, + ] +) + +over "GLOBAL" ( + customData = { + string libraryName = "mujocoPhysics" + string libraryPath = "." + bool useLiteralIdentifier = 0 + dictionary libraryTokens = { + dictionary euler = { + string doc = """ + This token represents the Euler numerical integrator. + """ + } + + dictionary rk4 = { + string doc = """ + This token represents the RK4 numerical integrator. + """ + } + + dictionary implicit = { + string doc = """ + This token represents the implicit numerical integrator. + """ + } + + dictionary implicitfast = { + string doc = """ + This token represents the implicitfast numerical integrator. + """ + } + + dictionary pyramidal = { + string doc = """ + This token represents the pyramidal contact friction cone type. + """ + } + + dictionary elliptic = { + string doc = """ + This token represents the elliptic contact friction cone type. + """ + } + + dictionary dense = { + string doc = """ + This token represents the dense constraint Jacobian and matrices + computed from it. + """ + } + + dictionary sparse = { + string doc = """ + This token represents the sparse constraint Jacobian and matrices + computed from it. + """ + } + + dictionary auto = { + string doc = """ + This token represents the auto constraint Jacobian and matrices + computed from it. + """ + } + + dictionary pgs = { + string doc = """ + This token represents the PGS constraint solver algorithm. + """ + } + + dictionary cg = { + string doc = """ + This token represents the CG constraint solver algorithm. + """ + } + + dictionary newton = { + string doc = """ + This token represents the Newton constraint solver algorithm. + """ + } + } + } +) +{ + +} + +class "MjcPhysicsSceneAPI" +( + customData = { + string className = "SceneAPI" + } + doc = """API providing global simulation options for Mujoco.""" + + inherits = +) +{ + uniform double mjc:physics:timestep = 0.002 ( + customData = { + string apiName = "Timestep" + } + displayName = "Timestep" + doc = """Controls the timestep in seconds used by MuJoCo.""" + ) + + uniform double mjc:physics:apirate = 100 ( + customData = { + string apiName = "ApiRate" + } + displayName = "ApiRate" + doc = """Determines the rate (in Hz) at which an external API allows + the update function to be executed.""" + ) + + uniform double mjc:physics:impratio = 1.0 ( + customData = { + string apiName = "ImpRatio" + } + displayName = "Impedance Ratio" + doc = """Ratio of frictional-to-normal constraint impedance for elliptic + friction cones.""" + ) + + uniform double3 mjc:physics:wind = (0.0, 0.0, 0.0) ( + customData = { + string apiName = "Wind" + } + displayName = "Wind Velocity" + doc = """Velocity vector of medium (i.e. wind).""" + ) + + uniform double3 mjc:physics:magnetic = (0.0, -0.5, 0.0) ( + customData = { + string apiName = "Magnetic" + } + displayName = "Magnetic Flux" + doc = """Global magnetic flux.""" + ) + + uniform double mjc:physics:density = 0.0 ( + customData = { + string apiName = "Density" + } + displayName = "Density" + doc = """Density of medium.""" + ) + + uniform double mjc:physics:viscosity = 0.0 ( + customData = { + string apiName = "Viscosity" + } + displayName = "Viscosity" + doc = """Viscosity of medium.""" + ) + + uniform double mjc:physics:o_margin = 0.0 ( + customData = { + string apiName = "OMargin" + } + displayName = "Contact Override Margin" + doc = """Replaces the margin parameter of all active contact pairs when + Contact override is enabled.""" + ) + + uniform double[] mjc:physics:o_solref = [0.02, 1.0] ( + customData = { + string apiName = "OSolRef" + } + displayName = "Contact Override SolRef" + doc = """Replaces the solref parameter of all active contact pairs when + Contact override is enabled.""" + ) + + uniform double[] mjc:physics:o_solimp = [0.9, 0.95, 0.001, 0.5, 2.0] ( + customData = { + string apiName = "OSolImp" + } + displayName = "Contact Override SolImp" + doc = """Replaces the solimp parameter of all active contact pairs when + Contact override is enabled.""" + ) + + uniform double[] mjc:physics:o_friction = [1.0, 1.0, 0.005, 0.0001, 0.0001] ( + customData = { + string apiName = "OFriction" + } + displayName = "Contact Override Friction" + doc = """Replaces the friction parameter of all active contact pairs when + Contact override is enabled.""" + ) + + uniform token mjc:physics:integrator = "euler" ( + allowedTokens = ["euler", "rk4", "implicit", "implicitfast"] + customData = { + string apiName = "Integrator" + } + displayName = "Integrator" + doc = """Numerical integrator to be used.""" + ) + + uniform token mjc:physics:cone = "pyramidal" ( + allowedTokens = ["pyramidal", "elliptic"] + customData = { + string apiName = "Cone" + } + displayName = "Friction Cone Type" + doc = """The type of contact friction cone.""" + ) + + uniform token mjc:physics:jacobian = "auto" ( + allowedTokens = ["auto", "dense", "sparse"] + customData = { + string apiName = "Jacobian" + } + displayName = "Jacobian Type" + doc = """The type of constraint Jacobian and matrices computed from it.""" + ) + + uniform token mjc:physics:solver = "newton" ( + allowedTokens = ["pgs", "cg", "newton"] + customData = { + string apiName = "Solver" + } + displayName = "Solver" + doc = """Constraint solver algorithm to be used.""" + ) + + uniform int mjc:physics:iterations = 100 ( + customData = { + string apiName = "Iterations" + } + displayName = "Solver Iterations" + doc = """Maximum number of iterations of the constraint solver.""" + ) + + uniform double mjc:physics:tolerance = 1e-08 ( + customData = { + string apiName = "Tolerance" + } + displayName = "Solver Tolerance" + doc = """Tolerance threshold used for early termination of the iterative + solver.""" + ) + + uniform int mjc:physics:ls_iterations = 50 ( + customData = { + string apiName = "LSIterations" + } + displayName = "Linesearch Iterations" + doc = """Maximum number of linesearch iterations performed by CG/Newton + constraint solvers.""" + ) + + uniform double mjc:physics:ls_tolerance = 0.01 ( + customData = { + string apiName = "LSTolerance" + } + displayName = "Linesearch Tolerance" + doc = """Tolerance threshold used for early termination of the linesearch algorithm.""" + ) + + uniform int mjc:physics:noslip_iterations = 0 ( + customData = { + string apiName = "NoslipIterations" + } + displayName = "Noslip Iterations" + doc = """Maximum number of iterations of the Noslip solver.""" + ) + + uniform double mjc:physics:noslip_tolerance = 1e-06 ( + customData = { + string apiName = "NoslipTolerance" + } + displayName = "Noslip Tolerance" + doc = """Tolerance threshold used for early termination of the Noslip solver.""" + ) + + uniform int mjc:physics:ccd_iterations = 50 ( + customData = { + string apiName = "CCDIterations" + } + displayName = "CCD Iterations" + doc = """Maximum number of iterations of the algorithm used for convex collisions.""" + ) + + uniform double mjc:physics:ccd_tolerance = 1e-06 ( + customData = { + string apiName = "CCDTolerance" + } + displayName = "CCD Tolerance" + doc = """Tolerance threshold used for early termination of the convex + collision algorithm.""" + ) + + uniform int mjc:physics:sdf_iterations = 10 ( + customData = { + string apiName = "SDFIterations" + } + displayName = "SDF Iterations" + doc = """Number of iterations used for Signed Distance Field collisions + (per initial point).""" + ) + + uniform int mjc:physics:sdf_initpoints = 40 ( + customData = { + string apiName = "SDFInitPoints" + } + displayName = "SDF Initial Points" + doc = """Number of starting points used for finding contacts with Signed + Distance Field collisions.""" + ) + + uniform int[] mjc:physics:actuatorgroupdisable ( + customData = { + string apiName = "ActuatorGroupDisable" + } + displayName = "Actuator Group Disable" + doc = """List of actuator groups to disable.""" + ) + + uniform bool mjc:physics:flag:constraint = True ( + customData = { + string apiName = "ConstraintFlag" + } + displayName = "Constraint Solver Toggle" + doc = """Enables constraint solver.""" + ) + + uniform bool mjc:physics:flag:equality = True ( + customData = { + string apiName = "EqualityFlag" + } + displayName = "Equality Constraints Toggle" + doc = """Enables all standard computations related to equality constraints.""" + ) + + uniform bool mjc:physics:flag:frictionloss = True ( + customData = { + string apiName = "FrictionLossFlag" + } + displayName = "Friction Loss Constraints Toggle" + doc = """Enables all standard computations related to friction loss constraints.""" + ) + + uniform bool mjc:physics:flag:limit = True ( + customData = { + string apiName = "LimitFlag" + } + displayName = "Joint and Tendon Limit Constraints Toggle" + doc = """Enables all standard computations related to joint and tendon limit constraints.""" + ) + + uniform bool mjc:physics:flag:contact = True ( + customData = { + string apiName = "ContactFlag" + } + displayName = "Contact Constraints and Collision Detection Toggle" + doc = """Enables collision detection and all standard computations related to contact constraints.""" + ) + + uniform bool mjc:physics:flag:passive = True ( + customData = { + string apiName = "PassiveFlag" + } + displayName = "Passive Forces Toggle" + doc = """Enables the simulation of joint and tendon spring-dampers, fluid dynamics forces, and custom passive forces.""" + ) + + uniform bool mjc:physics:flag:gravity = True ( + customData = { + string apiName = "GravityFlag" + } + displayName = "Gravity Toggle" + doc = """Enables the application of gravitational acceleration as defined in mjOption.""" + ) + + uniform bool mjc:physics:flag:clampctrl = True ( + customData = { + string apiName = "ClampCtrlFlag" + } + displayName = "Control Input Clamping Toggle" + doc = """Enables the clamping of control inputs to all actuators, according to actuator-specific attributes.""" + ) + + uniform bool mjc:physics:flag:warmstart = True ( + customData = { + string apiName = "WarmStartFlag" + } + displayName = "Solver Warm-Starting Toggle" + doc = """Enables warm-starting of the constraint solver, using the solution from the previous time step to initialize the iterative optimization.""" + ) + + uniform bool mjc:physics:flag:filterparent = True ( + customData = { + string apiName = "FilterParentFlag" + } + displayName = "Parent-Child Contact Filtering Toggle" + doc = """Enables the filtering of contact pairs where the two geoms belong to a parent and child body.""" + ) + + uniform bool mjc:physics:flag:actuation = True ( + customData = { + string apiName = "ActuationFlag" + } + displayName = "Actuation Forces Toggle" + doc = """Enables all standard computations related to actuator forces, including actuator dynamics.""" + ) + + uniform bool mjc:physics:flag:refsafe = True ( + customData = { + string apiName = "RefSafeFlag" + } + displayName = "Solver Reference Safety Mechanism Toggle" + doc = """Enables a safety mechanism that prevents instabilities due to solref[0] being too small compared to the simulation timestep.""" + ) + + uniform bool mjc:physics:flag:sensor = True ( + customData = { + string apiName = "SensorFlag" + } + displayName = "Sensor Computations Toggle" + doc = """Enables all computations related to sensors.""" + ) + + uniform bool mjc:physics:flag:midphase = True ( + customData = { + string apiName = "MidPhaseFlag" + } + displayName = "Mid-Phase Collision Filtering Toggle" + doc = """Enables mid-phase collision filtering using a static AABB bounding volume hierarchy (BVH).""" + ) + + uniform bool mjc:physics:flag:nativeccd = True ( + customData = { + string apiName = "NativeCCDFlag" + } + displayName = "Native Convex Collision Detection Toggle" + doc = """Enables the native convex collision detection pipeline instead of using the libccd library.""" + ) + + uniform bool mjc:physics:flag:eulerdamp = True ( + customData = { + string apiName = "EulerDampFlag" + } + displayName = "Euler Integrator Damping Toggle" + doc = """Enables implicit integration with respect to joint damping in the Euler integrator.""" + ) + + uniform bool mjc:physics:flag:autoreset = True ( + customData = { + string apiName = "AutoResetFlag" + } + displayName = "Automatic Simulation Reset Toggle" + doc = """Enables the automatic resetting of the simulation state when numerical issues are detected.""" + ) + + uniform bool mjc:physics:flag:override = False ( + customData = { + string apiName = "OverrideFlag" + } + displayName = "Contact Override Mechanism Toggle" + doc = """Enables the contact override mechanism.""" + ) + + uniform bool mjc:physics:flag:energy = False ( + customData = { + string apiName = "EnergyFlag" + } + displayName = "Energy Computation Toggle" + doc = """Enables the computation of potential and kinetic energy (mjData.energy[0,1]).""" + ) + + uniform bool mjc:physics:flag:fwdinv = False ( + customData = { + string apiName = "FwdinvFlag" + } + displayName = "Forward/Inverse Dynamics Comparison Toggle" + doc = """Enables the automatic comparison of forward and inverse dynamics.""" + ) + + uniform bool mjc:physics:flag:invdiscrete = False ( + customData = { + string apiName = "InvDiscreteFlag" + } + displayName = "Discrete-Time Inverse Dynamics Toggle" + doc = """Enables discrete-time inverse dynamics with mj_inverse for integrators other than RK4.""" + ) + + uniform bool mjc:physics:flag:multiccd = False ( + customData = { + string apiName = "MultiCCDFlag" + } + displayName = "Multiple Contact Collision Detection (CCD) Toggle" + doc = """Enables multiple-contact collision detection for geom pairs using a general-purpose convex-convex collider.""" + ) + + uniform bool mjc:physics:flag:island = False ( + customData = { + string apiName = "IslandFlag" + } + displayName = "Constraint Island Discovery Toggle" + doc = """Enables the discovery of constraint islands.""" + ) +} + +class "MjcSiteAPI" +( + customData = { + string className = "SiteAPI" + } + doc = """API describing a Mujoco site.""" + + inherits = +) +{} + + From 9ac5fff41d25dd4e48d831388ed567b09dd06826 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 12 May 2025 05:35:49 -0700 Subject: [PATCH 108/191] Move local function to top of test file. PiperOrigin-RevId: 757722664 Change-Id: Ief7d8ca9fda990bf582ca0feaa4b645f0a4bb6e7 --- test/engine/engine_util_sparse_test.cc | 87 ++++++++++++-------------- 1 file changed, 40 insertions(+), 47 deletions(-) diff --git a/test/engine/engine_util_sparse_test.cc b/test/engine/engine_util_sparse_test.cc index a11d18e3..c62e3384 100644 --- a/test/engine/engine_util_sparse_test.cc +++ b/test/engine/engine_util_sparse_test.cc @@ -26,6 +26,29 @@ namespace mujoco { namespace { +// permute the rows and columns of a dense matrix +inline void PermuteMat(mjtNum* res, const mjtNum* mat, int nr, int nc, + const int* perm_r, const int* perm_c, + bool scatter_r, bool scatter_c) { + for (int r = 0; r < nr; r++) { + for (int c = 0; c < nc; c++) { + if (scatter_r && scatter_c) { + // scatter both + res[perm_r[r] * nc + perm_c[c]] = mat[r * nc + c]; + } else if (scatter_r && !scatter_c) { + // scatter rows, gather columns + res[perm_r[r] * nc + c] = mat[r * nc + perm_c[c]]; + } else if (!scatter_r && scatter_c) { + // gather rows, scatter columns + res[r * nc + perm_c[c]] = mat[perm_r[r] * nc + c]; + } else { + // gather both + res[r * nc + c] = mat[perm_r[r] * nc + perm_c[c]]; + } + } + } +} + using ::testing::ElementsAre; using EngineUtilSparseTest = MujocoTest; @@ -1128,12 +1151,10 @@ TEST_F(EngineUtilSparseTest, BlockDiag) { // 4x5 matrix with 3 blocks constexpr int nr = 4; constexpr int nc = 5; - const mjtNum mat[nr*nc] = { - 1, 2, 0, 0, 0, - 0, 0, 3, 4, 0, - 0, 0, 5, 6, 0, - 0, 0, 0, 0, 7 - }; + const mjtNum mat[nr * nc] = {1, 2, 0, 0, 0, + 0, 0, 3, 4, 0, + 0, 0, 5, 6, 0, + 0, 0, 0, 0, 7}; // block structure constexpr int nb = 3; @@ -1145,7 +1166,7 @@ TEST_F(EngineUtilSparseTest, BlockDiag) { // test with identity permutations const int perm_r[nr] = {0, 1, 2, 3}; const int perm_c[nc] = {0, 1, 2, 3, 4}; - mjtNum res[nr*nc] = {0}; + mjtNum res[nr * nc] = {0}; mju_blockDiag(res, mat, nc, nc, nb, perm_r, perm_c, block_nr, block_nc, @@ -1156,16 +1177,11 @@ TEST_F(EngineUtilSparseTest, BlockDiag) { 7, 0, 0, 0, 0)); } -void PermuteMat(mjtNum* res, const mjtNum* mat, int nr, int nc, - const int* perm_r, const int* perm_c, - bool scatter_r, bool scatter_c); - - TEST_F(EngineUtilSparseTest, BlockDiagPerm) { // 4x5 matrix with 3 blocks constexpr int nr = 4; constexpr int nc = 5; - const mjtNum mat[nr*nc] = { + const mjtNum mat[nr * nc] = { 1, 2, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 5, 6, 0, @@ -1182,11 +1198,11 @@ TEST_F(EngineUtilSparseTest, BlockDiagPerm) { // scatter mat into mat_p const int perm_r[nr] = {1, 3, 2, 0}; const int perm_c[nc] = {2, 0, 4, 3, 1}; - mjtNum mat_p[nr*nc]; + mjtNum mat_p[nr * nc]; PermuteMat(mat_p, mat, nr, nc, perm_r, perm_c, true, true); // test with permutation - mjtNum res[nr*nc] = {0}; + mjtNum res[nr * nc] = {0}; mju_blockDiag(res, mat_p, nc, nc, nb, perm_r, perm_c, block_nr, block_nc, @@ -1201,7 +1217,7 @@ TEST_F(EngineUtilSparseTest, BlockDiagLessCols) { // 4x5 matrix with 3 blocks constexpr int nr = 4; constexpr int nc = 5; - const mjtNum mat[nr*nc] = { + const mjtNum mat[nr * nc] = { 1, 2, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 5, 6, 0, @@ -1218,12 +1234,12 @@ TEST_F(EngineUtilSparseTest, BlockDiagLessCols) { // scatter mat into mat_p const int perm_r[nr] = {1, 3, 2, 0}; const int perm_c[nc] = {2, 0, 4, 3, 1}; - mjtNum mat_p[nr*nc]; + mjtNum mat_p[nr * nc]; PermuteMat(mat_p, mat, nr, nc, perm_r, perm_c, true, true); // test with permutation and less columns (ignore middle block) constexpr int nc_res = 3; - mjtNum res2[nr*nc_res] = {0}; + mjtNum res2[nr * nc_res] = {0}; mju_blockDiag(res2, mat_p, nc, nc_res, nb, perm_r, perm_c, block_nr, block_nc, @@ -1238,7 +1254,7 @@ TEST_F(EngineUtilSparseTest, BlockDiagSparse) { // 4x5 matrix with 3 blocks constexpr int nr = 4; constexpr int nc = 5; - const mjtNum mat[nr*nc] = { + const mjtNum mat[nr * nc] = { 1, 2, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 5, 6, 0, @@ -1269,7 +1285,7 @@ TEST_F(EngineUtilSparseTest, BlockDiagSparse) { mat_sparse, rownnz, rowadr, colind, nr, nb, perm_r, perm_c, block_r, block_c, nullptr, nullptr); - mjtNum dense_res[nr*nc]; + mjtNum dense_res[nr * nc]; mju_sparse2dense(dense_res, res, nr, nc, res_rownnz, res_rowadr, res_colind); EXPECT_THAT(dense_res, ElementsAre(1, 2, 0, 0, 0, 3, 4, 0, 0, 0, @@ -1301,50 +1317,27 @@ TEST_F(EngineUtilSparseTest, PermuteMat) { 0, 0, 5, 6}; const int perm_r[] = {2, 0, 1}; const int perm_c[] = {3, 2, 0, 1}; - mjtNum gather[3*4]; + mjtNum gather[3 * 4]; PermuteMat(gather, mat, 3, 4, perm_r, perm_c, false, false); EXPECT_THAT(gather, ElementsAre(6, 5, 0, 0, 0, 0, 1, 2, 4, 3, 0, 0)); - mjtNum scatter[3*4]; + mjtNum scatter[3 * 4]; PermuteMat(scatter, gather, 3, 4, perm_r, perm_c, true, true); EXPECT_THAT(scatter, ElementsAre(1, 2, 0, 0, 0, 0, 3, 4, 0, 0, 5, 6)); - mjtNum mixed[3*4]; + mjtNum mixed[3 * 4]; PermuteMat(mixed, mat, 3, 4, perm_r, perm_c, true, false); EXPECT_THAT(mixed, ElementsAre(4, 3, 0, 0, 6, 5, 0, 0, 0, 0, 1, 2)); - mjtNum mixed_back[3*4]; + mjtNum mixed_back[3 * 4]; PermuteMat(mixed_back, mixed, 3, 4, perm_r, perm_c, false, true); EXPECT_THAT(mixed_back, ElementsAre(1, 2, 0, 0, 0, 0, 3, 4, 0, 0, 5, 6)); } -// local function for permuting the rows and columns of a dense matrix -void PermuteMat(mjtNum* res, const mjtNum* mat, int nr, int nc, - const int* perm_r, const int* perm_c, - bool scatter_r, bool scatter_c) { - for (int r = 0; r < nr; r++) { - for (int c = 0; c < nc; c++) { - if (scatter_r && scatter_c) { - // scatter both - res[perm_r[r] * nc + perm_c[c]] = mat[r * nc + c]; - } else if (scatter_r && !scatter_c) { - // scatter rows, gather columns - res[perm_r[r] * nc + c] = mat[r * nc + perm_c[c]]; - } else if (!scatter_r && scatter_c) { - // gather rows, scatter columns - res[r * nc + perm_c[c]] = mat[perm_r[r] * nc + c]; - } else { - // gather both - res[r * nc + c] = mat[perm_r[r] * nc + perm_c[c]]; - } - } - } -} - } // namespace } // namespace mujoco From 6a977d073b47b85d7be8a7a07d97748759bf2148 Mon Sep 17 00:00:00 2001 From: Saran Tunyasuvunakool Date: Mon, 12 May 2025 07:59:23 -0700 Subject: [PATCH 109/191] Fix GitHub Actions builds. PiperOrigin-RevId: 757765160 Change-Id: Idaf86f80a24bc480ce35711cb706a4702a249d31 --- .github/workflows/build.yml | 104 ++++++++++++++++++++++-------------- simulate/CMakeLists.txt | 1 + 2 files changed, 65 insertions(+), 40 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4762eb73..b2daab71 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,8 +24,24 @@ jobs: fail-fast: false matrix: include: + - os: ubuntu-24.04 + label: "ubuntu-24.04-gcc-14" + cmake_args: >- + -G Ninja + -DCMAKE_C_COMPILER:STRING=gcc-14 + -DCMAKE_CXX_COMPILER:STRING=g++-14 + -DCMAKE_EXE_LINKER_FLAGS:STRING=-Wl,--no-as-needed + tmpdir: "/tmp" + - os: ubuntu-24.04 + label: "ubuntu-24.04-gcc-13" + cmake_args: >- + -G Ninja + -DCMAKE_C_COMPILER:STRING=gcc-13 + -DCMAKE_CXX_COMPILER:STRING=g++-13 + -DCMAKE_EXE_LINKER_FLAGS:STRING=-Wl,--no-as-needed + tmpdir: "/tmp" - os: ubuntu-22.04 - additional_label: "-gcc-12" + label: "ubuntu-22.04-gcc-12" cmake_args: >- -G Ninja -DCMAKE_C_COMPILER:STRING=gcc-12 @@ -33,7 +49,7 @@ jobs: -DCMAKE_EXE_LINKER_FLAGS:STRING=-Wl,--no-as-needed tmpdir: "/tmp" - os: ubuntu-22.04 - additional_label: "-gcc-11" + label: "ubuntu-22.04-gcc-11" cmake_args: >- -G Ninja -DCMAKE_C_COMPILER:STRING=gcc-11 @@ -41,23 +57,47 @@ jobs: -DCMAKE_EXE_LINKER_FLAGS:STRING=-Wl,--no-as-needed tmpdir: "/tmp" - os: ubuntu-22.04 - additional_label: "-gcc-10" + label: "ubuntu-22.04-gcc-10" cmake_args: >- -G Ninja -DCMAKE_C_COMPILER:STRING=gcc-10 -DCMAKE_CXX_COMPILER:STRING=g++-10 -DCMAKE_EXE_LINKER_FLAGS:STRING=-Wl,--no-as-needed tmpdir: "/tmp" - - os: ubuntu-22.04 - additional_label: "-gcc-9" + - os: ubuntu-24.04 + label: "ubuntu-24.04-clang-18" cmake_args: >- -G Ninja - -DCMAKE_C_COMPILER:STRING=gcc-9 - -DCMAKE_CXX_COMPILER:STRING=g++-9 - -DCMAKE_EXE_LINKER_FLAGS:STRING=-Wl,--no-as-needed + -DCMAKE_C_COMPILER:STRING=clang-18 + -DCMAKE_CXX_COMPILER:STRING=clang++-18 + -DMUJOCO_HARDEN:BOOL=ON + tmpdir: "/tmp" + - os: ubuntu-24.04 + label: "ubuntu-24.04-clang-17" + cmake_args: >- + -G Ninja + -DCMAKE_C_COMPILER:STRING=clang-17 + -DCMAKE_CXX_COMPILER:STRING=clang++-17 + -DMUJOCO_HARDEN:BOOL=ON + tmpdir: "/tmp" + - os: ubuntu-24.04 + label: "ubuntu-24.04-clang-16" + cmake_args: >- + -G Ninja + -DCMAKE_C_COMPILER:STRING=clang-16 + -DCMAKE_CXX_COMPILER:STRING=clang++-16 + -DMUJOCO_HARDEN:BOOL=ON tmpdir: "/tmp" - os: ubuntu-22.04 - additional_label: "-clang-14" + label: "ubuntu-22.04-clang-15" + cmake_args: >- + -G Ninja + -DCMAKE_C_COMPILER:STRING=clang-15 + -DCMAKE_CXX_COMPILER:STRING=clang++-15 + -DMUJOCO_HARDEN:BOOL=ON + tmpdir: "/tmp" + - os: ubuntu-22.04 + label: "ubuntu-22.04-clang-14" cmake_args: >- -G Ninja -DCMAKE_C_COMPILER:STRING=clang-14 @@ -65,49 +105,33 @@ jobs: -DMUJOCO_HARDEN:BOOL=ON tmpdir: "/tmp" - os: ubuntu-22.04 - additional_label: "-clang-13" + label: "ubuntu-22.04-clang-13" cmake_args: >- -G Ninja -DCMAKE_C_COMPILER:STRING=clang-13 -DCMAKE_CXX_COMPILER:STRING=clang++-13 -DMUJOCO_HARDEN:BOOL=ON tmpdir: "/tmp" - - os: ubuntu-20.04 - additional_label: "-clang-12" - cmake_args: >- - -G Ninja - -DCMAKE_C_COMPILER:STRING=clang-12 - -DCMAKE_CXX_COMPILER:STRING=clang++-12 - -DMUJOCO_HARDEN:BOOL=ON - tmpdir: "/tmp" - - os: ubuntu-20.04 - additional_label: "-clang-11" - cmake_args: >- - -G Ninja - -DCMAKE_C_COMPILER:STRING=clang-11 - -DCMAKE_CXX_COMPILER:STRING=clang++-11 - -DMUJOCO_HARDEN:BOOL=ON - tmpdir: "/tmp" - - os: ubuntu-20.04 - additional_label: "-clang-10" - cmake_args: >- - -G Ninja - -DCMAKE_C_COMPILER:STRING=clang-10 - -DCMAKE_CXX_COMPILER:STRING=clang++-10 - -DMUJOCO_HARDEN:BOOL=ON - tmpdir: "/tmp" - - os: macos-13 + - os: macos-15 + label: "macos-15-arm64" cmake_args: >- -G Ninja -DMUJOCO_HARDEN:BOOL=ON tmpdir: "/tmp" - - os: windows-2022 + - os: macos-15-large + label: "macos-15-x86_64" cmake_args: >- - -DCMAKE_SYSTEM_VERSION="10.0.22621.0" + -G Ninja + -DMUJOCO_HARDEN:BOOL=ON + tmpdir: "/tmp" + - os: windows-2025 + label: "windows-2025" + cmake_args: >- + -DCMAKE_SYSTEM_VERSION="10.0.26100.0" cmake_build_args: "-- -m" tmpdir: "C:/Temp" - name: "${{ matrix.os }}${{ matrix.additional_label }}" + name: "${{ matrix.label }}" runs-on: ${{ matrix.os }} steps: @@ -273,7 +297,7 @@ jobs: CHATMSG_AUTHOR_NAME: ${{ github.event.head_commit.author.name }} CHATMSG_AUTHOR_EMAIL: ${{ github.event.head_commit.author.email }} CHATMSG_COMMIT_MESSAGE: ${{ github.event.head_commit.message }} - CHATMSG_JOB_ID: ${{ matrix.os }}${{ matrix.additional_label }} + CHATMSG_JOB_ID: ${{ matrix.label }} if: ${{ failure() && github.event_name == 'push' && env.GCHAT_API_URL != '' }} run: | CHATMSG="$(cat <<-'EOF' | python3 @@ -288,7 +312,7 @@ jobs: email=env('CHATMSG_AUTHOR_EMAIL'), msg=env('CHATMSG_COMMIT_MESSAGE').replace('```', '') ) - text = '<{result}|*FAILURE*>: job `{job}` commit `{commit}`\n```Author: {name}<{email}>\n\n{msg}```'.format(**data) + text = '<{result}|*FAILURE*>: job `{job}` commit `{commit}`\n```Author: {name} <{email}>\n\n{msg}```'.format(**data) print(json.dumps({'text' : text})) EOF )" && diff --git a/simulate/CMakeLists.txt b/simulate/CMakeLists.txt index c22ea4a7..6489c562 100644 --- a/simulate/CMakeLists.txt +++ b/simulate/CMakeLists.txt @@ -110,6 +110,7 @@ target_sources( target_compile_options(platform_ui_adapter PRIVATE ${MUJOCO_SIMULATE_COMPILE_OPTIONS}) if(APPLE) target_sources(platform_ui_adapter PUBLIC glfw_corevideo.h PRIVATE glfw_corevideo.mm) + set_source_files_properties(glfw_corevideo.mm PROPERTIES COMPILE_FLAGS -Wno-deprecated-declarations) target_link_libraries(platform_ui_adapter PUBLIC "-framework CoreVideo") endif() target_include_directories( From dfe22f7ef708e0df679bdb06408bd13cf64b4cd3 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 12 May 2025 08:29:44 -0700 Subject: [PATCH 110/191] Remove stale reference to uncompressed sparse matrix. PiperOrigin-RevId: 757775265 Change-Id: Ic18c0c89ab84285a7d93e24c65e50832ebc53c6f --- doc/APIreference/functions.rst | 2 +- include/mujoco/mujoco.h | 2 +- python/mujoco/introspect/functions.py | 2 +- src/engine/engine_support.c | 2 +- src/engine/engine_support.h | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index e13ad19d..a86386d5 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -459,7 +459,7 @@ Multiply vector by (inertia matrix)^(1/2). .. mujoco-include:: mj_addM Add inertia matrix to destination matrix. -Destination can be sparse uncompressed, or dense when all int* are NULL +Destination can be sparse or dense when all int* are NULL. .. _mj_applyFT: diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index 5bca71ea..b955cb13 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -485,7 +485,7 @@ MJAPI void mj_mulM(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* MJAPI void mj_mulM2(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec); // Add inertia matrix to destination matrix. -// Destination can be sparse uncompressed, or dense when all int* are NULL +// Destination can be sparse or dense when all int* are NULL. MJAPI void mj_addM(const mjModel* m, mjData* d, mjtNum* dst, int* rownnz, int* rowadr, int* colind); // Apply Cartesian force and torque (outside xfrc_applied mechanism). diff --git a/python/mujoco/introspect/functions.py b/python/mujoco/introspect/functions.py index b74e222f..f62a0e7a 100644 --- a/python/mujoco/introspect/functions.py +++ b/python/mujoco/introspect/functions.py @@ -2900,7 +2900,7 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), ), ), - doc='Add inertia matrix to destination matrix. Destination can be sparse uncompressed, or dense when all int* are NULL', # pylint: disable=line-too-long + doc='Add inertia matrix to destination matrix. Destination can be sparse or dense when all int* are NULL.', # pylint: disable=line-too-long )), ('mj_applyFT', FunctionDecl( diff --git a/src/engine/engine_support.c b/src/engine/engine_support.c index 541cf31f..3d95da43 100644 --- a/src/engine/engine_support.c +++ b/src/engine/engine_support.c @@ -1062,7 +1062,7 @@ void mj_mulM2(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec) // add inertia matrix to destination matrix -// destination can be sparse uncompressed, or dense when all int* are NULL +// destination can be sparse or dense when all int* are NULL void mj_addM(const mjModel* m, mjData* d, mjtNum* dst, int* rownnz, int* rowadr, int* colind) { // sparse diff --git a/src/engine/engine_support.h b/src/engine/engine_support.h index 220979da..818e9622 100644 --- a/src/engine/engine_support.h +++ b/src/engine/engine_support.h @@ -131,7 +131,7 @@ MJAPI void mj_mulM(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* MJAPI void mj_mulM2(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec); // add inertia matrix to destination matrix -// destination can be sparse uncompressed, or dense when all int* are NULL +// destination can be sparse or dense when all int* are NULL MJAPI void mj_addM(const mjModel* m, mjData* d, mjtNum* dst, int* rownnz, int* rowadr, int* colind); From bd5cb88211bef3064d1fb51e363f08cbe0d88b8b Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Mon, 12 May 2025 08:35:57 -0700 Subject: [PATCH 111/191] Change mjPhysics schema library name from mujocoPhysics to mjcPhysics. PiperOrigin-RevId: 757777584 Change-Id: I6c8661aac3018892f113e65fada1b27903635200 --- src/experimental/usd/mjcPhysics/schema.usda | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/experimental/usd/mjcPhysics/schema.usda b/src/experimental/usd/mjcPhysics/schema.usda index 1a13a089..d6e2727d 100644 --- a/src/experimental/usd/mjcPhysics/schema.usda +++ b/src/experimental/usd/mjcPhysics/schema.usda @@ -7,7 +7,7 @@ over "GLOBAL" ( customData = { - string libraryName = "mujocoPhysics" + string libraryName = "mjcPhysics" string libraryPath = "." bool useLiteralIdentifier = 0 dictionary libraryTokens = { @@ -521,4 +521,3 @@ class "MjcSiteAPI" ) {} - From b53ed63b5fa0ac8bffffc0c033fca85011c26e73 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 12 May 2025 08:58:12 -0700 Subject: [PATCH 112/191] Use `2humanoid100.xml` model in a benchmark test. PiperOrigin-RevId: 757784271 Change-Id: I7da11ab85ba9f5e023cdbe3c77d31224f3444ca5 --- .../engine_util_sparse_benchmark_test.cc | 19 +- test/benchmark/testdata/2humanoid100.xml | 123 +++++++++ test/benchmark/testdata/humanoid.xml | 252 ++++++++++++++++++ 3 files changed, 388 insertions(+), 6 deletions(-) create mode 100644 test/benchmark/testdata/2humanoid100.xml create mode 100644 test/benchmark/testdata/humanoid.xml diff --git a/test/benchmark/engine_util_sparse_benchmark_test.cc b/test/benchmark/engine_util_sparse_benchmark_test.cc index 2c2ff6aa..6be10ded 100644 --- a/test/benchmark/engine_util_sparse_benchmark_test.cc +++ b/test/benchmark/engine_util_sparse_benchmark_test.cc @@ -544,11 +544,15 @@ BM_transposeSparse_old(benchmark::State& state) { BENCHMARK(BM_transposeSparse_old); static void BM_sqrMatTDSparse(benchmark::State& state, SqrMatTDFuncPtr func) { - static mjModel* m = LoadModelFromPath("humanoid/humanoid100.xml"); - mjData* d = mj_makeData(m); + static mjModel* m = + LoadModelFromPath("../test/benchmark/testdata/2humanoid100.xml"); - // force use of sparse matrices + // force use of sparse matrices, Newton solver, no islands m->opt.jacobian = mjJAC_SPARSE; + m->opt.solver = mjSOL_NEWTON; + m->opt.enableflags &= ~mjENBL_ISLAND; + + mjData* d = mj_makeData(m); // warm-up rollout to get a typical state while (d->time < 2) { @@ -575,10 +579,13 @@ static void BM_sqrMatTDSparse(benchmark::State& state, SqrMatTDFuncPtr func) { // time benchmark if (func) { - for (auto s : state) { - mju_sqrMatTDUncompressedInit(rowadr, m->nv); + mju_sqrMatTDSparseCount(rownnz, rowadr, m->nv, + d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind, + d->efc_JT_rownnz, d->efc_JT_rowadr, + d->efc_JT_colind, nullptr, d, 1); - // compute H = J'*D*J, uncompressed layout + for (auto s : state) { + // compute H = J'*D*J, compressed layout func(H, d->efc_J, d->efc_JT, D, d->nefc, m->nv, rownnz, rowadr, colind, d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind, NULL, d->efc_JT_rownnz, d->efc_JT_rowadr, d->efc_JT_colind, diff --git a/test/benchmark/testdata/2humanoid100.xml b/test/benchmark/testdata/2humanoid100.xml new file mode 100644 index 00000000..424ed2c4 --- /dev/null +++ b/test/benchmark/testdata/2humanoid100.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/benchmark/testdata/humanoid.xml b/test/benchmark/testdata/humanoid.xml new file mode 100644 index 00000000..7545e193 --- /dev/null +++ b/test/benchmark/testdata/humanoid.xml @@ -0,0 +1,252 @@ + + From 09bf5409ab07b425a914320132c8bf7ac3f0a3e5 Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Mon, 12 May 2025 08:59:34 -0700 Subject: [PATCH 113/191] Add code generated files for mjcPhysics schema. PiperOrigin-RevId: 757784741 Change-Id: Iacef6ef78c8e95ce51c5546562ad5b679f460deb --- src/experimental/usd/mjcPhysics/api.h | 42 + .../usd/mjcPhysics/generatedSchema.usda | 228 +++ src/experimental/usd/mjcPhysics/plugInfo.json | 38 + src/experimental/usd/mjcPhysics/sceneAPI.cpp | 701 +++++++++ src/experimental/usd/mjcPhysics/sceneAPI.h | 1379 +++++++++++++++++ src/experimental/usd/mjcPhysics/siteAPI.cpp | 99 ++ src/experimental/usd/mjcPhysics/siteAPI.h | 158 ++ src/experimental/usd/mjcPhysics/tokens.cpp | 156 ++ src/experimental/usd/mjcPhysics/tokens.h | 332 ++++ .../usd/mjcPhysics/wrapSceneAPI.cpp | 653 ++++++++ .../usd/mjcPhysics/wrapSiteAPI.cpp | 120 ++ .../usd/mjcPhysics/wrapTokens.cpp | 150 ++ 12 files changed, 4056 insertions(+) create mode 100644 src/experimental/usd/mjcPhysics/api.h create mode 100644 src/experimental/usd/mjcPhysics/generatedSchema.usda create mode 100644 src/experimental/usd/mjcPhysics/plugInfo.json create mode 100644 src/experimental/usd/mjcPhysics/sceneAPI.cpp create mode 100644 src/experimental/usd/mjcPhysics/sceneAPI.h create mode 100644 src/experimental/usd/mjcPhysics/siteAPI.cpp create mode 100644 src/experimental/usd/mjcPhysics/siteAPI.h create mode 100644 src/experimental/usd/mjcPhysics/tokens.cpp create mode 100644 src/experimental/usd/mjcPhysics/tokens.h create mode 100644 src/experimental/usd/mjcPhysics/wrapSceneAPI.cpp create mode 100644 src/experimental/usd/mjcPhysics/wrapSiteAPI.cpp create mode 100644 src/experimental/usd/mjcPhysics/wrapTokens.cpp diff --git a/src/experimental/usd/mjcPhysics/api.h b/src/experimental/usd/mjcPhysics/api.h new file mode 100644 index 00000000..dc5476c9 --- /dev/null +++ b/src/experimental/usd/mjcPhysics/api.h @@ -0,0 +1,42 @@ +// Copyright 2025 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. + +#ifndef MJCPHYSICS_API_H +#define MJCPHYSICS_API_H + +#include "pxr/base/arch/export.h" + +#if defined(PXR_STATIC) +#define MJCPHYSICS_API +#define MJCPHYSICS_API_TEMPLATE_CLASS(...) +#define MJCPHYSICS_API_TEMPLATE_STRUCT(...) +#define MJCPHYSICS_LOCAL +#else +#if defined(MJCPHYSICS_EXPORTS) +#define MJCPHYSICS_API ARCH_EXPORT +#define MJCPHYSICS_API_TEMPLATE_CLASS(...) \ + ARCH_EXPORT_TEMPLATE(class, __VA_ARGS__) +#define MJCPHYSICS_API_TEMPLATE_STRUCT(...) \ + ARCH_EXPORT_TEMPLATE(struct, __VA_ARGS__) +#else +#define MJCPHYSICS_API ARCH_IMPORT +#define MJCPHYSICS_API_TEMPLATE_CLASS(...) \ + ARCH_IMPORT_TEMPLATE(class, __VA_ARGS__) +#define MJCPHYSICS_API_TEMPLATE_STRUCT(...) \ + ARCH_IMPORT_TEMPLATE(struct, __VA_ARGS__) +#endif +#define MJCPHYSICS_LOCAL ARCH_HIDDEN +#endif + +#endif diff --git a/src/experimental/usd/mjcPhysics/generatedSchema.usda b/src/experimental/usd/mjcPhysics/generatedSchema.usda new file mode 100644 index 00000000..62050c6c --- /dev/null +++ b/src/experimental/usd/mjcPhysics/generatedSchema.usda @@ -0,0 +1,228 @@ +#usda 1.0 +( + "WARNING: THIS FILE IS GENERATED BY usdGenSchema. DO NOT EDIT." +) + +class "MjcPhysicsSceneAPI" ( + doc = "API providing global simulation options for Mujoco." +) +{ + uniform int[] mjc:physics:actuatorgroupdisable ( + displayName = "Actuator Group Disable" + doc = "List of actuator groups to disable." + ) + uniform double mjc:physics:apirate = 100 ( + displayName = "ApiRate" + doc = """Determines the rate (in Hz) at which an external API allows + the update function to be executed.""" + ) + uniform int mjc:physics:ccd_iterations = 50 ( + displayName = "CCD Iterations" + doc = "Maximum number of iterations of the algorithm used for convex collisions." + ) + uniform double mjc:physics:ccd_tolerance = 0.000001 ( + displayName = "CCD Tolerance" + doc = """Tolerance threshold used for early termination of the convex + collision algorithm.""" + ) + uniform token mjc:physics:cone = "pyramidal" ( + allowedTokens = ["pyramidal", "elliptic"] + displayName = "Friction Cone Type" + doc = "The type of contact friction cone." + ) + uniform double mjc:physics:density = 0 ( + displayName = "Density" + doc = "Density of medium." + ) + uniform bool mjc:physics:flag:actuation = 1 ( + displayName = "Actuation Forces Toggle" + doc = "Enables all standard computations related to actuator forces, including actuator dynamics." + ) + uniform bool mjc:physics:flag:autoreset = 1 ( + displayName = "Automatic Simulation Reset Toggle" + doc = "Enables the automatic resetting of the simulation state when numerical issues are detected." + ) + uniform bool mjc:physics:flag:clampctrl = 1 ( + displayName = "Control Input Clamping Toggle" + doc = "Enables the clamping of control inputs to all actuators, according to actuator-specific attributes." + ) + uniform bool mjc:physics:flag:constraint = 1 ( + displayName = "Constraint Solver Toggle" + doc = "Enables constraint solver." + ) + uniform bool mjc:physics:flag:contact = 1 ( + displayName = "Contact Constraints and Collision Detection Toggle" + doc = "Enables collision detection and all standard computations related to contact constraints." + ) + uniform bool mjc:physics:flag:energy = 0 ( + displayName = "Energy Computation Toggle" + doc = "Enables the computation of potential and kinetic energy (mjData.energy[0,1])." + ) + uniform bool mjc:physics:flag:equality = 1 ( + displayName = "Equality Constraints Toggle" + doc = "Enables all standard computations related to equality constraints." + ) + uniform bool mjc:physics:flag:eulerdamp = 1 ( + displayName = "Euler Integrator Damping Toggle" + doc = "Enables implicit integration with respect to joint damping in the Euler integrator." + ) + uniform bool mjc:physics:flag:filterparent = 1 ( + displayName = "Parent-Child Contact Filtering Toggle" + doc = "Enables the filtering of contact pairs where the two geoms belong to a parent and child body." + ) + uniform bool mjc:physics:flag:frictionloss = 1 ( + displayName = "Friction Loss Constraints Toggle" + doc = "Enables all standard computations related to friction loss constraints." + ) + uniform bool mjc:physics:flag:fwdinv = 0 ( + displayName = "Forward/Inverse Dynamics Comparison Toggle" + doc = "Enables the automatic comparison of forward and inverse dynamics." + ) + uniform bool mjc:physics:flag:gravity = 1 ( + displayName = "Gravity Toggle" + doc = "Enables the application of gravitational acceleration as defined in mjOption." + ) + uniform bool mjc:physics:flag:invdiscrete = 0 ( + displayName = "Discrete-Time Inverse Dynamics Toggle" + doc = "Enables discrete-time inverse dynamics with mj_inverse for integrators other than RK4." + ) + uniform bool mjc:physics:flag:island = 0 ( + displayName = "Constraint Island Discovery Toggle" + doc = "Enables the discovery of constraint islands." + ) + uniform bool mjc:physics:flag:limit = 1 ( + displayName = "Joint and Tendon Limit Constraints Toggle" + doc = "Enables all standard computations related to joint and tendon limit constraints." + ) + uniform bool mjc:physics:flag:midphase = 1 ( + displayName = "Mid-Phase Collision Filtering Toggle" + doc = "Enables mid-phase collision filtering using a static AABB bounding volume hierarchy (BVH)." + ) + uniform bool mjc:physics:flag:multiccd = 0 ( + displayName = "Multiple Contact Collision Detection (CCD) Toggle" + doc = "Enables multiple-contact collision detection for geom pairs using a general-purpose convex-convex collider." + ) + uniform bool mjc:physics:flag:nativeccd = 1 ( + displayName = "Native Convex Collision Detection Toggle" + doc = "Enables the native convex collision detection pipeline instead of using the libccd library." + ) + uniform bool mjc:physics:flag:override = 0 ( + displayName = "Contact Override Mechanism Toggle" + doc = "Enables the contact override mechanism." + ) + uniform bool mjc:physics:flag:passive = 1 ( + displayName = "Passive Forces Toggle" + doc = "Enables the simulation of joint and tendon spring-dampers, fluid dynamics forces, and custom passive forces." + ) + uniform bool mjc:physics:flag:refsafe = 1 ( + displayName = "Solver Reference Safety Mechanism Toggle" + doc = "Enables a safety mechanism that prevents instabilities due to solref[0] being too small compared to the simulation timestep." + ) + uniform bool mjc:physics:flag:sensor = 1 ( + displayName = "Sensor Computations Toggle" + doc = "Enables all computations related to sensors." + ) + uniform bool mjc:physics:flag:warmstart = 1 ( + displayName = "Solver Warm-Starting Toggle" + doc = "Enables warm-starting of the constraint solver, using the solution from the previous time step to initialize the iterative optimization." + ) + uniform double mjc:physics:impratio = 1 ( + displayName = "Impedance Ratio" + doc = """Ratio of frictional-to-normal constraint impedance for elliptic + friction cones.""" + ) + uniform token mjc:physics:integrator = "euler" ( + allowedTokens = ["euler", "rk4", "implicit", "implicitfast"] + displayName = "Integrator" + doc = "Numerical integrator to be used." + ) + uniform int mjc:physics:iterations = 100 ( + displayName = "Solver Iterations" + doc = "Maximum number of iterations of the constraint solver." + ) + uniform token mjc:physics:jacobian = "auto" ( + allowedTokens = ["auto", "dense", "sparse"] + displayName = "Jacobian Type" + doc = "The type of constraint Jacobian and matrices computed from it." + ) + uniform int mjc:physics:ls_iterations = 50 ( + displayName = "Linesearch Iterations" + doc = """Maximum number of linesearch iterations performed by CG/Newton + constraint solvers.""" + ) + uniform double mjc:physics:ls_tolerance = 0.01 ( + displayName = "Linesearch Tolerance" + doc = "Tolerance threshold used for early termination of the linesearch algorithm." + ) + uniform double3 mjc:physics:magnetic = (0, -0.5, 0) ( + displayName = "Magnetic Flux" + doc = "Global magnetic flux." + ) + uniform int mjc:physics:noslip_iterations = 0 ( + displayName = "Noslip Iterations" + doc = "Maximum number of iterations of the Noslip solver." + ) + uniform double mjc:physics:noslip_tolerance = 0.000001 ( + displayName = "Noslip Tolerance" + doc = "Tolerance threshold used for early termination of the Noslip solver." + ) + uniform double[] mjc:physics:o_friction = [1, 1, 0.005, 0.0001, 0.0001] ( + displayName = "Contact Override Friction" + doc = """Replaces the friction parameter of all active contact pairs when + Contact override is enabled.""" + ) + uniform double mjc:physics:o_margin = 0 ( + displayName = "Contact Override Margin" + doc = """Replaces the margin parameter of all active contact pairs when + Contact override is enabled.""" + ) + uniform double[] mjc:physics:o_solimp = [0.9, 0.95, 0.001, 0.5, 2] ( + displayName = "Contact Override SolImp" + doc = """Replaces the solimp parameter of all active contact pairs when + Contact override is enabled.""" + ) + uniform double[] mjc:physics:o_solref = [0.02, 1] ( + displayName = "Contact Override SolRef" + doc = """Replaces the solref parameter of all active contact pairs when + Contact override is enabled.""" + ) + uniform int mjc:physics:sdf_initpoints = 40 ( + displayName = "SDF Initial Points" + doc = """Number of starting points used for finding contacts with Signed + Distance Field collisions.""" + ) + uniform int mjc:physics:sdf_iterations = 10 ( + displayName = "SDF Iterations" + doc = """Number of iterations used for Signed Distance Field collisions + (per initial point).""" + ) + uniform token mjc:physics:solver = "newton" ( + allowedTokens = ["pgs", "cg", "newton"] + displayName = "Solver" + doc = "Constraint solver algorithm to be used." + ) + uniform double mjc:physics:timestep = 0.002 ( + displayName = "Timestep" + doc = "Controls the timestep in seconds used by MuJoCo." + ) + uniform double mjc:physics:tolerance = 1e-8 ( + displayName = "Solver Tolerance" + doc = """Tolerance threshold used for early termination of the iterative + solver.""" + ) + uniform double mjc:physics:viscosity = 0 ( + displayName = "Viscosity" + doc = "Viscosity of medium." + ) + uniform double3 mjc:physics:wind = (0, 0, 0) ( + displayName = "Wind Velocity" + doc = "Velocity vector of medium (i.e. wind)." + ) +} + +class "MjcSiteAPI" ( + doc = "API describing a Mujoco site." +) +{ +} + diff --git a/src/experimental/usd/mjcPhysics/plugInfo.json b/src/experimental/usd/mjcPhysics/plugInfo.json new file mode 100644 index 00000000..fcf19910 --- /dev/null +++ b/src/experimental/usd/mjcPhysics/plugInfo.json @@ -0,0 +1,38 @@ +#Portions of this file auto - generated by usdGenSchema. +#Edits will survive regeneration except for comments and +#changes to types with autoGenerated = true. +{ + "Plugins": [ + { + "Info": { + "Types": { + "MjcPhysicsSceneAPI": { + "alias": { + "UsdSchemaBase": "MjcPhysicsSceneAPI" + }, + "autoGenerated": true, + "bases": [ + "UsdAPISchemaBase" + ], + "schemaKind": "singleApplyAPI" + }, + "MjcPhysicsSiteAPI": { + "alias": { + "UsdSchemaBase": "MjcSiteAPI" + }, + "autoGenerated": true, + "bases": [ + "UsdAPISchemaBase" + ], + "schemaKind": "singleApplyAPI" + } + } + }, + "LibraryPath": "", + "Name": "mjcPhysics", + "ResourcePath": "", + "Root": ".", + "Type": "library" + } + ] +} diff --git a/src/experimental/usd/mjcPhysics/sceneAPI.cpp b/src/experimental/usd/mjcPhysics/sceneAPI.cpp new file mode 100644 index 00000000..0c01a533 --- /dev/null +++ b/src/experimental/usd/mjcPhysics/sceneAPI.cpp @@ -0,0 +1,701 @@ +// Copyright 2025 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. + +#include "./sceneAPI.h" + +#include "pxr/usd/sdf/assetPath.h" +#include "pxr/usd/sdf/types.h" +#include "pxr/usd/usd/schemaRegistry.h" +#include "pxr/usd/usd/typed.h" + +PXR_NAMESPACE_OPEN_SCOPE + +// Register the schema with the TfType system. +TF_REGISTRY_FUNCTION(TfType) { + TfType::Define >(); +} + +/* virtual */ +MjcPhysicsSceneAPI::~MjcPhysicsSceneAPI() {} + +/* static */ +MjcPhysicsSceneAPI MjcPhysicsSceneAPI::Get(const UsdStagePtr &stage, + const SdfPath &path) { + if (!stage) { + TF_CODING_ERROR("Invalid stage"); + return MjcPhysicsSceneAPI(); + } + return MjcPhysicsSceneAPI(stage->GetPrimAtPath(path)); +} + +/* virtual */ +UsdSchemaKind MjcPhysicsSceneAPI::_GetSchemaKind() const { + return MjcPhysicsSceneAPI::schemaKind; +} + +/* static */ +bool MjcPhysicsSceneAPI::CanApply(const UsdPrim &prim, std::string *whyNot) { + return prim.CanApplyAPI(whyNot); +} + +/* static */ +MjcPhysicsSceneAPI MjcPhysicsSceneAPI::Apply(const UsdPrim &prim) { + if (prim.ApplyAPI()) { + return MjcPhysicsSceneAPI(prim); + } + return MjcPhysicsSceneAPI(); +} + +/* static */ +const TfType &MjcPhysicsSceneAPI::_GetStaticTfType() { + static TfType tfType = TfType::Find(); + return tfType; +} + +/* static */ +bool MjcPhysicsSceneAPI::_IsTypedSchema() { + static bool isTyped = _GetStaticTfType().IsA(); + return isTyped; +} + +/* virtual */ +const TfType &MjcPhysicsSceneAPI::_GetTfType() const { + return _GetStaticTfType(); +} + +UsdAttribute MjcPhysicsSceneAPI::GetTimestepAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsTimestep); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateTimestepAttr(VtValue const &defaultValue, + bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsTimestep, SdfValueTypeNames->Double, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetApiRateAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsApirate); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateApiRateAttr(VtValue const &defaultValue, + bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsApirate, SdfValueTypeNames->Double, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetImpRatioAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsImpratio); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateImpRatioAttr(VtValue const &defaultValue, + bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsImpratio, SdfValueTypeNames->Double, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetWindAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsWind); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateWindAttr(VtValue const &defaultValue, + bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsWind, SdfValueTypeNames->Double3, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetMagneticAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsMagnetic); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateMagneticAttr(VtValue const &defaultValue, + bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsMagnetic, SdfValueTypeNames->Double3, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetDensityAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsDensity); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateDensityAttr(VtValue const &defaultValue, + bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsDensity, SdfValueTypeNames->Double, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetViscosityAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsViscosity); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateViscosityAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsViscosity, SdfValueTypeNames->Double, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetOMarginAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsO_margin); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateOMarginAttr(VtValue const &defaultValue, + bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsO_margin, SdfValueTypeNames->Double, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetOSolRefAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsO_solref); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateOSolRefAttr(VtValue const &defaultValue, + bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsO_solref, SdfValueTypeNames->DoubleArray, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetOSolImpAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsO_solimp); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateOSolImpAttr(VtValue const &defaultValue, + bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsO_solimp, SdfValueTypeNames->DoubleArray, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetOFrictionAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsO_friction); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateOFrictionAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsO_friction, SdfValueTypeNames->DoubleArray, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetIntegratorAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsIntegrator); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateIntegratorAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsIntegrator, SdfValueTypeNames->Token, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetConeAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsCone); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateConeAttr(VtValue const &defaultValue, + bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsCone, SdfValueTypeNames->Token, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetJacobianAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsJacobian); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateJacobianAttr(VtValue const &defaultValue, + bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsJacobian, SdfValueTypeNames->Token, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetSolverAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsSolver); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateSolverAttr(VtValue const &defaultValue, + bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsSolver, SdfValueTypeNames->Token, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetIterationsAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsIterations); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateIterationsAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsIterations, SdfValueTypeNames->Int, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetToleranceAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsTolerance); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateToleranceAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsTolerance, SdfValueTypeNames->Double, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetLSIterationsAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsLs_iterations); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateLSIterationsAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsLs_iterations, SdfValueTypeNames->Int, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetLSToleranceAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsLs_tolerance); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateLSToleranceAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsLs_tolerance, SdfValueTypeNames->Double, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetNoslipIterationsAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsNoslip_iterations); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateNoslipIterationsAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsNoslip_iterations, SdfValueTypeNames->Int, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetNoslipToleranceAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsNoslip_tolerance); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateNoslipToleranceAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsNoslip_tolerance, SdfValueTypeNames->Double, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetCCDIterationsAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsCcd_iterations); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateCCDIterationsAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsCcd_iterations, SdfValueTypeNames->Int, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetCCDToleranceAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsCcd_tolerance); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateCCDToleranceAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsCcd_tolerance, SdfValueTypeNames->Double, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetSDFIterationsAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsSdf_iterations); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateSDFIterationsAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsSdf_iterations, SdfValueTypeNames->Int, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetSDFInitPointsAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsSdf_initpoints); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateSDFInitPointsAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsSdf_initpoints, SdfValueTypeNames->Int, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetActuatorGroupDisableAttr() const { + return GetPrim().GetAttribute( + MjcPhysicsTokens->mjcPhysicsActuatorgroupdisable); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateActuatorGroupDisableAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsActuatorgroupdisable, + SdfValueTypeNames->IntArray, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetConstraintFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagConstraint); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateConstraintFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagConstraint, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetEqualityFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagEquality); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateEqualityFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagEquality, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetFrictionLossFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagFrictionloss); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateFrictionLossFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagFrictionloss, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetLimitFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagLimit); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateLimitFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagLimit, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetContactFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagContact); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateContactFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagContact, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetPassiveFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagPassive); +} + +UsdAttribute MjcPhysicsSceneAPI::CreatePassiveFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagPassive, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetGravityFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagGravity); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateGravityFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagGravity, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetClampCtrlFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagClampctrl); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateClampCtrlFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagClampctrl, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetWarmStartFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagWarmstart); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateWarmStartFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagWarmstart, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetFilterParentFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagFilterparent); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateFilterParentFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagFilterparent, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetActuationFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagActuation); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateActuationFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagActuation, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetRefSafeFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagRefsafe); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateRefSafeFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagRefsafe, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetSensorFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagSensor); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateSensorFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagSensor, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetMidPhaseFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagMidphase); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateMidPhaseFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagMidphase, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetNativeCCDFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagNativeccd); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateNativeCCDFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagNativeccd, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetEulerDampFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagEulerdamp); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateEulerDampFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagEulerdamp, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetAutoResetFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagAutoreset); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateAutoResetFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagAutoreset, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetOverrideFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagOverride); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateOverrideFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagOverride, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetEnergyFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagEnergy); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateEnergyFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagEnergy, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetFwdinvFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagFwdinv); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateFwdinvFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagFwdinv, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetInvDiscreteFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagInvdiscrete); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateInvDiscreteFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagInvdiscrete, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetMultiCCDFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagMulticcd); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateMultiCCDFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagMulticcd, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsSceneAPI::GetIslandFlagAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagIsland); +} + +UsdAttribute MjcPhysicsSceneAPI::CreateIslandFlagAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPhysicsFlagIsland, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +namespace { +static inline TfTokenVector _ConcatenateAttributeNames( + const TfTokenVector &left, const TfTokenVector &right) { + TfTokenVector result; + result.reserve(left.size() + right.size()); + result.insert(result.end(), left.begin(), left.end()); + result.insert(result.end(), right.begin(), right.end()); + return result; +} +} // namespace + +/*static*/ +const TfTokenVector &MjcPhysicsSceneAPI::GetSchemaAttributeNames( + bool includeInherited) { + static TfTokenVector localNames = { + MjcPhysicsTokens->mjcPhysicsTimestep, + MjcPhysicsTokens->mjcPhysicsApirate, + MjcPhysicsTokens->mjcPhysicsImpratio, + MjcPhysicsTokens->mjcPhysicsWind, + MjcPhysicsTokens->mjcPhysicsMagnetic, + MjcPhysicsTokens->mjcPhysicsDensity, + MjcPhysicsTokens->mjcPhysicsViscosity, + MjcPhysicsTokens->mjcPhysicsO_margin, + MjcPhysicsTokens->mjcPhysicsO_solref, + MjcPhysicsTokens->mjcPhysicsO_solimp, + MjcPhysicsTokens->mjcPhysicsO_friction, + MjcPhysicsTokens->mjcPhysicsIntegrator, + MjcPhysicsTokens->mjcPhysicsCone, + MjcPhysicsTokens->mjcPhysicsJacobian, + MjcPhysicsTokens->mjcPhysicsSolver, + MjcPhysicsTokens->mjcPhysicsIterations, + MjcPhysicsTokens->mjcPhysicsTolerance, + MjcPhysicsTokens->mjcPhysicsLs_iterations, + MjcPhysicsTokens->mjcPhysicsLs_tolerance, + MjcPhysicsTokens->mjcPhysicsNoslip_iterations, + MjcPhysicsTokens->mjcPhysicsNoslip_tolerance, + MjcPhysicsTokens->mjcPhysicsCcd_iterations, + MjcPhysicsTokens->mjcPhysicsCcd_tolerance, + MjcPhysicsTokens->mjcPhysicsSdf_iterations, + MjcPhysicsTokens->mjcPhysicsSdf_initpoints, + MjcPhysicsTokens->mjcPhysicsActuatorgroupdisable, + MjcPhysicsTokens->mjcPhysicsFlagConstraint, + MjcPhysicsTokens->mjcPhysicsFlagEquality, + MjcPhysicsTokens->mjcPhysicsFlagFrictionloss, + MjcPhysicsTokens->mjcPhysicsFlagLimit, + MjcPhysicsTokens->mjcPhysicsFlagContact, + MjcPhysicsTokens->mjcPhysicsFlagPassive, + MjcPhysicsTokens->mjcPhysicsFlagGravity, + MjcPhysicsTokens->mjcPhysicsFlagClampctrl, + MjcPhysicsTokens->mjcPhysicsFlagWarmstart, + MjcPhysicsTokens->mjcPhysicsFlagFilterparent, + MjcPhysicsTokens->mjcPhysicsFlagActuation, + MjcPhysicsTokens->mjcPhysicsFlagRefsafe, + MjcPhysicsTokens->mjcPhysicsFlagSensor, + MjcPhysicsTokens->mjcPhysicsFlagMidphase, + MjcPhysicsTokens->mjcPhysicsFlagNativeccd, + MjcPhysicsTokens->mjcPhysicsFlagEulerdamp, + MjcPhysicsTokens->mjcPhysicsFlagAutoreset, + MjcPhysicsTokens->mjcPhysicsFlagOverride, + MjcPhysicsTokens->mjcPhysicsFlagEnergy, + MjcPhysicsTokens->mjcPhysicsFlagFwdinv, + MjcPhysicsTokens->mjcPhysicsFlagInvdiscrete, + MjcPhysicsTokens->mjcPhysicsFlagMulticcd, + MjcPhysicsTokens->mjcPhysicsFlagIsland, + }; + static TfTokenVector allNames = _ConcatenateAttributeNames( + UsdAPISchemaBase::GetSchemaAttributeNames(true), localNames); + + if (includeInherited) + return allNames; + else + return localNames; +} + +PXR_NAMESPACE_CLOSE_SCOPE + +// ===================================================================== // +// Feel free to add custom code below this line. It will be preserved by +// the code generator. +// +// Just remember to wrap code in the appropriate delimiters: +// 'PXR_NAMESPACE_OPEN_SCOPE', 'PXR_NAMESPACE_CLOSE_SCOPE'. +// ===================================================================== // +// --(BEGIN CUSTOM CODE)-- diff --git a/src/experimental/usd/mjcPhysics/sceneAPI.h b/src/experimental/usd/mjcPhysics/sceneAPI.h new file mode 100644 index 00000000..0fd0d56e --- /dev/null +++ b/src/experimental/usd/mjcPhysics/sceneAPI.h @@ -0,0 +1,1379 @@ +// Copyright 2025 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. + +#ifndef MJCPHYSICS_GENERATED_SCENEAPI_H +#define MJCPHYSICS_GENERATED_SCENEAPI_H + +/// \file mjcPhysics/sceneAPI.h + +#include "./api.h" +#include "./tokens.h" +#include "pxr/base/gf/matrix4d.h" +#include "pxr/base/gf/vec3d.h" +#include "pxr/base/gf/vec3f.h" +#include "pxr/base/tf/token.h" +#include "pxr/base/tf/type.h" +#include "pxr/base/vt/value.h" +#include "pxr/pxr.h" +#include "pxr/usd/usd/apiSchemaBase.h" +#include "pxr/usd/usd/prim.h" +#include "pxr/usd/usd/stage.h" + +PXR_NAMESPACE_OPEN_SCOPE + +class SdfAssetPath; + +// -------------------------------------------------------------------------- // +// MJCPHYSICSSCENEAPI // +// -------------------------------------------------------------------------- // + +/// \class MjcPhysicsSceneAPI +/// +/// API providing global simulation options for Mujoco. +/// +/// For any described attribute \em Fallback \em Value or \em Allowed \em Values +/// below that are text/tokens, the actual token is published and defined in +/// \ref MjcPhysicsTokens. So to set an attribute to the value "rightHanded", +/// use MjcPhysicsTokens->rightHanded as the value. +/// +class MjcPhysicsSceneAPI : public UsdAPISchemaBase { + public: + /// Compile time constant representing what kind of schema this class is. + /// + /// \sa UsdSchemaKind + static const UsdSchemaKind schemaKind = UsdSchemaKind::SingleApplyAPI; + + /// Construct a MjcPhysicsSceneAPI on UsdPrim \p prim . + /// Equivalent to MjcPhysicsSceneAPI::Get(prim.GetStage(), prim.GetPath()) + /// for a \em valid \p prim, but will not immediately throw an error for + /// an invalid \p prim + explicit MjcPhysicsSceneAPI(const UsdPrim &prim = UsdPrim()) + : UsdAPISchemaBase(prim) {} + + /// Construct a MjcPhysicsSceneAPI on the prim held by \p schemaObj . + /// Should be preferred over MjcPhysicsSceneAPI(schemaObj.GetPrim()), + /// as it preserves SchemaBase state. + explicit MjcPhysicsSceneAPI(const UsdSchemaBase &schemaObj) + : UsdAPISchemaBase(schemaObj) {} + + /// Destructor. + MJCPHYSICS_API + virtual ~MjcPhysicsSceneAPI(); + + /// Return a vector of names of all pre-declared attributes for this schema + /// class and all its ancestor classes. Does not include attributes that + /// may be authored by custom/extended methods of the schemas involved. + MJCPHYSICS_API + static const TfTokenVector &GetSchemaAttributeNames( + bool includeInherited = true); + + /// Return a MjcPhysicsSceneAPI holding the prim adhering to this + /// schema at \p path on \p stage. If no prim exists at \p path on + /// \p stage, or if the prim at that path does not adhere to this schema, + /// return an invalid schema object. This is shorthand for the following: + /// + /// \code + /// MjcPhysicsSceneAPI(stage->GetPrimAtPath(path)); + /// \endcode + /// + MJCPHYSICS_API + static MjcPhysicsSceneAPI Get(const UsdStagePtr &stage, const SdfPath &path); + + /// Returns true if this single-apply API schema can be applied to + /// the given \p prim. If this schema can not be a applied to the prim, + /// this returns false and, if provided, populates \p whyNot with the + /// reason it can not be applied. + /// + /// Note that if CanApply returns false, that does not necessarily imply + /// that calling Apply will fail. Callers are expected to call CanApply + /// before calling Apply if they want to ensure that it is valid to + /// apply a schema. + /// + /// \sa UsdPrim::GetAppliedSchemas() + /// \sa UsdPrim::HasAPI() + /// \sa UsdPrim::CanApplyAPI() + /// \sa UsdPrim::ApplyAPI() + /// \sa UsdPrim::RemoveAPI() + /// + MJCPHYSICS_API + static bool CanApply(const UsdPrim &prim, std::string *whyNot = nullptr); + + /// Applies this single-apply API schema to the given \p prim. + /// This information is stored by adding "MjcPhysicsSceneAPI" to the + /// token-valued, listOp metadata \em apiSchemas on the prim. + /// + /// \return A valid MjcPhysicsSceneAPI object is returned upon success. + /// An invalid (or empty) MjcPhysicsSceneAPI object is returned upon + /// failure. See \ref UsdPrim::ApplyAPI() for conditions + /// resulting in failure. + /// + /// \sa UsdPrim::GetAppliedSchemas() + /// \sa UsdPrim::HasAPI() + /// \sa UsdPrim::CanApplyAPI() + /// \sa UsdPrim::ApplyAPI() + /// \sa UsdPrim::RemoveAPI() + /// + MJCPHYSICS_API + static MjcPhysicsSceneAPI Apply(const UsdPrim &prim); + + protected: + /// Returns the kind of schema this class belongs to. + /// + /// \sa UsdSchemaKind + MJCPHYSICS_API + UsdSchemaKind _GetSchemaKind() const override; + + private: + // needs to invoke _GetStaticTfType. + friend class UsdSchemaRegistry; + MJCPHYSICS_API + static const TfType &_GetStaticTfType(); + + static bool _IsTypedSchema(); + + // override SchemaBase virtuals. + MJCPHYSICS_API + const TfType &_GetTfType() const override; + + public: + // --------------------------------------------------------------------- // + // TIMESTEP + // --------------------------------------------------------------------- // + /// Controls the timestep in seconds used by MuJoCo. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double mjc:physics:timestep = 0.002` | + /// | C++ Type | double | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetTimestepAttr() const; + + /// See GetTimestepAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateTimestepAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // APIRATE + // --------------------------------------------------------------------- // + /// Determines the rate (in Hz) at which an external API allows + /// the update function to be executed. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double mjc:physics:apirate = 100` | + /// | C++ Type | double | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetApiRateAttr() const; + + /// See GetApiRateAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateApiRateAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // IMPRATIO + // --------------------------------------------------------------------- // + /// Ratio of frictional-to-normal constraint impedance for elliptic + /// friction cones. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double mjc:physics:impratio = 1` | + /// | C++ Type | double | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetImpRatioAttr() const; + + /// See GetImpRatioAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateImpRatioAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // WIND + // --------------------------------------------------------------------- // + /// Velocity vector of medium (i.e. wind). + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double3 mjc:physics:wind = (0, 0, 0)` | + /// | C++ Type | GfVec3d | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double3 | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetWindAttr() const; + + /// See GetWindAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateWindAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MAGNETIC + // --------------------------------------------------------------------- // + /// Global magnetic flux. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double3 mjc:physics:magnetic = (0, -0.5, 0)` | + /// | C++ Type | GfVec3d | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double3 | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetMagneticAttr() const; + + /// See GetMagneticAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMagneticAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // DENSITY + // --------------------------------------------------------------------- // + /// Density of medium. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double mjc:physics:density = 0` | + /// | C++ Type | double | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetDensityAttr() const; + + /// See GetDensityAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateDensityAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // VISCOSITY + // --------------------------------------------------------------------- // + /// Viscosity of medium. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double mjc:physics:viscosity = 0` | + /// | C++ Type | double | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetViscosityAttr() const; + + /// See GetViscosityAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateViscosityAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // OMARGIN + // --------------------------------------------------------------------- // + /// Replaces the margin parameter of all active contact pairs when + /// Contact override is enabled. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double mjc:physics:o_margin = 0` | + /// | C++ Type | double | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetOMarginAttr() const; + + /// See GetOMarginAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateOMarginAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // OSOLREF + // --------------------------------------------------------------------- // + /// Replaces the solref parameter of all active contact pairs when + /// Contact override is enabled. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double[] mjc:physics:o_solref = [0.02, 1]` | + /// | C++ Type | VtArray | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->DoubleArray | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetOSolRefAttr() const; + + /// See GetOSolRefAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateOSolRefAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // OSOLIMP + // --------------------------------------------------------------------- // + /// Replaces the solimp parameter of all active contact pairs when + /// Contact override is enabled. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double[] mjc:physics:o_solimp = [0.9, 0.95, + /// 0.001, 0.5, 2]` | | C++ Type | VtArray | | \ref Usd_Datatypes "Usd + /// Type" | SdfValueTypeNames->DoubleArray | | \ref SdfVariability + /// "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetOSolImpAttr() const; + + /// See GetOSolImpAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateOSolImpAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // OFRICTION + // --------------------------------------------------------------------- // + /// Replaces the friction parameter of all active contact pairs when + /// Contact override is enabled. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double[] mjc:physics:o_friction = [1, 1, 0.005, + /// 0.0001, 0.0001]` | | C++ Type | VtArray | | \ref Usd_Datatypes + /// "Usd Type" | SdfValueTypeNames->DoubleArray | | \ref SdfVariability + /// "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetOFrictionAttr() const; + + /// See GetOFrictionAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateOFrictionAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // INTEGRATOR + // --------------------------------------------------------------------- // + /// Numerical integrator to be used. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform token mjc:physics:integrator = "euler"` | + /// | C++ Type | TfToken | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Token | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + /// | \ref MjcPhysicsTokens "Allowed Values" | euler, rk4, implicit, + /// implicitfast | + MJCPHYSICS_API + UsdAttribute GetIntegratorAttr() const; + + /// See GetIntegratorAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateIntegratorAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // CONE + // --------------------------------------------------------------------- // + /// The type of contact friction cone. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform token mjc:physics:cone = "pyramidal"` | + /// | C++ Type | TfToken | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Token | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + /// | \ref MjcPhysicsTokens "Allowed Values" | pyramidal, elliptic | + MJCPHYSICS_API + UsdAttribute GetConeAttr() const; + + /// See GetConeAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateConeAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // JACOBIAN + // --------------------------------------------------------------------- // + /// The type of constraint Jacobian and matrices computed from it. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform token mjc:physics:jacobian = "auto"` | + /// | C++ Type | TfToken | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Token | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + /// | \ref MjcPhysicsTokens "Allowed Values" | auto, dense, sparse | + MJCPHYSICS_API + UsdAttribute GetJacobianAttr() const; + + /// See GetJacobianAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateJacobianAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // SOLVER + // --------------------------------------------------------------------- // + /// Constraint solver algorithm to be used. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform token mjc:physics:solver = "newton"` | + /// | C++ Type | TfToken | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Token | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + /// | \ref MjcPhysicsTokens "Allowed Values" | pgs, cg, newton | + MJCPHYSICS_API + UsdAttribute GetSolverAttr() const; + + /// See GetSolverAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateSolverAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // ITERATIONS + // --------------------------------------------------------------------- // + /// Maximum number of iterations of the constraint solver. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform int mjc:physics:iterations = 100` | + /// | C++ Type | int | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Int | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetIterationsAttr() const; + + /// See GetIterationsAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateIterationsAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // TOLERANCE + // --------------------------------------------------------------------- // + /// Tolerance threshold used for early termination of the iterative + /// solver. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double mjc:physics:tolerance = 1e-8` | + /// | C++ Type | double | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetToleranceAttr() const; + + /// See GetToleranceAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateToleranceAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // LSITERATIONS + // --------------------------------------------------------------------- // + /// Maximum number of linesearch iterations performed by CG/Newton + /// constraint solvers. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform int mjc:physics:ls_iterations = 50` | + /// | C++ Type | int | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Int | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetLSIterationsAttr() const; + + /// See GetLSIterationsAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateLSIterationsAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // LSTOLERANCE + // --------------------------------------------------------------------- // + /// Tolerance threshold used for early termination of the linesearch + /// algorithm. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double mjc:physics:ls_tolerance = 0.01` | + /// | C++ Type | double | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetLSToleranceAttr() const; + + /// See GetLSToleranceAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateLSToleranceAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // NOSLIPITERATIONS + // --------------------------------------------------------------------- // + /// Maximum number of iterations of the Noslip solver. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform int mjc:physics:noslip_iterations = 0` | + /// | C++ Type | int | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Int | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetNoslipIterationsAttr() const; + + /// See GetNoslipIterationsAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateNoslipIterationsAttr( + VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // NOSLIPTOLERANCE + // --------------------------------------------------------------------- // + /// Tolerance threshold used for early termination of the Noslip solver. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double mjc:physics:noslip_tolerance = 0.000001` | + /// | C++ Type | double | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetNoslipToleranceAttr() const; + + /// See GetNoslipToleranceAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateNoslipToleranceAttr( + VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // CCDITERATIONS + // --------------------------------------------------------------------- // + /// Maximum number of iterations of the algorithm used for convex collisions. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform int mjc:physics:ccd_iterations = 50` | + /// | C++ Type | int | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Int | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetCCDIterationsAttr() const; + + /// See GetCCDIterationsAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateCCDIterationsAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // CCDTOLERANCE + // --------------------------------------------------------------------- // + /// Tolerance threshold used for early termination of the convex + /// collision algorithm. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double mjc:physics:ccd_tolerance = 0.000001` | + /// | C++ Type | double | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetCCDToleranceAttr() const; + + /// See GetCCDToleranceAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateCCDToleranceAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // SDFITERATIONS + // --------------------------------------------------------------------- // + /// Number of iterations used for Signed Distance Field collisions + /// (per initial point). + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform int mjc:physics:sdf_iterations = 10` | + /// | C++ Type | int | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Int | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetSDFIterationsAttr() const; + + /// See GetSDFIterationsAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateSDFIterationsAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // SDFINITPOINTS + // --------------------------------------------------------------------- // + /// Number of starting points used for finding contacts with Signed + /// Distance Field collisions. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform int mjc:physics:sdf_initpoints = 40` | + /// | C++ Type | int | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Int | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetSDFInitPointsAttr() const; + + /// See GetSDFInitPointsAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateSDFInitPointsAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // ACTUATORGROUPDISABLE + // --------------------------------------------------------------------- // + /// List of actuator groups to disable. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform int[] mjc:physics:actuatorgroupdisable` | + /// | C++ Type | VtArray | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->IntArray | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetActuatorGroupDisableAttr() const; + + /// See GetActuatorGroupDisableAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateActuatorGroupDisableAttr( + VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // CONSTRAINTFLAG + // --------------------------------------------------------------------- // + /// Enables constraint solver. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:constraint = 1` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetConstraintFlagAttr() const; + + /// See GetConstraintFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateConstraintFlagAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // EQUALITYFLAG + // --------------------------------------------------------------------- // + /// Enables all standard computations related to equality constraints. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:equality = 1` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetEqualityFlagAttr() const; + + /// See GetEqualityFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateEqualityFlagAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // FRICTIONLOSSFLAG + // --------------------------------------------------------------------- // + /// Enables all standard computations related to friction loss constraints. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:frictionloss = 1` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetFrictionLossFlagAttr() const; + + /// See GetFrictionLossFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateFrictionLossFlagAttr( + VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // LIMITFLAG + // --------------------------------------------------------------------- // + /// Enables all standard computations related to joint and tendon limit + /// constraints. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:limit = 1` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetLimitFlagAttr() const; + + /// See GetLimitFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateLimitFlagAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // CONTACTFLAG + // --------------------------------------------------------------------- // + /// Enables collision detection and all standard computations related to + /// contact constraints. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:contact = 1` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetContactFlagAttr() const; + + /// See GetContactFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateContactFlagAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // PASSIVEFLAG + // --------------------------------------------------------------------- // + /// Enables the simulation of joint and tendon spring-dampers, fluid dynamics + /// forces, and custom passive forces. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:passive = 1` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetPassiveFlagAttr() const; + + /// See GetPassiveFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreatePassiveFlagAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // GRAVITYFLAG + // --------------------------------------------------------------------- // + /// Enables the application of gravitational acceleration as defined in + /// mjOption. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:gravity = 1` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetGravityFlagAttr() const; + + /// See GetGravityFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateGravityFlagAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // CLAMPCTRLFLAG + // --------------------------------------------------------------------- // + /// Enables the clamping of control inputs to all actuators, according to + /// actuator-specific attributes. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:clampctrl = 1` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetClampCtrlFlagAttr() const; + + /// See GetClampCtrlFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateClampCtrlFlagAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // WARMSTARTFLAG + // --------------------------------------------------------------------- // + /// Enables warm-starting of the constraint solver, using the solution from + /// the previous time step to initialize the iterative optimization. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:warmstart = 1` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetWarmStartFlagAttr() const; + + /// See GetWarmStartFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateWarmStartFlagAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // FILTERPARENTFLAG + // --------------------------------------------------------------------- // + /// Enables the filtering of contact pairs where the two geoms belong to a + /// parent and child body. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:filterparent = 1` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetFilterParentFlagAttr() const; + + /// See GetFilterParentFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateFilterParentFlagAttr( + VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // ACTUATIONFLAG + // --------------------------------------------------------------------- // + /// Enables all standard computations related to actuator forces, including + /// actuator dynamics. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:actuation = 1` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetActuationFlagAttr() const; + + /// See GetActuationFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateActuationFlagAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // REFSAFEFLAG + // --------------------------------------------------------------------- // + /// Enables a safety mechanism that prevents instabilities due to solref[0] + /// being too small compared to the simulation timestep. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:refsafe = 1` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetRefSafeFlagAttr() const; + + /// See GetRefSafeFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateRefSafeFlagAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // SENSORFLAG + // --------------------------------------------------------------------- // + /// Enables all computations related to sensors. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:sensor = 1` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetSensorFlagAttr() const; + + /// See GetSensorFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateSensorFlagAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MIDPHASEFLAG + // --------------------------------------------------------------------- // + /// Enables mid-phase collision filtering using a static AABB bounding volume + /// hierarchy (BVH). + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:midphase = 1` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetMidPhaseFlagAttr() const; + + /// See GetMidPhaseFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMidPhaseFlagAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // NATIVECCDFLAG + // --------------------------------------------------------------------- // + /// Enables the native convex collision detection pipeline instead of using + /// the libccd library. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:nativeccd = 1` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetNativeCCDFlagAttr() const; + + /// See GetNativeCCDFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateNativeCCDFlagAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // EULERDAMPFLAG + // --------------------------------------------------------------------- // + /// Enables implicit integration with respect to joint damping in the Euler + /// integrator. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:eulerdamp = 1` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetEulerDampFlagAttr() const; + + /// See GetEulerDampFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateEulerDampFlagAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // AUTORESETFLAG + // --------------------------------------------------------------------- // + /// Enables the automatic resetting of the simulation state when numerical + /// issues are detected. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:autoreset = 1` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetAutoResetFlagAttr() const; + + /// See GetAutoResetFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateAutoResetFlagAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // OVERRIDEFLAG + // --------------------------------------------------------------------- // + /// Enables the contact override mechanism. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:override = 0` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetOverrideFlagAttr() const; + + /// See GetOverrideFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateOverrideFlagAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // ENERGYFLAG + // --------------------------------------------------------------------- // + /// Enables the computation of potential and kinetic energy + /// (mjData.energy[0,1]). + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:energy = 0` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetEnergyFlagAttr() const; + + /// See GetEnergyFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateEnergyFlagAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // FWDINVFLAG + // --------------------------------------------------------------------- // + /// Enables the automatic comparison of forward and inverse dynamics. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:fwdinv = 0` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetFwdinvFlagAttr() const; + + /// See GetFwdinvFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateFwdinvFlagAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // INVDISCRETEFLAG + // --------------------------------------------------------------------- // + /// Enables discrete-time inverse dynamics with mj_inverse for integrators + /// other than RK4. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:invdiscrete = 0` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetInvDiscreteFlagAttr() const; + + /// See GetInvDiscreteFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateInvDiscreteFlagAttr( + VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MULTICCDFLAG + // --------------------------------------------------------------------- // + /// Enables multiple-contact collision detection for geom pairs using a + /// general-purpose convex-convex collider. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:multiccd = 0` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetMultiCCDFlagAttr() const; + + /// See GetMultiCCDFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMultiCCDFlagAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // ISLANDFLAG + // --------------------------------------------------------------------- // + /// Enables the discovery of constraint islands. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:physics:flag:island = 0` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetIslandFlagAttr() const; + + /// See GetIslandFlagAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateIslandFlagAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // ===================================================================== // + // Feel free to add custom code below this line, it will be preserved by + // the code generator. + // + // Just remember to: + // - Close the class declaration with }; + // - Close the namespace with PXR_NAMESPACE_CLOSE_SCOPE + // - Close the include guard with #endif + // ===================================================================== // + // --(BEGIN CUSTOM CODE)-- +}; + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif diff --git a/src/experimental/usd/mjcPhysics/siteAPI.cpp b/src/experimental/usd/mjcPhysics/siteAPI.cpp new file mode 100644 index 00000000..1693dd16 --- /dev/null +++ b/src/experimental/usd/mjcPhysics/siteAPI.cpp @@ -0,0 +1,99 @@ +// Copyright 2025 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. + +#include "./siteAPI.h" + +#include "pxr/usd/sdf/assetPath.h" +#include "pxr/usd/sdf/types.h" +#include "pxr/usd/usd/schemaRegistry.h" +#include "pxr/usd/usd/typed.h" + +PXR_NAMESPACE_OPEN_SCOPE + +// Register the schema with the TfType system. +TF_REGISTRY_FUNCTION(TfType) { + TfType::Define >(); +} + +/* virtual */ +MjcPhysicsSiteAPI::~MjcPhysicsSiteAPI() {} + +/* static */ +MjcPhysicsSiteAPI MjcPhysicsSiteAPI::Get(const UsdStagePtr &stage, + const SdfPath &path) { + if (!stage) { + TF_CODING_ERROR("Invalid stage"); + return MjcPhysicsSiteAPI(); + } + return MjcPhysicsSiteAPI(stage->GetPrimAtPath(path)); +} + +/* virtual */ +UsdSchemaKind MjcPhysicsSiteAPI::_GetSchemaKind() const { + return MjcPhysicsSiteAPI::schemaKind; +} + +/* static */ +bool MjcPhysicsSiteAPI::CanApply(const UsdPrim &prim, std::string *whyNot) { + return prim.CanApplyAPI(whyNot); +} + +/* static */ +MjcPhysicsSiteAPI MjcPhysicsSiteAPI::Apply(const UsdPrim &prim) { + if (prim.ApplyAPI()) { + return MjcPhysicsSiteAPI(prim); + } + return MjcPhysicsSiteAPI(); +} + +/* static */ +const TfType &MjcPhysicsSiteAPI::_GetStaticTfType() { + static TfType tfType = TfType::Find(); + return tfType; +} + +/* static */ +bool MjcPhysicsSiteAPI::_IsTypedSchema() { + static bool isTyped = _GetStaticTfType().IsA(); + return isTyped; +} + +/* virtual */ +const TfType &MjcPhysicsSiteAPI::_GetTfType() const { + return _GetStaticTfType(); +} + +/*static*/ +const TfTokenVector &MjcPhysicsSiteAPI::GetSchemaAttributeNames( + bool includeInherited) { + static TfTokenVector localNames; + static TfTokenVector allNames = + UsdAPISchemaBase::GetSchemaAttributeNames(true); + + if (includeInherited) + return allNames; + else + return localNames; +} + +PXR_NAMESPACE_CLOSE_SCOPE + +// ===================================================================== // +// Feel free to add custom code below this line. It will be preserved by +// the code generator. +// +// Just remember to wrap code in the appropriate delimiters: +// 'PXR_NAMESPACE_OPEN_SCOPE', 'PXR_NAMESPACE_CLOSE_SCOPE'. +// ===================================================================== // +// --(BEGIN CUSTOM CODE)-- diff --git a/src/experimental/usd/mjcPhysics/siteAPI.h b/src/experimental/usd/mjcPhysics/siteAPI.h new file mode 100644 index 00000000..e1981297 --- /dev/null +++ b/src/experimental/usd/mjcPhysics/siteAPI.h @@ -0,0 +1,158 @@ +// Copyright 2025 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. + +#ifndef MJCPHYSICS_GENERATED_SITEAPI_H +#define MJCPHYSICS_GENERATED_SITEAPI_H + +/// \file mjcPhysics/siteAPI.h + +#include "./api.h" +#include "pxr/base/gf/matrix4d.h" +#include "pxr/base/gf/vec3d.h" +#include "pxr/base/gf/vec3f.h" +#include "pxr/base/tf/token.h" +#include "pxr/base/tf/type.h" +#include "pxr/base/vt/value.h" +#include "pxr/pxr.h" +#include "pxr/usd/usd/apiSchemaBase.h" +#include "pxr/usd/usd/prim.h" +#include "pxr/usd/usd/stage.h" + +PXR_NAMESPACE_OPEN_SCOPE + +class SdfAssetPath; + +// -------------------------------------------------------------------------- // +// MJCSITEAPI // +// -------------------------------------------------------------------------- // + +/// \class MjcPhysicsSiteAPI +/// +/// API describing a Mujoco site. +/// +class MjcPhysicsSiteAPI : public UsdAPISchemaBase { + public: + /// Compile time constant representing what kind of schema this class is. + /// + /// \sa UsdSchemaKind + static const UsdSchemaKind schemaKind = UsdSchemaKind::SingleApplyAPI; + + /// Construct a MjcPhysicsSiteAPI on UsdPrim \p prim . + /// Equivalent to MjcPhysicsSiteAPI::Get(prim.GetStage(), prim.GetPath()) + /// for a \em valid \p prim, but will not immediately throw an error for + /// an invalid \p prim + explicit MjcPhysicsSiteAPI(const UsdPrim &prim = UsdPrim()) + : UsdAPISchemaBase(prim) {} + + /// Construct a MjcPhysicsSiteAPI on the prim held by \p schemaObj . + /// Should be preferred over MjcPhysicsSiteAPI(schemaObj.GetPrim()), + /// as it preserves SchemaBase state. + explicit MjcPhysicsSiteAPI(const UsdSchemaBase &schemaObj) + : UsdAPISchemaBase(schemaObj) {} + + /// Destructor. + MJCPHYSICS_API + virtual ~MjcPhysicsSiteAPI(); + + /// Return a vector of names of all pre-declared attributes for this schema + /// class and all its ancestor classes. Does not include attributes that + /// may be authored by custom/extended methods of the schemas involved. + MJCPHYSICS_API + static const TfTokenVector &GetSchemaAttributeNames( + bool includeInherited = true); + + /// Return a MjcPhysicsSiteAPI holding the prim adhering to this + /// schema at \p path on \p stage. If no prim exists at \p path on + /// \p stage, or if the prim at that path does not adhere to this schema, + /// return an invalid schema object. This is shorthand for the following: + /// + /// \code + /// MjcPhysicsSiteAPI(stage->GetPrimAtPath(path)); + /// \endcode + /// + MJCPHYSICS_API + static MjcPhysicsSiteAPI Get(const UsdStagePtr &stage, const SdfPath &path); + + /// Returns true if this single-apply API schema can be applied to + /// the given \p prim. If this schema can not be a applied to the prim, + /// this returns false and, if provided, populates \p whyNot with the + /// reason it can not be applied. + /// + /// Note that if CanApply returns false, that does not necessarily imply + /// that calling Apply will fail. Callers are expected to call CanApply + /// before calling Apply if they want to ensure that it is valid to + /// apply a schema. + /// + /// \sa UsdPrim::GetAppliedSchemas() + /// \sa UsdPrim::HasAPI() + /// \sa UsdPrim::CanApplyAPI() + /// \sa UsdPrim::ApplyAPI() + /// \sa UsdPrim::RemoveAPI() + /// + MJCPHYSICS_API + static bool CanApply(const UsdPrim &prim, std::string *whyNot = nullptr); + + /// Applies this single-apply API schema to the given \p prim. + /// This information is stored by adding "MjcSiteAPI" to the + /// token-valued, listOp metadata \em apiSchemas on the prim. + /// + /// \return A valid MjcPhysicsSiteAPI object is returned upon success. + /// An invalid (or empty) MjcPhysicsSiteAPI object is returned upon + /// failure. See \ref UsdPrim::ApplyAPI() for conditions + /// resulting in failure. + /// + /// \sa UsdPrim::GetAppliedSchemas() + /// \sa UsdPrim::HasAPI() + /// \sa UsdPrim::CanApplyAPI() + /// \sa UsdPrim::ApplyAPI() + /// \sa UsdPrim::RemoveAPI() + /// + MJCPHYSICS_API + static MjcPhysicsSiteAPI Apply(const UsdPrim &prim); + + protected: + /// Returns the kind of schema this class belongs to. + /// + /// \sa UsdSchemaKind + MJCPHYSICS_API + UsdSchemaKind _GetSchemaKind() const override; + + private: + // needs to invoke _GetStaticTfType. + friend class UsdSchemaRegistry; + MJCPHYSICS_API + static const TfType &_GetStaticTfType(); + + static bool _IsTypedSchema(); + + // override SchemaBase virtuals. + MJCPHYSICS_API + const TfType &_GetTfType() const override; + + public: + // ===================================================================== // + // Feel free to add custom code below this line, it will be preserved by + // the code generator. + // + // Just remember to: + // - Close the class declaration with }; + // - Close the namespace with PXR_NAMESPACE_CLOSE_SCOPE + // - Close the include guard with #endif + // ===================================================================== // + // --(BEGIN CUSTOM CODE)-- +}; + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif diff --git a/src/experimental/usd/mjcPhysics/tokens.cpp b/src/experimental/usd/mjcPhysics/tokens.cpp new file mode 100644 index 00000000..b6bfd4b8 --- /dev/null +++ b/src/experimental/usd/mjcPhysics/tokens.cpp @@ -0,0 +1,156 @@ +// Copyright 2025 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. + +#include "./tokens.h" + +PXR_NAMESPACE_OPEN_SCOPE + +MjcPhysicsTokensType::MjcPhysicsTokensType() + : auto_("auto", TfToken::Immortal), + cg("cg", TfToken::Immortal), + dense("dense", TfToken::Immortal), + elliptic("elliptic", TfToken::Immortal), + euler("euler", TfToken::Immortal), + implicit("implicit", TfToken::Immortal), + implicitfast("implicitfast", TfToken::Immortal), + mjcPhysicsActuatorgroupdisable("mjc:physics:actuatorgroupdisable", + TfToken::Immortal), + mjcPhysicsApirate("mjc:physics:apirate", TfToken::Immortal), + mjcPhysicsCcd_iterations("mjc:physics:ccd_iterations", TfToken::Immortal), + mjcPhysicsCcd_tolerance("mjc:physics:ccd_tolerance", TfToken::Immortal), + mjcPhysicsCone("mjc:physics:cone", TfToken::Immortal), + mjcPhysicsDensity("mjc:physics:density", TfToken::Immortal), + mjcPhysicsFlagActuation("mjc:physics:flag:actuation", TfToken::Immortal), + mjcPhysicsFlagAutoreset("mjc:physics:flag:autoreset", TfToken::Immortal), + mjcPhysicsFlagClampctrl("mjc:physics:flag:clampctrl", TfToken::Immortal), + mjcPhysicsFlagConstraint("mjc:physics:flag:constraint", + TfToken::Immortal), + mjcPhysicsFlagContact("mjc:physics:flag:contact", TfToken::Immortal), + mjcPhysicsFlagEnergy("mjc:physics:flag:energy", TfToken::Immortal), + mjcPhysicsFlagEquality("mjc:physics:flag:equality", TfToken::Immortal), + mjcPhysicsFlagEulerdamp("mjc:physics:flag:eulerdamp", TfToken::Immortal), + mjcPhysicsFlagFilterparent("mjc:physics:flag:filterparent", + TfToken::Immortal), + mjcPhysicsFlagFrictionloss("mjc:physics:flag:frictionloss", + TfToken::Immortal), + mjcPhysicsFlagFwdinv("mjc:physics:flag:fwdinv", TfToken::Immortal), + mjcPhysicsFlagGravity("mjc:physics:flag:gravity", TfToken::Immortal), + mjcPhysicsFlagInvdiscrete("mjc:physics:flag:invdiscrete", + TfToken::Immortal), + mjcPhysicsFlagIsland("mjc:physics:flag:island", TfToken::Immortal), + mjcPhysicsFlagLimit("mjc:physics:flag:limit", TfToken::Immortal), + mjcPhysicsFlagMidphase("mjc:physics:flag:midphase", TfToken::Immortal), + mjcPhysicsFlagMulticcd("mjc:physics:flag:multiccd", TfToken::Immortal), + mjcPhysicsFlagNativeccd("mjc:physics:flag:nativeccd", TfToken::Immortal), + mjcPhysicsFlagOverride("mjc:physics:flag:override", TfToken::Immortal), + mjcPhysicsFlagPassive("mjc:physics:flag:passive", TfToken::Immortal), + mjcPhysicsFlagRefsafe("mjc:physics:flag:refsafe", TfToken::Immortal), + mjcPhysicsFlagSensor("mjc:physics:flag:sensor", TfToken::Immortal), + mjcPhysicsFlagWarmstart("mjc:physics:flag:warmstart", TfToken::Immortal), + mjcPhysicsImpratio("mjc:physics:impratio", TfToken::Immortal), + mjcPhysicsIntegrator("mjc:physics:integrator", TfToken::Immortal), + mjcPhysicsIterations("mjc:physics:iterations", TfToken::Immortal), + mjcPhysicsJacobian("mjc:physics:jacobian", TfToken::Immortal), + mjcPhysicsLs_iterations("mjc:physics:ls_iterations", TfToken::Immortal), + mjcPhysicsLs_tolerance("mjc:physics:ls_tolerance", TfToken::Immortal), + mjcPhysicsMagnetic("mjc:physics:magnetic", TfToken::Immortal), + mjcPhysicsNoslip_iterations("mjc:physics:noslip_iterations", + TfToken::Immortal), + mjcPhysicsNoslip_tolerance("mjc:physics:noslip_tolerance", + TfToken::Immortal), + mjcPhysicsO_friction("mjc:physics:o_friction", TfToken::Immortal), + mjcPhysicsO_margin("mjc:physics:o_margin", TfToken::Immortal), + mjcPhysicsO_solimp("mjc:physics:o_solimp", TfToken::Immortal), + mjcPhysicsO_solref("mjc:physics:o_solref", TfToken::Immortal), + mjcPhysicsSdf_initpoints("mjc:physics:sdf_initpoints", TfToken::Immortal), + mjcPhysicsSdf_iterations("mjc:physics:sdf_iterations", TfToken::Immortal), + mjcPhysicsSolver("mjc:physics:solver", TfToken::Immortal), + mjcPhysicsTimestep("mjc:physics:timestep", TfToken::Immortal), + mjcPhysicsTolerance("mjc:physics:tolerance", TfToken::Immortal), + mjcPhysicsViscosity("mjc:physics:viscosity", TfToken::Immortal), + mjcPhysicsWind("mjc:physics:wind", TfToken::Immortal), + newton("newton", TfToken::Immortal), + pgs("pgs", TfToken::Immortal), + pyramidal("pyramidal", TfToken::Immortal), + rk4("rk4", TfToken::Immortal), + sparse("sparse", TfToken::Immortal), + MjcPhysicsSceneAPI("MjcPhysicsSceneAPI", TfToken::Immortal), + MjcSiteAPI("MjcSiteAPI", TfToken::Immortal), + allTokens({auto_, + cg, + dense, + elliptic, + euler, + implicit, + implicitfast, + mjcPhysicsActuatorgroupdisable, + mjcPhysicsApirate, + mjcPhysicsCcd_iterations, + mjcPhysicsCcd_tolerance, + mjcPhysicsCone, + mjcPhysicsDensity, + mjcPhysicsFlagActuation, + mjcPhysicsFlagAutoreset, + mjcPhysicsFlagClampctrl, + mjcPhysicsFlagConstraint, + mjcPhysicsFlagContact, + mjcPhysicsFlagEnergy, + mjcPhysicsFlagEquality, + mjcPhysicsFlagEulerdamp, + mjcPhysicsFlagFilterparent, + mjcPhysicsFlagFrictionloss, + mjcPhysicsFlagFwdinv, + mjcPhysicsFlagGravity, + mjcPhysicsFlagInvdiscrete, + mjcPhysicsFlagIsland, + mjcPhysicsFlagLimit, + mjcPhysicsFlagMidphase, + mjcPhysicsFlagMulticcd, + mjcPhysicsFlagNativeccd, + mjcPhysicsFlagOverride, + mjcPhysicsFlagPassive, + mjcPhysicsFlagRefsafe, + mjcPhysicsFlagSensor, + mjcPhysicsFlagWarmstart, + mjcPhysicsImpratio, + mjcPhysicsIntegrator, + mjcPhysicsIterations, + mjcPhysicsJacobian, + mjcPhysicsLs_iterations, + mjcPhysicsLs_tolerance, + mjcPhysicsMagnetic, + mjcPhysicsNoslip_iterations, + mjcPhysicsNoslip_tolerance, + mjcPhysicsO_friction, + mjcPhysicsO_margin, + mjcPhysicsO_solimp, + mjcPhysicsO_solref, + mjcPhysicsSdf_initpoints, + mjcPhysicsSdf_iterations, + mjcPhysicsSolver, + mjcPhysicsTimestep, + mjcPhysicsTolerance, + mjcPhysicsViscosity, + mjcPhysicsWind, + newton, + pgs, + pyramidal, + rk4, + sparse, + MjcPhysicsSceneAPI, + MjcSiteAPI}) {} + +TfStaticData MjcPhysicsTokens; + +PXR_NAMESPACE_CLOSE_SCOPE diff --git a/src/experimental/usd/mjcPhysics/tokens.h b/src/experimental/usd/mjcPhysics/tokens.h new file mode 100644 index 00000000..8d6b5f92 --- /dev/null +++ b/src/experimental/usd/mjcPhysics/tokens.h @@ -0,0 +1,332 @@ +// Copyright 2025 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. + +#ifndef MJCPHYSICS_TOKENS_H +#define MJCPHYSICS_TOKENS_H + +/// \file mjcPhysics/tokens.h + +// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +// +// This is an automatically generated file (by usdGenSchema.py). +// Do not hand-edit! +// +// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + +#include + +#include "./api.h" +#include "pxr/base/tf/staticData.h" +#include "pxr/base/tf/token.h" +#include "pxr/pxr.h" + +PXR_NAMESPACE_OPEN_SCOPE + +/// \class MjcPhysicsTokensType +/// +/// \link MjcPhysicsTokens \endlink provides static, efficient +/// \link TfToken TfTokens\endlink for use in all public USD API. +/// +/// These tokens are auto-generated from the module's schema, representing +/// property names, for when you need to fetch an attribute or relationship +/// directly by name, e.g. UsdPrim::GetAttribute(), in the most efficient +/// manner, and allow the compiler to verify that you spelled the name +/// correctly. +/// +/// MjcPhysicsTokens also contains all of the \em allowedTokens values +/// declared for schema builtin attributes of 'token' scene description type. +/// Use MjcPhysicsTokens like so: +/// +/// \code +/// gprim.GetMyTokenValuedAttr().Set(MjcPhysicsTokens->auto_); +/// \endcode +struct MjcPhysicsTokensType { + MJCPHYSICS_API MjcPhysicsTokensType(); + /// \brief "auto" + /// + /// Fallback value for MjcPhysicsSceneAPI::GetJacobianAttr(), This token + /// represents the auto constraint Jacobian and matrices computed from it. + const TfToken auto_; + /// \brief "cg" + /// + /// Possible value for MjcPhysicsSceneAPI::GetSolverAttr(), This token + /// represents the CG constraint solver algorithm. + const TfToken cg; + /// \brief "dense" + /// + /// Possible value for MjcPhysicsSceneAPI::GetJacobianAttr(), This token + /// represents the dense constraint Jacobian and matrices computed from it. + const TfToken dense; + /// \brief "elliptic" + /// + /// Possible value for MjcPhysicsSceneAPI::GetConeAttr(), This token + /// represents the elliptic contact friction cone type. + const TfToken elliptic; + /// \brief "euler" + /// + /// Fallback value for MjcPhysicsSceneAPI::GetIntegratorAttr(), This token + /// represents the Euler numerical integrator. + const TfToken euler; + /// \brief "implicit" + /// + /// Possible value for MjcPhysicsSceneAPI::GetIntegratorAttr(), This token + /// represents the implicit numerical integrator. + const TfToken implicit; + /// \brief "implicitfast" + /// + /// Possible value for MjcPhysicsSceneAPI::GetIntegratorAttr(), This token + /// represents the implicitfast numerical integrator. + const TfToken implicitfast; + /// \brief "mjc:physics:actuatorgroupdisable" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsActuatorgroupdisable; + /// \brief "mjc:physics:apirate" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsApirate; + /// \brief "mjc:physics:ccd_iterations" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsCcd_iterations; + /// \brief "mjc:physics:ccd_tolerance" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsCcd_tolerance; + /// \brief "mjc:physics:cone" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsCone; + /// \brief "mjc:physics:density" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsDensity; + /// \brief "mjc:physics:flag:actuation" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagActuation; + /// \brief "mjc:physics:flag:autoreset" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagAutoreset; + /// \brief "mjc:physics:flag:clampctrl" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagClampctrl; + /// \brief "mjc:physics:flag:constraint" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagConstraint; + /// \brief "mjc:physics:flag:contact" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagContact; + /// \brief "mjc:physics:flag:energy" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagEnergy; + /// \brief "mjc:physics:flag:equality" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagEquality; + /// \brief "mjc:physics:flag:eulerdamp" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagEulerdamp; + /// \brief "mjc:physics:flag:filterparent" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagFilterparent; + /// \brief "mjc:physics:flag:frictionloss" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagFrictionloss; + /// \brief "mjc:physics:flag:fwdinv" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagFwdinv; + /// \brief "mjc:physics:flag:gravity" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagGravity; + /// \brief "mjc:physics:flag:invdiscrete" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagInvdiscrete; + /// \brief "mjc:physics:flag:island" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagIsland; + /// \brief "mjc:physics:flag:limit" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagLimit; + /// \brief "mjc:physics:flag:midphase" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagMidphase; + /// \brief "mjc:physics:flag:multiccd" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagMulticcd; + /// \brief "mjc:physics:flag:nativeccd" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagNativeccd; + /// \brief "mjc:physics:flag:override" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagOverride; + /// \brief "mjc:physics:flag:passive" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagPassive; + /// \brief "mjc:physics:flag:refsafe" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagRefsafe; + /// \brief "mjc:physics:flag:sensor" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagSensor; + /// \brief "mjc:physics:flag:warmstart" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsFlagWarmstart; + /// \brief "mjc:physics:impratio" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsImpratio; + /// \brief "mjc:physics:integrator" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsIntegrator; + /// \brief "mjc:physics:iterations" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsIterations; + /// \brief "mjc:physics:jacobian" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsJacobian; + /// \brief "mjc:physics:ls_iterations" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsLs_iterations; + /// \brief "mjc:physics:ls_tolerance" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsLs_tolerance; + /// \brief "mjc:physics:magnetic" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsMagnetic; + /// \brief "mjc:physics:noslip_iterations" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsNoslip_iterations; + /// \brief "mjc:physics:noslip_tolerance" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsNoslip_tolerance; + /// \brief "mjc:physics:o_friction" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsO_friction; + /// \brief "mjc:physics:o_margin" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsO_margin; + /// \brief "mjc:physics:o_solimp" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsO_solimp; + /// \brief "mjc:physics:o_solref" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsO_solref; + /// \brief "mjc:physics:sdf_initpoints" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsSdf_initpoints; + /// \brief "mjc:physics:sdf_iterations" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsSdf_iterations; + /// \brief "mjc:physics:solver" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsSolver; + /// \brief "mjc:physics:timestep" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsTimestep; + /// \brief "mjc:physics:tolerance" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsTolerance; + /// \brief "mjc:physics:viscosity" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsViscosity; + /// \brief "mjc:physics:wind" + /// + /// MjcPhysicsSceneAPI + const TfToken mjcPhysicsWind; + /// \brief "newton" + /// + /// Fallback value for MjcPhysicsSceneAPI::GetSolverAttr(), This token + /// represents the Newton constraint solver algorithm. + const TfToken newton; + /// \brief "pgs" + /// + /// Possible value for MjcPhysicsSceneAPI::GetSolverAttr(), This token + /// represents the PGS constraint solver algorithm. + const TfToken pgs; + /// \brief "pyramidal" + /// + /// Fallback value for MjcPhysicsSceneAPI::GetConeAttr(), This token + /// represents the pyramidal contact friction cone type. + const TfToken pyramidal; + /// \brief "rk4" + /// + /// Possible value for MjcPhysicsSceneAPI::GetIntegratorAttr(), This token + /// represents the RK4 numerical integrator. + const TfToken rk4; + /// \brief "sparse" + /// + /// Possible value for MjcPhysicsSceneAPI::GetJacobianAttr(), This token + /// represents the sparse constraint Jacobian and matrices computed from it. + const TfToken sparse; + /// \brief "MjcPhysicsSceneAPI" + /// + /// Schema identifer and family for MjcPhysicsSceneAPI + const TfToken MjcPhysicsSceneAPI; + /// \brief "MjcSiteAPI" + /// + /// Schema identifer and family for MjcPhysicsSiteAPI + const TfToken MjcSiteAPI; + /// A vector of all of the tokens listed above. + const std::vector allTokens; +}; + +/// \var MjcPhysicsTokens +/// +/// A global variable with static, efficient \link TfToken TfTokens\endlink +/// for use in all public USD API. \sa MjcPhysicsTokensType +extern MJCPHYSICS_API TfStaticData MjcPhysicsTokens; + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif diff --git a/src/experimental/usd/mjcPhysics/wrapSceneAPI.cpp b/src/experimental/usd/mjcPhysics/wrapSceneAPI.cpp new file mode 100644 index 00000000..f8936938 --- /dev/null +++ b/src/experimental/usd/mjcPhysics/wrapSceneAPI.cpp @@ -0,0 +1,653 @@ +// Copyright 2025 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. + +#include +#include + +#include "./sceneAPI.h" +#include "pxr/base/tf/pyAnnotatedBoolResult.h" +#include "pxr/base/tf/pyContainerConversions.h" +#include "pxr/base/tf/pyResultConversions.h" +#include "pxr/base/tf/pyUtils.h" +#include "pxr/base/tf/wrapTypeHelpers.h" +#include "pxr/usd/sdf/primSpec.h" +#include "pxr/usd/usd/pyConversions.h" +#include "pxr/usd/usd/schemaBase.h" + +using namespace boost::python; + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace { + +#define WRAP_CUSTOM \ + template \ + static void _CustomWrapCode(Cls &_class) + +// fwd decl. +WRAP_CUSTOM; + +static UsdAttribute _CreateTimestepAttr(MjcPhysicsSceneAPI &self, + object defaultVal, bool writeSparsely) { + return self.CreateTimestepAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double), writeSparsely); +} + +static UsdAttribute _CreateApiRateAttr(MjcPhysicsSceneAPI &self, + object defaultVal, bool writeSparsely) { + return self.CreateApiRateAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double), writeSparsely); +} + +static UsdAttribute _CreateImpRatioAttr(MjcPhysicsSceneAPI &self, + object defaultVal, bool writeSparsely) { + return self.CreateImpRatioAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double), writeSparsely); +} + +static UsdAttribute _CreateWindAttr(MjcPhysicsSceneAPI &self, object defaultVal, + bool writeSparsely) { + return self.CreateWindAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double3), + writeSparsely); +} + +static UsdAttribute _CreateMagneticAttr(MjcPhysicsSceneAPI &self, + object defaultVal, bool writeSparsely) { + return self.CreateMagneticAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double3), + writeSparsely); +} + +static UsdAttribute _CreateDensityAttr(MjcPhysicsSceneAPI &self, + object defaultVal, bool writeSparsely) { + return self.CreateDensityAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double), writeSparsely); +} + +static UsdAttribute _CreateViscosityAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateViscosityAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double), writeSparsely); +} + +static UsdAttribute _CreateOMarginAttr(MjcPhysicsSceneAPI &self, + object defaultVal, bool writeSparsely) { + return self.CreateOMarginAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double), writeSparsely); +} + +static UsdAttribute _CreateOSolRefAttr(MjcPhysicsSceneAPI &self, + object defaultVal, bool writeSparsely) { + return self.CreateOSolRefAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->DoubleArray), + writeSparsely); +} + +static UsdAttribute _CreateOSolImpAttr(MjcPhysicsSceneAPI &self, + object defaultVal, bool writeSparsely) { + return self.CreateOSolImpAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->DoubleArray), + writeSparsely); +} + +static UsdAttribute _CreateOFrictionAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateOFrictionAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->DoubleArray), + writeSparsely); +} + +static UsdAttribute _CreateIntegratorAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateIntegratorAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Token), writeSparsely); +} + +static UsdAttribute _CreateConeAttr(MjcPhysicsSceneAPI &self, object defaultVal, + bool writeSparsely) { + return self.CreateConeAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Token), writeSparsely); +} + +static UsdAttribute _CreateJacobianAttr(MjcPhysicsSceneAPI &self, + object defaultVal, bool writeSparsely) { + return self.CreateJacobianAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Token), writeSparsely); +} + +static UsdAttribute _CreateSolverAttr(MjcPhysicsSceneAPI &self, + object defaultVal, bool writeSparsely) { + return self.CreateSolverAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Token), writeSparsely); +} + +static UsdAttribute _CreateIterationsAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateIterationsAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Int), writeSparsely); +} + +static UsdAttribute _CreateToleranceAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateToleranceAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double), writeSparsely); +} + +static UsdAttribute _CreateLSIterationsAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateLSIterationsAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Int), writeSparsely); +} + +static UsdAttribute _CreateLSToleranceAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateLSToleranceAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double), writeSparsely); +} + +static UsdAttribute _CreateNoslipIterationsAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateNoslipIterationsAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Int), writeSparsely); +} + +static UsdAttribute _CreateNoslipToleranceAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateNoslipToleranceAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double), writeSparsely); +} + +static UsdAttribute _CreateCCDIterationsAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateCCDIterationsAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Int), writeSparsely); +} + +static UsdAttribute _CreateCCDToleranceAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateCCDToleranceAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double), writeSparsely); +} + +static UsdAttribute _CreateSDFIterationsAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateSDFIterationsAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Int), writeSparsely); +} + +static UsdAttribute _CreateSDFInitPointsAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateSDFInitPointsAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Int), writeSparsely); +} + +static UsdAttribute _CreateActuatorGroupDisableAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateActuatorGroupDisableAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->IntArray), + writeSparsely); +} + +static UsdAttribute _CreateConstraintFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateConstraintFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateEqualityFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateEqualityFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateFrictionLossFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateFrictionLossFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateLimitFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateLimitFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateContactFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateContactFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreatePassiveFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreatePassiveFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateGravityFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateGravityFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateClampCtrlFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateClampCtrlFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateWarmStartFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateWarmStartFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateFilterParentFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateFilterParentFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateActuationFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateActuationFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateRefSafeFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateRefSafeFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateSensorFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateSensorFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateMidPhaseFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateMidPhaseFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateNativeCCDFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateNativeCCDFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateEulerDampFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateEulerDampFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateAutoResetFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateAutoResetFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateOverrideFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateOverrideFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateEnergyFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateEnergyFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateFwdinvFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateFwdinvFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateInvDiscreteFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateInvDiscreteFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateMultiCCDFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateMultiCCDFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static UsdAttribute _CreateIslandFlagAttr(MjcPhysicsSceneAPI &self, + object defaultVal, + bool writeSparsely) { + return self.CreateIslandFlagAttr( + UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); +} + +static std::string _Repr(const MjcPhysicsSceneAPI &self) { + std::string primRepr = TfPyRepr(self.GetPrim()); + return TfStringPrintf("MjcPhysics.SceneAPI(%s)", primRepr.c_str()); +} + +struct MjcPhysicsSceneAPI_CanApplyResult + : public TfPyAnnotatedBoolResult { + MjcPhysicsSceneAPI_CanApplyResult(bool val, std::string const &msg) + : TfPyAnnotatedBoolResult(val, msg) {} +}; + +static MjcPhysicsSceneAPI_CanApplyResult _WrapCanApply(const UsdPrim &prim) { + std::string whyNot; + bool result = MjcPhysicsSceneAPI::CanApply(prim, &whyNot); + return MjcPhysicsSceneAPI_CanApplyResult(result, whyNot); +} + +} // anonymous namespace + +void wrapMjcPhysicsSceneAPI() { + typedef MjcPhysicsSceneAPI This; + + MjcPhysicsSceneAPI_CanApplyResult::Wrap( + "_CanApplyResult", "whyNot"); + + class_ > cls("SceneAPI"); + + cls.def(init(arg("prim"))) + .def(init(arg("schemaObj"))) + .def(TfTypePythonClass()) + + .def("Get", &This::Get, (arg("stage"), arg("path"))) + .staticmethod("Get") + + .def("CanApply", &_WrapCanApply, (arg("prim"))) + .staticmethod("CanApply") + + .def("Apply", &This::Apply, (arg("prim"))) + .staticmethod("Apply") + + .def("GetSchemaAttributeNames", &This::GetSchemaAttributeNames, + arg("includeInherited") = true, + return_value_policy()) + .staticmethod("GetSchemaAttributeNames") + + .def("_GetStaticTfType", (TfType const &(*)())TfType::Find, + return_value_policy()) + .staticmethod("_GetStaticTfType") + + .def(!self) + + .def("GetTimestepAttr", &This::GetTimestepAttr) + .def("CreateTimestepAttr", &_CreateTimestepAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetApiRateAttr", &This::GetApiRateAttr) + .def("CreateApiRateAttr", &_CreateApiRateAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetImpRatioAttr", &This::GetImpRatioAttr) + .def("CreateImpRatioAttr", &_CreateImpRatioAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetWindAttr", &This::GetWindAttr) + .def("CreateWindAttr", &_CreateWindAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetMagneticAttr", &This::GetMagneticAttr) + .def("CreateMagneticAttr", &_CreateMagneticAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetDensityAttr", &This::GetDensityAttr) + .def("CreateDensityAttr", &_CreateDensityAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetViscosityAttr", &This::GetViscosityAttr) + .def("CreateViscosityAttr", &_CreateViscosityAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetOMarginAttr", &This::GetOMarginAttr) + .def("CreateOMarginAttr", &_CreateOMarginAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetOSolRefAttr", &This::GetOSolRefAttr) + .def("CreateOSolRefAttr", &_CreateOSolRefAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetOSolImpAttr", &This::GetOSolImpAttr) + .def("CreateOSolImpAttr", &_CreateOSolImpAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetOFrictionAttr", &This::GetOFrictionAttr) + .def("CreateOFrictionAttr", &_CreateOFrictionAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetIntegratorAttr", &This::GetIntegratorAttr) + .def("CreateIntegratorAttr", &_CreateIntegratorAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetConeAttr", &This::GetConeAttr) + .def("CreateConeAttr", &_CreateConeAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetJacobianAttr", &This::GetJacobianAttr) + .def("CreateJacobianAttr", &_CreateJacobianAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetSolverAttr", &This::GetSolverAttr) + .def("CreateSolverAttr", &_CreateSolverAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetIterationsAttr", &This::GetIterationsAttr) + .def("CreateIterationsAttr", &_CreateIterationsAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetToleranceAttr", &This::GetToleranceAttr) + .def("CreateToleranceAttr", &_CreateToleranceAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetLSIterationsAttr", &This::GetLSIterationsAttr) + .def("CreateLSIterationsAttr", &_CreateLSIterationsAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetLSToleranceAttr", &This::GetLSToleranceAttr) + .def("CreateLSToleranceAttr", &_CreateLSToleranceAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetNoslipIterationsAttr", &This::GetNoslipIterationsAttr) + .def("CreateNoslipIterationsAttr", &_CreateNoslipIterationsAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetNoslipToleranceAttr", &This::GetNoslipToleranceAttr) + .def("CreateNoslipToleranceAttr", &_CreateNoslipToleranceAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetCCDIterationsAttr", &This::GetCCDIterationsAttr) + .def("CreateCCDIterationsAttr", &_CreateCCDIterationsAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetCCDToleranceAttr", &This::GetCCDToleranceAttr) + .def("CreateCCDToleranceAttr", &_CreateCCDToleranceAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetSDFIterationsAttr", &This::GetSDFIterationsAttr) + .def("CreateSDFIterationsAttr", &_CreateSDFIterationsAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetSDFInitPointsAttr", &This::GetSDFInitPointsAttr) + .def("CreateSDFInitPointsAttr", &_CreateSDFInitPointsAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetActuatorGroupDisableAttr", &This::GetActuatorGroupDisableAttr) + .def("CreateActuatorGroupDisableAttr", &_CreateActuatorGroupDisableAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetConstraintFlagAttr", &This::GetConstraintFlagAttr) + .def("CreateConstraintFlagAttr", &_CreateConstraintFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetEqualityFlagAttr", &This::GetEqualityFlagAttr) + .def("CreateEqualityFlagAttr", &_CreateEqualityFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetFrictionLossFlagAttr", &This::GetFrictionLossFlagAttr) + .def("CreateFrictionLossFlagAttr", &_CreateFrictionLossFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetLimitFlagAttr", &This::GetLimitFlagAttr) + .def("CreateLimitFlagAttr", &_CreateLimitFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetContactFlagAttr", &This::GetContactFlagAttr) + .def("CreateContactFlagAttr", &_CreateContactFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetPassiveFlagAttr", &This::GetPassiveFlagAttr) + .def("CreatePassiveFlagAttr", &_CreatePassiveFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetGravityFlagAttr", &This::GetGravityFlagAttr) + .def("CreateGravityFlagAttr", &_CreateGravityFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetClampCtrlFlagAttr", &This::GetClampCtrlFlagAttr) + .def("CreateClampCtrlFlagAttr", &_CreateClampCtrlFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetWarmStartFlagAttr", &This::GetWarmStartFlagAttr) + .def("CreateWarmStartFlagAttr", &_CreateWarmStartFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetFilterParentFlagAttr", &This::GetFilterParentFlagAttr) + .def("CreateFilterParentFlagAttr", &_CreateFilterParentFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetActuationFlagAttr", &This::GetActuationFlagAttr) + .def("CreateActuationFlagAttr", &_CreateActuationFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetRefSafeFlagAttr", &This::GetRefSafeFlagAttr) + .def("CreateRefSafeFlagAttr", &_CreateRefSafeFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetSensorFlagAttr", &This::GetSensorFlagAttr) + .def("CreateSensorFlagAttr", &_CreateSensorFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetMidPhaseFlagAttr", &This::GetMidPhaseFlagAttr) + .def("CreateMidPhaseFlagAttr", &_CreateMidPhaseFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetNativeCCDFlagAttr", &This::GetNativeCCDFlagAttr) + .def("CreateNativeCCDFlagAttr", &_CreateNativeCCDFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetEulerDampFlagAttr", &This::GetEulerDampFlagAttr) + .def("CreateEulerDampFlagAttr", &_CreateEulerDampFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetAutoResetFlagAttr", &This::GetAutoResetFlagAttr) + .def("CreateAutoResetFlagAttr", &_CreateAutoResetFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetOverrideFlagAttr", &This::GetOverrideFlagAttr) + .def("CreateOverrideFlagAttr", &_CreateOverrideFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetEnergyFlagAttr", &This::GetEnergyFlagAttr) + .def("CreateEnergyFlagAttr", &_CreateEnergyFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetFwdinvFlagAttr", &This::GetFwdinvFlagAttr) + .def("CreateFwdinvFlagAttr", &_CreateFwdinvFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetInvDiscreteFlagAttr", &This::GetInvDiscreteFlagAttr) + .def("CreateInvDiscreteFlagAttr", &_CreateInvDiscreteFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetMultiCCDFlagAttr", &This::GetMultiCCDFlagAttr) + .def("CreateMultiCCDFlagAttr", &_CreateMultiCCDFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("GetIslandFlagAttr", &This::GetIslandFlagAttr) + .def("CreateIslandFlagAttr", &_CreateIslandFlagAttr, + (arg("defaultValue") = object(), arg("writeSparsely") = false)) + + .def("__repr__", ::_Repr); + + _CustomWrapCode(cls); +} + +// ===================================================================== // +// Feel free to add custom code below this line, it will be preserved by +// the code generator. The entry point for your custom code should look +// minimally like the following: +// +// WRAP_CUSTOM { +// _class +// .def("MyCustomMethod", ...) +// ; +// } +// +// Of course any other ancillary or support code may be provided. +// +// Just remember to wrap code in the appropriate delimiters: +// 'namespace {', '}'. +// +// ===================================================================== // +// --(BEGIN CUSTOM CODE)-- + +namespace { + +WRAP_CUSTOM {} + +} // namespace diff --git a/src/experimental/usd/mjcPhysics/wrapSiteAPI.cpp b/src/experimental/usd/mjcPhysics/wrapSiteAPI.cpp new file mode 100644 index 00000000..267978d7 --- /dev/null +++ b/src/experimental/usd/mjcPhysics/wrapSiteAPI.cpp @@ -0,0 +1,120 @@ +// Copyright 2025 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. + +#include +#include + +#include "./siteAPI.h" +#include "pxr/base/tf/pyAnnotatedBoolResult.h" +#include "pxr/base/tf/pyContainerConversions.h" +#include "pxr/base/tf/pyResultConversions.h" +#include "pxr/base/tf/pyUtils.h" +#include "pxr/base/tf/wrapTypeHelpers.h" +#include "pxr/usd/sdf/primSpec.h" +#include "pxr/usd/usd/pyConversions.h" +#include "pxr/usd/usd/schemaBase.h" + +using namespace boost::python; + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace { + +#define WRAP_CUSTOM \ + template \ + static void _CustomWrapCode(Cls &_class) + +// fwd decl. +WRAP_CUSTOM; + +static std::string _Repr(const MjcPhysicsSiteAPI &self) { + std::string primRepr = TfPyRepr(self.GetPrim()); + return TfStringPrintf("MjcPhysics.SiteAPI(%s)", primRepr.c_str()); +} + +struct MjcPhysicsSiteAPI_CanApplyResult + : public TfPyAnnotatedBoolResult { + MjcPhysicsSiteAPI_CanApplyResult(bool val, std::string const &msg) + : TfPyAnnotatedBoolResult(val, msg) {} +}; + +static MjcPhysicsSiteAPI_CanApplyResult _WrapCanApply(const UsdPrim &prim) { + std::string whyNot; + bool result = MjcPhysicsSiteAPI::CanApply(prim, &whyNot); + return MjcPhysicsSiteAPI_CanApplyResult(result, whyNot); +} + +} // anonymous namespace + +void wrapMjcPhysicsSiteAPI() { + typedef MjcPhysicsSiteAPI This; + + MjcPhysicsSiteAPI_CanApplyResult::Wrap( + "_CanApplyResult", "whyNot"); + + class_ > cls("SiteAPI"); + + cls.def(init(arg("prim"))) + .def(init(arg("schemaObj"))) + .def(TfTypePythonClass()) + + .def("Get", &This::Get, (arg("stage"), arg("path"))) + .staticmethod("Get") + + .def("CanApply", &_WrapCanApply, (arg("prim"))) + .staticmethod("CanApply") + + .def("Apply", &This::Apply, (arg("prim"))) + .staticmethod("Apply") + + .def("GetSchemaAttributeNames", &This::GetSchemaAttributeNames, + arg("includeInherited") = true, + return_value_policy()) + .staticmethod("GetSchemaAttributeNames") + + .def("_GetStaticTfType", (TfType const &(*)())TfType::Find, + return_value_policy()) + .staticmethod("_GetStaticTfType") + + .def(!self) + + .def("__repr__", ::_Repr); + + _CustomWrapCode(cls); +} + +// ===================================================================== // +// Feel free to add custom code below this line, it will be preserved by +// the code generator. The entry point for your custom code should look +// minimally like the following: +// +// WRAP_CUSTOM { +// _class +// .def("MyCustomMethod", ...) +// ; +// } +// +// Of course any other ancillary or support code may be provided. +// +// Just remember to wrap code in the appropriate delimiters: +// 'namespace {', '}'. +// +// ===================================================================== // +// --(BEGIN CUSTOM CODE)-- + +namespace { + +WRAP_CUSTOM {} + +} // namespace diff --git a/src/experimental/usd/mjcPhysics/wrapTokens.cpp b/src/experimental/usd/mjcPhysics/wrapTokens.cpp new file mode 100644 index 00000000..bd9d4b75 --- /dev/null +++ b/src/experimental/usd/mjcPhysics/wrapTokens.cpp @@ -0,0 +1,150 @@ +// Copyright 2025 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. + +// GENERATED FILE. DO NOT EDIT. +#include + +#include "./tokens.h" + +PXR_NAMESPACE_USING_DIRECTIVE + +namespace { + +// Helper to return a static token as a string. We wrap tokens as Python +// strings and for some reason simply wrapping the token using def_readonly +// bypasses to-Python conversion, leading to the error that there's no +// Python type for the C++ TfToken type. So we wrap this functor instead. +class _WrapStaticToken { + public: + _WrapStaticToken(const TfToken* token) : _token(token) {} + + std::string operator()() const { return _token->GetString(); } + + private: + const TfToken* _token; +}; + +template +void _AddToken(T& cls, const char* name, const TfToken& token) { + cls.add_static_property( + name, + boost::python::make_function( + _WrapStaticToken(&token), + boost::python::return_value_policy(), + boost::mpl::vector1())); +} + +} // namespace + +void wrapMjcPhysicsTokens() { + boost::python::class_ cls( + "Tokens", boost::python::no_init); + _AddToken(cls, "auto_", MjcPhysicsTokens->auto_); + _AddToken(cls, "cg", MjcPhysicsTokens->cg); + _AddToken(cls, "dense", MjcPhysicsTokens->dense); + _AddToken(cls, "elliptic", MjcPhysicsTokens->elliptic); + _AddToken(cls, "euler", MjcPhysicsTokens->euler); + _AddToken(cls, "implicit", MjcPhysicsTokens->implicit); + _AddToken(cls, "implicitfast", MjcPhysicsTokens->implicitfast); + _AddToken(cls, "mjcPhysicsActuatorgroupdisable", + MjcPhysicsTokens->mjcPhysicsActuatorgroupdisable); + _AddToken(cls, "mjcPhysicsApirate", MjcPhysicsTokens->mjcPhysicsApirate); + _AddToken(cls, "mjcPhysicsCcd_iterations", + MjcPhysicsTokens->mjcPhysicsCcd_iterations); + _AddToken(cls, "mjcPhysicsCcd_tolerance", + MjcPhysicsTokens->mjcPhysicsCcd_tolerance); + _AddToken(cls, "mjcPhysicsCone", MjcPhysicsTokens->mjcPhysicsCone); + _AddToken(cls, "mjcPhysicsDensity", MjcPhysicsTokens->mjcPhysicsDensity); + _AddToken(cls, "mjcPhysicsFlagActuation", + MjcPhysicsTokens->mjcPhysicsFlagActuation); + _AddToken(cls, "mjcPhysicsFlagAutoreset", + MjcPhysicsTokens->mjcPhysicsFlagAutoreset); + _AddToken(cls, "mjcPhysicsFlagClampctrl", + MjcPhysicsTokens->mjcPhysicsFlagClampctrl); + _AddToken(cls, "mjcPhysicsFlagConstraint", + MjcPhysicsTokens->mjcPhysicsFlagConstraint); + _AddToken(cls, "mjcPhysicsFlagContact", + MjcPhysicsTokens->mjcPhysicsFlagContact); + _AddToken(cls, "mjcPhysicsFlagEnergy", + MjcPhysicsTokens->mjcPhysicsFlagEnergy); + _AddToken(cls, "mjcPhysicsFlagEquality", + MjcPhysicsTokens->mjcPhysicsFlagEquality); + _AddToken(cls, "mjcPhysicsFlagEulerdamp", + MjcPhysicsTokens->mjcPhysicsFlagEulerdamp); + _AddToken(cls, "mjcPhysicsFlagFilterparent", + MjcPhysicsTokens->mjcPhysicsFlagFilterparent); + _AddToken(cls, "mjcPhysicsFlagFrictionloss", + MjcPhysicsTokens->mjcPhysicsFlagFrictionloss); + _AddToken(cls, "mjcPhysicsFlagFwdinv", + MjcPhysicsTokens->mjcPhysicsFlagFwdinv); + _AddToken(cls, "mjcPhysicsFlagGravity", + MjcPhysicsTokens->mjcPhysicsFlagGravity); + _AddToken(cls, "mjcPhysicsFlagInvdiscrete", + MjcPhysicsTokens->mjcPhysicsFlagInvdiscrete); + _AddToken(cls, "mjcPhysicsFlagIsland", + MjcPhysicsTokens->mjcPhysicsFlagIsland); + _AddToken(cls, "mjcPhysicsFlagLimit", MjcPhysicsTokens->mjcPhysicsFlagLimit); + _AddToken(cls, "mjcPhysicsFlagMidphase", + MjcPhysicsTokens->mjcPhysicsFlagMidphase); + _AddToken(cls, "mjcPhysicsFlagMulticcd", + MjcPhysicsTokens->mjcPhysicsFlagMulticcd); + _AddToken(cls, "mjcPhysicsFlagNativeccd", + MjcPhysicsTokens->mjcPhysicsFlagNativeccd); + _AddToken(cls, "mjcPhysicsFlagOverride", + MjcPhysicsTokens->mjcPhysicsFlagOverride); + _AddToken(cls, "mjcPhysicsFlagPassive", + MjcPhysicsTokens->mjcPhysicsFlagPassive); + _AddToken(cls, "mjcPhysicsFlagRefsafe", + MjcPhysicsTokens->mjcPhysicsFlagRefsafe); + _AddToken(cls, "mjcPhysicsFlagSensor", + MjcPhysicsTokens->mjcPhysicsFlagSensor); + _AddToken(cls, "mjcPhysicsFlagWarmstart", + MjcPhysicsTokens->mjcPhysicsFlagWarmstart); + _AddToken(cls, "mjcPhysicsImpratio", MjcPhysicsTokens->mjcPhysicsImpratio); + _AddToken(cls, "mjcPhysicsIntegrator", + MjcPhysicsTokens->mjcPhysicsIntegrator); + _AddToken(cls, "mjcPhysicsIterations", + MjcPhysicsTokens->mjcPhysicsIterations); + _AddToken(cls, "mjcPhysicsJacobian", MjcPhysicsTokens->mjcPhysicsJacobian); + _AddToken(cls, "mjcPhysicsLs_iterations", + MjcPhysicsTokens->mjcPhysicsLs_iterations); + _AddToken(cls, "mjcPhysicsLs_tolerance", + MjcPhysicsTokens->mjcPhysicsLs_tolerance); + _AddToken(cls, "mjcPhysicsMagnetic", MjcPhysicsTokens->mjcPhysicsMagnetic); + _AddToken(cls, "mjcPhysicsNoslip_iterations", + MjcPhysicsTokens->mjcPhysicsNoslip_iterations); + _AddToken(cls, "mjcPhysicsNoslip_tolerance", + MjcPhysicsTokens->mjcPhysicsNoslip_tolerance); + _AddToken(cls, "mjcPhysicsO_friction", + MjcPhysicsTokens->mjcPhysicsO_friction); + _AddToken(cls, "mjcPhysicsO_margin", MjcPhysicsTokens->mjcPhysicsO_margin); + _AddToken(cls, "mjcPhysicsO_solimp", MjcPhysicsTokens->mjcPhysicsO_solimp); + _AddToken(cls, "mjcPhysicsO_solref", MjcPhysicsTokens->mjcPhysicsO_solref); + _AddToken(cls, "mjcPhysicsSdf_initpoints", + MjcPhysicsTokens->mjcPhysicsSdf_initpoints); + _AddToken(cls, "mjcPhysicsSdf_iterations", + MjcPhysicsTokens->mjcPhysicsSdf_iterations); + _AddToken(cls, "mjcPhysicsSolver", MjcPhysicsTokens->mjcPhysicsSolver); + _AddToken(cls, "mjcPhysicsTimestep", MjcPhysicsTokens->mjcPhysicsTimestep); + _AddToken(cls, "mjcPhysicsTolerance", MjcPhysicsTokens->mjcPhysicsTolerance); + _AddToken(cls, "mjcPhysicsViscosity", MjcPhysicsTokens->mjcPhysicsViscosity); + _AddToken(cls, "mjcPhysicsWind", MjcPhysicsTokens->mjcPhysicsWind); + _AddToken(cls, "newton", MjcPhysicsTokens->newton); + _AddToken(cls, "pgs", MjcPhysicsTokens->pgs); + _AddToken(cls, "pyramidal", MjcPhysicsTokens->pyramidal); + _AddToken(cls, "rk4", MjcPhysicsTokens->rk4); + _AddToken(cls, "sparse", MjcPhysicsTokens->sparse); + _AddToken(cls, "MjcPhysicsSceneAPI", MjcPhysicsTokens->MjcPhysicsSceneAPI); + _AddToken(cls, "MjcSiteAPI", MjcPhysicsTokens->MjcSiteAPI); +} From d77126dc58860f60710ed2a78a80f4f81ec1a786 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Mon, 12 May 2025 09:12:44 -0700 Subject: [PATCH 114/191] Add `vertcollide` option for adding spheres at flex vertices. PiperOrigin-RevId: 757789990 Change-Id: I8ccba52b13e8c02a7be78f992297d6f414d98f8e --- doc/XMLreference.rst | 12 +- doc/XMLschema.rst | 4 +- doc/includes/references.h | 3 +- include/mujoco/mjspec.h | 3 +- .../plugin/elasticity/poncho_vertcollide.xml | 1432 +++++++++++++++++ python/mujoco/introspect/structs.py | 7 +- src/user/user_flexcomp.cc | 16 + src/xml/xml_native_reader.cc | 14 +- 8 files changed, 1480 insertions(+), 11 deletions(-) create mode 100644 model/plugin/elasticity/poncho_vertcollide.xml diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 647a731b..efbecc2f 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -3554,6 +3554,7 @@ saving the XML: .. _flexcomp-contact-internal: .. _flexcomp-contact-selfcollide: +.. _flexcomp-contact-vertcollide: .. _flexcomp-contact-activelayers: .. _flexcomp-contact-contype: .. _flexcomp-contact-conaffinity: @@ -3567,8 +3568,8 @@ saving the XML: .. _flexcomp-contact-gap: .. |body/flexcomp/contact attrib list| replace:: - :at:`internal`, :at:`selfcollide`, :at:`activelayers`, :at:`contype`, :at:`conaffinity`, :at:`condim`, - :at:`priority`, :at:`friction`, :at:`solmix`, :at:`solimp`, :at:`margin`, :at:`gap` + :at:`internal`, :at:`selfcollide`, :at:`vertcollide`, :at:`activelayers`, :at:`contype`, :at:`conaffinity`, + :at:`condim`, :at:`priority`, :at:`friction`, :at:`solmix`, :at:`solimp`, :at:`margin`, :at:`gap` |body/flexcomp/contact attrib list| Same as in :ref:`flex/contact`. All attributes are passed through to the automatically-generated flex. @@ -4086,6 +4087,13 @@ extensions specific to flexes. **sap** in 1D and 2D, and **bvh** in 3D. Which strategy performs better depends on the specifics of the model. The automatic setting is just a simple rule which we have found to perform well in general. +.. _flex-contact-vertcollide: + +:at:`vertcollide`: :at-val:`[true, false], "false"` + Enables or disables vertex collisions. if **true**, spherical geoms are added at the vertices of flex, with radius + equal to the radius of the flex. These geoms can collide with other geoms and are not visible by default. If + **false**, no additional geoms are added. + .. _flex-contact-activelayers: :at:`activelayers`: :at-val:`int(1), "1"` diff --git a/doc/XMLschema.rst b/doc/XMLschema.rst index 9036d1c0..5b32d4bb 100644 --- a/doc/XMLschema.rst +++ b/doc/XMLschema.rst @@ -452,7 +452,7 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | | | | | :ref:`margin` | :ref:`gap` | :ref:`internal` | :ref:`selfcollide` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`activelayers` | | | | | +| | | | :ref:`activelayers` | :ref:`vertcollide` | | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_2| flexcomp |br| |_2| |L| | | .. table:: | @@ -502,7 +502,7 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | | | | | :ref:`margin` | :ref:`gap` | :ref:`internal` | :ref:`selfcollide` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`activelayers` | | | | | +| | | | :ref:`activelayers` | :ref:`vertcollide` | | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_2| flex |br| |_2| |L| | | .. table:: | diff --git a/doc/includes/references.h b/doc/includes/references.h index 0689e150..d8071b2c 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -2049,7 +2049,8 @@ typedef struct mjsFlex_ { // flex specification double radius; // radius around primitive element mjtByte internal; // enable internal collisions mjtByte flatskin; // render flex skin with flat shading - int selfcollide; // mode for flex self colllision + int selfcollide; // mode for flex self collision + int vertcollide; // mode for vertex collision int activelayers; // number of active element layers in 3D int group; // group for visualizatioh double edgestiffness; // edge stiffness diff --git a/include/mujoco/mjspec.h b/include/mujoco/mjspec.h index 3202d9dc..fb95afad 100644 --- a/include/mujoco/mjspec.h +++ b/include/mujoco/mjspec.h @@ -430,7 +430,8 @@ typedef struct mjsFlex_ { // flex specification double radius; // radius around primitive element mjtByte internal; // enable internal collisions mjtByte flatskin; // render flex skin with flat shading - int selfcollide; // mode for flex self colllision + int selfcollide; // mode for flex self collision + int vertcollide; // mode for vertex collision int activelayers; // number of active element layers in 3D int group; // group for visualizatioh double edgestiffness; // edge stiffness diff --git a/model/plugin/elasticity/poncho_vertcollide.xml b/model/plugin/elasticity/poncho_vertcollide.xml new file mode 100644 index 00000000..895df038 --- /dev/null +++ b/model/plugin/elasticity/poncho_vertcollide.xml @@ -0,0 +1,1432 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index c7c3f4d1..e3ec1bc8 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -10722,7 +10722,12 @@ STRUCTS: Mapping[str, StructDecl] = dict([ StructFieldDecl( name='selfcollide', type=ValueType(name='int'), - doc='mode for flex self colllision', + doc='mode for flex self collision', + ), + StructFieldDecl( + name='vertcollide', + type=ValueType(name='int'), + doc='mode for vertex collision', ), StructFieldDecl( name='activelayers', diff --git a/src/user/user_flexcomp.cc b/src/user/user_flexcomp.cc index 1d0dbece..cb7124c1 100644 --- a/src/user/user_flexcomp.cc +++ b/src/user/user_flexcomp.cc @@ -33,6 +33,7 @@ #include "engine/engine_util_errmem.h" #include "user/user_flexcomp.h" #include +#include "user/user_api.h" #include "user/user_model.h" #include "user/user_objects.h" #include "user/user_resource.h" @@ -459,6 +460,14 @@ bool mjCFlexcomp::Make(mjsBody* body, char* error, int error_sz) { // add new body at vertex coordinates mjsBody* pb = mjs_addBody(body, 0); + // add geom if vertcollide + if (dflex->vertcollide) { + mjsGeom* geom = mjs_addGeom(pb, 0); + geom->type = mjGEOM_SPHERE; + geom->size[0] = dflex->radius; + geom->group = 4; + } + // set frame and inertial pb->pos[0] = point[3*i]; pb->pos[1] = point[3*i+1]; @@ -544,6 +553,13 @@ bool mjCFlexcomp::Make(mjsBody* body, char* error, int error_sz) { pb->inertia[2] = pb->mass*(2.0*inertiabox*inertiabox)/3.0; pb->explicitinertial = true; + // add geom if vertcollide + if (dflex->vertcollide) { + mjsGeom* geom = mjs_addGeom(pb, 0); + geom->type = mjGEOM_SPHERE; + geom->size[0] = dflex->radius; + } + for (int d=0; d < 3; d++) { mjsJoint* jnt = mjs_addJoint(pb, 0); jnt->type = mjJNT_SLIDE; diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index a5bf6824..8404364a 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -311,9 +311,9 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { {"<"}, {"edge", "?", "5", "equality", "solref", "solimp", "stiffness", "damping"}, {"elasticity", "?", "4", "young", "poisson", "damping", "thickness"}, - {"contact", "?", "13", "contype", "conaffinity", "condim", "priority", + {"contact", "?", "14", "contype", "conaffinity", "condim", "priority", "friction", "solmix", "solref", "solimp", "margin", "gap", - "internal", "selfcollide", "activelayers"}, + "internal", "selfcollide", "activelayers", "vertcollide"}, {"pin", "*", "4", "id", "range", "grid", "gridrange"}, {"plugin", "*", "2", "plugin", "instance"}, {"<"}, @@ -327,9 +327,9 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { {"flex", "*", "13", "name", "group", "dim", "radius", "material", "rgba", "flatskin", "body", "vertex", "element", "texcoord", "elemtexcoord", "node"}, {"<"}, - {"contact", "?", "13", "contype", "conaffinity", "condim", "priority", + {"contact", "?", "14", "contype", "conaffinity", "condim", "priority", "friction", "solmix", "solref", "solimp", "margin", "gap", - "internal", "selfcollide", "activelayers"}, + "internal", "selfcollide", "activelayers", "vertcollide"}, {"edge", "?", "2", "stiffness", "damping"}, {"elasticity", "?", "4", "young", "poisson", "damping", "thickness"}, {">"}, @@ -1380,6 +1380,9 @@ void mjXReader::OneFlex(XMLElement* elem, mjsFlex* flex) { flex->internal = (n == 1); } MapValue(cont, "selfcollide", &flex->selfcollide, flexself_map, 5); + if (MapValue(cont, "vertcollide", &flex->vertcollide, bool_map, 2)) { + flex->vertcollide = (n == 1); + } ReadAttrInt(cont, "activelayers", &flex->activelayers); } @@ -2682,6 +2685,9 @@ void mjXReader::OneFlexcomp(XMLElement* elem, mjsBody* body, const mjVFS* vfs) { dflex.internal = (n == 1); } MapValue(cont, "selfcollide", &dflex.selfcollide, flexself_map, 5); + if (MapValue(cont, "vertcollide", &n, bool_map, 2)) { + dflex.vertcollide = (n == 1); + } ReadAttrInt(cont, "activelayers", &dflex.activelayers); } From 8a7c42c747bfcf9160e3125048219566b41ff8cf Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Mon, 12 May 2025 10:52:41 -0700 Subject: [PATCH 115/191] Add SDF functions to public API. PiperOrigin-RevId: 757829339 Change-Id: I81ee7c3f33acf234835bf34cbb632e23cb9e284e --- doc/APIreference/APItypes.rst | 20 ++++++ doc/includes/references.h | 19 ++++++ include/mujoco/mjmodel.h | 8 +++ include/mujoco/mjplugin.h | 10 +++ include/mujoco/mujoco.h | 13 ++++ python/mujoco/introspect/enums.py | 11 ++++ python/mujoco/introspect/functions.py | 93 +++++++++++++++++++++++++++ python/mujoco/introspect/structs.py | 49 ++++++++++++++ src/engine/engine_collision_sdf.h | 19 +----- unity/Runtime/Bindings/MjBindings.cs | 6 ++ 10 files changed, 232 insertions(+), 16 deletions(-) diff --git a/doc/APIreference/APItypes.rst b/doc/APIreference/APItypes.rst index 7e2befa0..205b7762 100644 --- a/doc/APIreference/APItypes.rst +++ b/doc/APIreference/APItypes.rst @@ -357,6 +357,26 @@ last argument to :ref:`mj_local2global`. .. mujoco-include:: mjtSameFrame +.. _mjtFlexSelf: + +mjtFlexSelf +~~~~~~~~~~~~ + +Types of flex self-collisions midphase. + +.. mujoco-include:: mjtFlexSelf + + +.. _mjtSDFType: + +mjtSDFType +~~~~~~~~~~~ + +Formulas used to combine SDFs when calling mjc_distance and mjc_gradient. + +.. mujoco-include:: mjtSDFType + + .. _tyDataEnums: Data diff --git a/doc/includes/references.h b/doc/includes/references.h index d8071b2c..363906a0 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -766,6 +766,12 @@ typedef enum mjtFlexSelf_ { // mode for flex selfcollide mjFLEXSELF_SAP, // use SAP in midphase mjFLEXSELF_AUTO // choose between BVH and SAP automatically } mjtFlexSelf; +typedef enum mjtSDFType_ { // signed distance function (SDF) type + mjSDFTYPE_SINGLE = 0, // single SDF + mjSDFTYPE_INTERSECTION, // max(A, B) + mjSDFTYPE_MIDSURFACE, // A - B + mjSDFTYPE_COLLISION, // A + B + abs(max(A, B)) +} mjtSDFType; struct mjLROpt_ { // options for mj_setLengthRange() // flags int mode; // which actuators to process (mjtLRMode) @@ -1586,6 +1592,15 @@ struct mjpPlugin_ { void (*sdf_aabb)(mjtNum aabb[6], const mjtNum* attributes); }; typedef struct mjpPlugin_ mjpPlugin; +struct mjSDF_ { + const mjpPlugin** plugin; + int* id; + mjtSDFType type; + mjtNum* relpos; + mjtNum* relmat; + mjtGeom* geomtype; +}; +typedef struct mjSDF_ mjSDF; typedef enum mjtGridPos_ { // grid position for overlay mjGRID_TOPLEFT = 0, // top left mjGRID_TOPRIGHT, // top right @@ -3637,6 +3652,10 @@ void mju_insertionSortInt(int* list, int n); mjtNum mju_Halton(int index, int base); char* mju_strncpy(char *dst, const char *src, int n); mjtNum mju_sigmoid(mjtNum x); +const mjpPlugin* mjc_getSDF(const mjModel* m, int id); +mjtNum mjc_distance(const mjModel* m, const mjData* d, const mjSDF* s, const mjtNum x[3]); +void mjc_gradient(const mjModel* m, const mjData* d, const mjSDF* s, mjtNum gradient[3], + const mjtNum x[3]); void mjd_transitionFD(const mjModel* m, mjData* d, mjtNum eps, mjtByte flg_centered, mjtNum* A, mjtNum* B, mjtNum* C, mjtNum* D); void mjd_inverseFD(const mjModel* m, mjData* d, mjtNum eps, mjtByte flg_actuation, diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index b6bdbfc6..ceaa76d1 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -407,6 +407,14 @@ typedef enum mjtFlexSelf_ { // mode for flex selfcollide } mjtFlexSelf; +typedef enum mjtSDFType_ { // signed distance function (SDF) type + mjSDFTYPE_SINGLE = 0, // single SDF + mjSDFTYPE_INTERSECTION, // max(A, B) + mjSDFTYPE_MIDSURFACE, // A - B + mjSDFTYPE_COLLISION, // A + B + abs(max(A, B)) +} mjtSDFType; + + //---------------------------------- mjLROpt ------------------------------------------------------- struct mjLROpt_ { // options for mj_setLengthRange() diff --git a/include/mujoco/mjplugin.h b/include/mujoco/mjplugin.h index 0fc31a6c..44f2af20 100644 --- a/include/mujoco/mjplugin.h +++ b/include/mujoco/mjplugin.h @@ -135,6 +135,16 @@ struct mjpPlugin_ { }; typedef struct mjpPlugin_ mjpPlugin; +struct mjSDF_ { + const mjpPlugin** plugin; + int* id; + mjtSDFType type; + mjtNum* relpos; + mjtNum* relmat; + mjtGeom* geomtype; +}; +typedef struct mjSDF_ mjSDF; + #if defined(__has_attribute) #if __has_attribute(constructor) diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index b955cb13..784d1ab8 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -1308,6 +1308,19 @@ MJAPI char* mju_strncpy(char *dst, const char *src, int n); MJAPI mjtNum mju_sigmoid(mjtNum x); +//---------------------------------- Signed Distance Function -------------------------------------- + +// get sdf from geom id +MJAPI const mjpPlugin* mjc_getSDF(const mjModel* m, int id); + +// signed distance function +MJAPI mjtNum mjc_distance(const mjModel* m, const mjData* d, const mjSDF* s, const mjtNum x[3]); + +// gradient of sdf +MJAPI void mjc_gradient(const mjModel* m, const mjData* d, const mjSDF* s, mjtNum gradient[3], + const mjtNum x[3]); + + //---------------------------------- Derivatives --------------------------------------------------- // Finite differenced transition matrices (control theory notation) diff --git a/python/mujoco/introspect/enums.py b/python/mujoco/introspect/enums.py index dd1a1fad..a1cab44f 100644 --- a/python/mujoco/introspect/enums.py +++ b/python/mujoco/introspect/enums.py @@ -428,6 +428,17 @@ ENUMS: Mapping[str, EnumDecl] = dict([ ('mjFLEXSELF_AUTO', 4), ]), )), + ('mjtSDFType', + EnumDecl( + name='mjtSDFType', + declname='enum mjtSDFType_', + values=dict([ + ('mjSDFTYPE_SINGLE', 0), + ('mjSDFTYPE_INTERSECTION', 1), + ('mjSDFTYPE_MIDSURFACE', 2), + ('mjSDFTYPE_COLLISION', 3), + ]), + )), ('mjtTaskStatus', EnumDecl( name='mjtTaskStatus', diff --git a/python/mujoco/introspect/functions.py b/python/mujoco/introspect/functions.py index f62a0e7a..ef8bcd58 100644 --- a/python/mujoco/introspect/functions.py +++ b/python/mujoco/introspect/functions.py @@ -8570,6 +8570,99 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), doc='Sigmoid function over 0<=x<=1 using quintic polynomial.', )), + ('mjc_getSDF', + FunctionDecl( + name='mjc_getSDF', + return_type=PointerType( + inner_type=ValueType(name='mjpPlugin', is_const=True), + ), + parameters=( + FunctionParameterDecl( + name='m', + type=PointerType( + inner_type=ValueType(name='mjModel', is_const=True), + ), + ), + FunctionParameterDecl( + name='id', + type=ValueType(name='int'), + ), + ), + doc='get sdf from geom id', + )), + ('mjc_distance', + FunctionDecl( + name='mjc_distance', + return_type=ValueType(name='mjtNum'), + parameters=( + FunctionParameterDecl( + name='m', + type=PointerType( + inner_type=ValueType(name='mjModel', is_const=True), + ), + ), + FunctionParameterDecl( + name='d', + type=PointerType( + inner_type=ValueType(name='mjData', is_const=True), + ), + ), + FunctionParameterDecl( + name='s', + type=PointerType( + inner_type=ValueType(name='mjSDF', is_const=True), + ), + ), + FunctionParameterDecl( + name='x', + type=ArrayType( + inner_type=ValueType(name='mjtNum', is_const=True), + extents=(3,), + ), + ), + ), + doc='signed distance function', + )), + ('mjc_gradient', + FunctionDecl( + name='mjc_gradient', + return_type=ValueType(name='void'), + parameters=( + FunctionParameterDecl( + name='m', + type=PointerType( + inner_type=ValueType(name='mjModel', is_const=True), + ), + ), + FunctionParameterDecl( + name='d', + type=PointerType( + inner_type=ValueType(name='mjData', is_const=True), + ), + ), + FunctionParameterDecl( + name='s', + type=PointerType( + inner_type=ValueType(name='mjSDF', is_const=True), + ), + ), + FunctionParameterDecl( + name='gradient', + type=ArrayType( + inner_type=ValueType(name='mjtNum'), + extents=(3,), + ), + ), + FunctionParameterDecl( + name='x', + type=ArrayType( + inner_type=ValueType(name='mjtNum', is_const=True), + extents=(3,), + ), + ), + ), + doc='gradient of sdf', + )), ('mjd_transitionFD', FunctionDecl( name='mjd_transitionFD', diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index e3ec1bc8..5ccdc9b8 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -8966,6 +8966,55 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), ), )), + ('mjSDF', + StructDecl( + name='mjSDF', + declname='struct mjSDF_', + fields=( + StructFieldDecl( + name='plugin', + type=PointerType( + inner_type=PointerType( + inner_type=ValueType(name='mjpPlugin', is_const=True), + ), + ), + doc='', + ), + StructFieldDecl( + name='id', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='', + ), + StructFieldDecl( + name='type', + type=ValueType(name='mjtSDFType'), + doc='', + ), + StructFieldDecl( + name='relpos', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='', + ), + StructFieldDecl( + name='relmat', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='', + ), + StructFieldDecl( + name='geomtype', + type=PointerType( + inner_type=ValueType(name='mjtGeom'), + ), + doc='', + ), + ), + )), ('mjrRect', StructDecl( name='mjrRect', diff --git a/src/engine/engine_collision_sdf.h b/src/engine/engine_collision_sdf.h index c6acbe1c..c0ea7cdb 100644 --- a/src/engine/engine_collision_sdf.h +++ b/src/engine/engine_collision_sdf.h @@ -19,27 +19,14 @@ #include #include #include +#include #ifdef __cplusplus extern "C" { #endif -typedef enum mjtSDFType_ { // signed distance function (SDF) type - mjSDFTYPE_SINGLE = 0, // single SDF - mjSDFTYPE_INTERSECTION, // max(A, B) - mjSDFTYPE_MIDSURFACE, // A - B - mjSDFTYPE_COLLISION, // A + B + abs(max(A, B)) -} mjtSDFType; - -struct mjSDF_ { - const mjpPlugin** plugin; - int* id; - mjtSDFType type; - mjtNum* relpos; - mjtNum* relmat; - mjtGeom* geomtype; -}; -typedef struct mjSDF_ mjSDF; +// get sdf from geom id +MJAPI const mjpPlugin* mjc_getSDF(const mjModel* m, int id); // signed distance function MJAPI mjtNum mjc_distance(const mjModel* m, const mjData* d, const mjSDF* s, const mjtNum x[3]); diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index b7137723..c1042de6 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -419,6 +419,12 @@ public enum mjtFlexSelf : int{ mjFLEXSELF_SAP = 3, mjFLEXSELF_AUTO = 4, } +public enum mjtSDFType : int{ + mjSDFTYPE_SINGLE = 0, + mjSDFTYPE_INTERSECTION = 1, + mjSDFTYPE_MIDSURFACE = 2, + mjSDFTYPE_COLLISION = 3, +} public enum mjtPluginCapabilityBit : int{ mjPLUGIN_ACTUATOR = 1, mjPLUGIN_SENSOR = 2, From 755564a3484447cdf03d79030f96b1f3460cf515 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 12 May 2025 15:38:15 -0700 Subject: [PATCH 116/191] Remove unused code related to legacy island implementation. Also fix a docstring. PiperOrigin-RevId: 757939564 Change-Id: I3a970fd38c63886d34cd23b1fb45617a9ce5c147 --- doc/includes/references.h | 2 +- include/mujoco/mjdata.h | 2 +- python/mujoco/introspect/structs.py | 2 +- src/engine/engine_core_constraint.c | 4 +- src/engine/engine_core_constraint.h | 4 +- src/engine/engine_core_smooth.c | 4 +- src/engine/engine_solver.c | 3 +- src/engine/engine_support.c | 2 +- src/engine/engine_util_solve.c | 6 +- src/engine/engine_util_sparse.c | 14 ++--- src/engine/engine_util_sparse.h | 43 +++++--------- src/engine/engine_util_sparse_avx.h | 65 ++++++---------------- test/engine/engine_core_constraint_test.cc | 5 +- test/engine/engine_util_sparse_test.cc | 49 +++++----------- 14 files changed, 67 insertions(+), 138 deletions(-) diff --git a/doc/includes/references.h b/doc/includes/references.h index 363906a0..402043aa 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -388,7 +388,7 @@ struct mjData_ { int* island_idofadr; // island start address in idof vector (nisland x 1) int* island_dofadr; // island start address in dof vector (nisland x 1) int* map_dof2idof; // map from dof to idof (nv x 1) - int* map_idof2dof; // map from idof to dof; idof >= ni: unconstrained (nv x 1) + int* map_idof2dof; // map from idof to dof; >= nidof: unconstrained (nv x 1) // computed by mj_island (dofs sorted by island) mjtNum* ifrc_smooth; // net unconstrained force (nidof x 1) diff --git a/include/mujoco/mjdata.h b/include/mujoco/mjdata.h index 839b3aed..963630cc 100644 --- a/include/mujoco/mjdata.h +++ b/include/mujoco/mjdata.h @@ -416,7 +416,7 @@ struct mjData_ { int* island_idofadr; // island start address in idof vector (nisland x 1) int* island_dofadr; // island start address in dof vector (nisland x 1) int* map_dof2idof; // map from dof to idof (nv x 1) - int* map_idof2dof; // map from idof to dof; idof >= ni: unconstrained (nv x 1) + int* map_idof2dof; // map from idof to dof; >= nidof: unconstrained (nv x 1) // computed by mj_island (dofs sorted by island) mjtNum* ifrc_smooth; // net unconstrained force (nidof x 1) diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index 5ccdc9b8..b4e48f39 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -5981,7 +5981,7 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=PointerType( inner_type=ValueType(name='int'), ), - doc='map from idof to dof; idof >= ni: unconstrained', + doc='map from idof to dof; >= nidof: unconstrained', array_extent=('nv',), ), StructFieldDecl( diff --git a/src/engine/engine_core_constraint.c b/src/engine/engine_core_constraint.c index 391ed4d2..dba314d9 100644 --- a/src/engine/engine_core_constraint.c +++ b/src/engine/engine_core_constraint.c @@ -2286,7 +2286,7 @@ void mj_referenceConstraint(const mjModel* m, mjData* d) { //---------------------------- update constraint state --------------------------------------------- // compute efc_state, efc_force -// optional: cost(qacc) = shat(jar); cone Hessians +// optional: cost(qacc) = s_hat(jar); cone Hessians void mj_constraintUpdate_impl(int ne, int nf, int nefc, const mjtNum* D, const mjtNum* R, const mjtNum* floss, const mjtNum* jar, const int* type, const int* id, @@ -2485,7 +2485,7 @@ void mj_constraintUpdate_impl(int ne, int nf, int nefc, // compute efc_state, efc_force, qfrc_constraint -// optional: cost(qacc) = shat(jar) where jar = Jac*qacc-aref; cone Hessians +// optional: cost(qacc) = s_hat(jar) where jar = Jac*qacc-aref; cone Hessians void mj_constraintUpdate(const mjModel* m, mjData* d, const mjtNum* jar, mjtNum cost[1], int flg_coneHessian) { mj_constraintUpdate_impl(d->ne, d->nf, d->nefc, d->efc_D, d->efc_R, d->efc_frictionloss, diff --git a/src/engine/engine_core_constraint.h b/src/engine/engine_core_constraint.h index f752b143..a0a7c6ca 100644 --- a/src/engine/engine_core_constraint.h +++ b/src/engine/engine_core_constraint.h @@ -97,7 +97,7 @@ MJAPI void mj_projectConstraint(const mjModel* m, mjData* d); MJAPI void mj_referenceConstraint(const mjModel* m, mjData* d); // compute efc_state, efc_force -// optional: cost(qacc) = shat(jar); cone Hessians +// optional: cost(qacc) = s_hat(jar); cone Hessians MJAPI void mj_constraintUpdate_impl(int ne, int nf, int nefc, const mjtNum* D, const mjtNum* R, const mjtNum* floss, const mjtNum* jar, const int* type, const int* id, @@ -105,7 +105,7 @@ MJAPI void mj_constraintUpdate_impl(int ne, int nf, int nefc, int flg_coneHessian); // compute efc_state, efc_force, qfrc_constraint -// optional: cost(qacc) = shat(jar) where jar = Jac*qacc-aref; cone Hessians +// optional: cost(qacc) = s_hat(jar) where jar = Jac*qacc-aref; cone Hessians MJAPI void mj_constraintUpdate(const mjModel* m, mjData* d, const mjtNum* jar, mjtNum cost[1], int flg_coneHessian); diff --git a/src/engine/engine_core_smooth.c b/src/engine/engine_core_smooth.c index b2bdc8b5..112f62bd 100644 --- a/src/engine/engine_core_smooth.c +++ b/src/engine/engine_core_smooth.c @@ -1875,13 +1875,13 @@ void mj_solveLD(mjtNum* restrict x, const mjtNum* qLD, const mjtNum* qLDiagInv, // one vector if (n == 1) { - x[i] -= mju_dotSparse(qLD+adr, x, d, colind+adr, /*flg_unc1=*/0); + x[i] -= mju_dotSparse(qLD+adr, x, d, colind+adr); } // multiple vectors else { for (int offset=0; offset < n*nv; offset+=nv) { - x[i+offset] -= mju_dotSparse(qLD+adr, x+offset, d, colind+adr, /*flg_unc1=*/0); + x[i+offset] -= mju_dotSparse(qLD+adr, x+offset, d, colind+adr); } } } diff --git a/src/engine/engine_solver.c b/src/engine/engine_solver.c index fb9dedd1..354386bb 100644 --- a/src/engine/engine_solver.c +++ b/src/engine/engine_solver.c @@ -187,8 +187,7 @@ static void residual(const mjModel* m, const mjData* d, mjtNum* res, int i, int res[j] = d->efc_b[i+j] + mju_dotSparse(d->efc_AR + d->efc_AR_rowadr[i+j], d->efc_force, d->efc_AR_rownnz[i+j], - d->efc_AR_colind + d->efc_AR_rowadr[i+j], - /*flg_unc1=*/0); + d->efc_AR_colind + d->efc_AR_rowadr[i+j]); } } diff --git a/src/engine/engine_support.c b/src/engine/engine_support.c index 3d95da43..b7036659 100644 --- a/src/engine/engine_support.c +++ b/src/engine/engine_support.c @@ -1048,7 +1048,7 @@ void mj_mulM2(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec) // non-simple: add off-diagonals if (!m->dof_simplenum[i]) { int adr = d->M_rowadr[i]; - res[i] += mju_dotSparse(qLD+adr, vec, d->M_rownnz[i] - 1, d->M_colind+adr, /*flg_unc1=*/0); + res[i] += mju_dotSparse(qLD+adr, vec, d->M_rownnz[i] - 1, d->M_colind+adr); } } diff --git a/src/engine/engine_util_solve.c b/src/engine/engine_util_solve.c index 11889667..4d9bde30 100644 --- a/src/engine/engine_util_solve.c +++ b/src/engine/engine_util_solve.c @@ -275,7 +275,7 @@ void mju_cholSolveSparse(mjtNum* res, const mjtNum* mat, const mjtNum* vec, int // x(i) -= sum_j L(i,j)*x(j), j=0:i-1 if (nnz > 1) { - res[i] -= mju_dotSparse(mat+adr, res, nnz-1, colind+adr, /*flg_unc1=*/0); + res[i] -= mju_dotSparse(mat+adr, res, nnz-1, colind+adr); // modulo AVX, the above line does // for (int j=0; j 0) { int adr = rowadr[i] + d1; - res[i] -= mju_dotSparse(LU+adr, res, nnz, colind+adr, /*flg_unc1=*/0); + res[i] -= mju_dotSparse(LU+adr, res, nnz, colind+adr); } } @@ -720,7 +720,7 @@ void mju_solveLUSparse(mjtNum* res, const mjtNum* LU, const mjtNum* vec, int n, int d = diag[i]; int adr = rowadr[i]; if (d > 0) { - res[i] -= mju_dotSparse(LU+adr, res, d, colind+adr, /*flg_unc1=*/0); + res[i] -= mju_dotSparse(LU+adr, res, d, colind+adr); } // divide by diagonal element of L diff --git a/src/engine/engine_util_sparse.c b/src/engine/engine_util_sparse.c index 00fb99d9..250020e4 100644 --- a/src/engine/engine_util_sparse.c +++ b/src/engine/engine_util_sparse.c @@ -60,9 +60,8 @@ void mju_dotSparseX3(mjtNum* res0, mjtNum* res1, mjtNum* res2, // dot-product, both vectors are sparse -// flg_unc2: is vec2 memory layout uncompressed -mjtNum mju_dotSparse2(const mjtNum* vec1, const mjtNum* vec2, int nnz1, const int* ind1, int nnz2, - const int* ind2, int flg_unc2) { +mjtNum mju_dotSparse2(const mjtNum* vec1, const int* ind1, int nnz1, + const mjtNum* vec2, const int* ind2, int nnz2) { int i1 = 0, i2 = 0; mjtNum res = 0; @@ -77,12 +76,7 @@ mjtNum mju_dotSparse2(const mjtNum* vec1, const mjtNum* vec2, int nnz1, const in // match: accumulate result, advance both if (adr1 == adr2) { - if (flg_unc2) { - res += vec1[i1++] * vec2[adr2]; - i2++; - } else { - res += vec1[i1++] * vec2[i2++]; - } + res += vec1[i1++] * vec2[i2++]; } // otherwise advance smaller @@ -161,7 +155,7 @@ void mju_mulMatVecSparse(mjtNum* res, const mjtNum* mat, const mjtNum* vec, #else // regular sparse dot-product for (int r=0; r < nr; r++) { - res[r] = mju_dotSparse(mat+rowadr[r], vec, rownnz[r], colind+rowadr[r], /*flg_unc1=*/0); + res[r] = mju_dotSparse(mat+rowadr[r], vec, rownnz[r], colind+rowadr[r]); } #endif // mjUSEAVX } diff --git a/src/engine/engine_util_sparse.h b/src/engine/engine_util_sparse.h index 1246abc1..ea26014b 100644 --- a/src/engine/engine_util_sparse.h +++ b/src/engine/engine_util_sparse.h @@ -28,9 +28,9 @@ extern "C" { //------------------------------ sparse operations ------------------------------------------------- -// dot-product, both vectors are sparse, vec2 can be uncompressed -MJAPI mjtNum mju_dotSparse2(const mjtNum* vec1, const mjtNum* vec2, int nnz1, const int* ind1, - int nnz2, const int* ind2, int flg_unc2); +// dot-product, both vectors are sparse +MJAPI mjtNum mju_dotSparse2(const mjtNum* vec1, const int* ind1, int nnz1, + const mjtNum* vec2, const int* ind2, int nnz2); // convert matrix from dense to sparse // nnz is size of res and colind, return 1 if too small, 0 otherwise @@ -128,12 +128,10 @@ MJAPI void mju_blockDiagSparse( // ------------------------------ inlined functions ------------------------------------------------ // dot-product, first vector is sparse -// flg_unc1: is vec1 memory layout uncompressed static inline -mjtNum mju_dotSparse(const mjtNum* vec1, const mjtNum* vec2, int nnz1, const int* ind1, - int flg_unc1) { +mjtNum mju_dotSparse(const mjtNum* vec1, const mjtNum* vec2, int nnz1, const int* ind1) { #ifdef mjUSEAVX - return mju_dotSparse_avx(vec1, vec2, nnz1, ind1, flg_unc1); + return mju_dotSparse_avx(vec1, vec2, nnz1, ind1); #else int i = 0; mjtNum res = 0; @@ -143,33 +141,18 @@ mjtNum mju_dotSparse(const mjtNum* vec1, const mjtNum* vec2, int nnz1, const int mjtNum res2 = 0; mjtNum res3 = 0; - - if (flg_unc1) { - for (; i <= n_4; i+=4) { - res0 += vec1[ind1[i+0]] * vec2[ind1[i+0]]; - res1 += vec1[ind1[i+1]] * vec2[ind1[i+1]]; - res2 += vec1[ind1[i+2]] * vec2[ind1[i+2]]; - res3 += vec1[ind1[i+3]] * vec2[ind1[i+3]]; - } - } else { - for (; i <= n_4; i+=4) { - res0 += vec1[i+0] * vec2[ind1[i+0]]; - res1 += vec1[i+1] * vec2[ind1[i+1]]; - res2 += vec1[i+2] * vec2[ind1[i+2]]; - res3 += vec1[i+3] * vec2[ind1[i+3]]; - } + for (; i <= n_4; i+=4) { + res0 += vec1[i+0] * vec2[ind1[i+0]]; + res1 += vec1[i+1] * vec2[ind1[i+1]]; + res2 += vec1[i+2] * vec2[ind1[i+2]]; + res3 += vec1[i+3] * vec2[ind1[i+3]]; } + res = (res0 + res2) + (res1 + res3); // scalar part - if (flg_unc1) { - for (; i < nnz1; i++) { - res += vec1[ind1[i]] * vec2[ind1[i]]; - } - } else { - for (; i < nnz1; i++) { - res += vec1[i] * vec2[ind1[i]]; - } + for (; i < nnz1; i++) { + res += vec1[i] * vec2[ind1[i]]; } return res; diff --git a/src/engine/engine_util_sparse_avx.h b/src/engine/engine_util_sparse_avx.h index 708db7c7..bdc9e931 100644 --- a/src/engine/engine_util_sparse_avx.h +++ b/src/engine/engine_util_sparse_avx.h @@ -30,10 +30,8 @@ //------------------------------ sparse operations using avx --------------------------------------- // dot-product, first vector is sparse -// flg_unc1: is vec1 memory layout uncompressed static inline -mjtNum mju_dotSparse_avx(const mjtNum* vec1, const mjtNum* vec2, int nnz1, const int* ind1, - int flg_unc1) { +mjtNum mju_dotSparse_avx(const mjtNum* vec1, const mjtNum* vec2, int nnz1, const int* ind1) { int i = 0; mjtNum res = 0; int nnz1_4 = nnz1 - 4; @@ -48,43 +46,22 @@ mjtNum mju_dotSparse_avx(const mjtNum* vec1, const mjtNum* vec2, int nnz1, const vec2[ind1[2]], vec2[ind1[1]], vec2[ind1[0]]); - if (flg_unc1) { - val1 = _mm256_set_pd(vec1[ind1[3]], - vec1[ind1[2]], - vec1[ind1[1]], - vec1[ind1[0]]); - } else { - val1 = _mm256_loadu_pd(vec1); - } + + val1 = _mm256_loadu_pd(vec1); + sum = _mm256_mul_pd(val1, val2); i = 4; // parallel computation - if (flg_unc1) { - while (i<=nnz1_4) { - val1 = _mm256_set_pd(vec1[ind1[i+3]], - vec1[ind1[i+2]], - vec1[ind1[i+1]], - vec1[ind1[i+0]]); - val2 = _mm256_set_pd(vec2[ind1[i+3]], - vec2[ind1[i+2]], - vec2[ind1[i+1]], - vec2[ind1[i+0]]); - prod = _mm256_mul_pd(val1, val2); - sum = _mm256_add_pd(sum, prod); - i += 4; - } - } else { - while (i<=nnz1_4) { - val1 = _mm256_loadu_pd(vec1+i); - val2 = _mm256_set_pd(vec2[ind1[i+3]], - vec2[ind1[i+2]], - vec2[ind1[i+1]], - vec2[ind1[i+0]]); - prod = _mm256_mul_pd(val1, val2); - sum = _mm256_add_pd(sum, prod); - i += 4; - } + while (i<=nnz1_4) { + val1 = _mm256_loadu_pd(vec1+i); + val2 = _mm256_set_pd(vec2[ind1[i+3]], + vec2[ind1[i+2]], + vec2[ind1[i+1]], + vec2[ind1[i+0]]); + prod = _mm256_mul_pd(val1, val2); + sum = _mm256_add_pd(sum, prod); + i += 4; } // reduce @@ -96,14 +73,8 @@ mjtNum mju_dotSparse_avx(const mjtNum* vec1, const mjtNum* vec2, int nnz1, const } // scalar part - if (flg_unc1) { - for (; i < nnz1; i++) { - res += vec1[ind1[i]] * vec2[ind1[i]]; - } - } else { - for (; i < nnz1; i++) { - res += vec1[i] * vec2[ind1[i]]; - } + for (; i < nnz1; i++) { + res += vec1[i] * vec2[ind1[i]]; } return res; @@ -209,7 +180,7 @@ void mju_mulMatVecSparse_avx(mjtNum* res, const mjtNum* mat, const mjtNum* vec, if (!rowsuper) { // regular sparse dot-product for (int r=0; r0) { - res[r] = mju_dotSparse_avx(mat+rowadr[r], vec, rownnz[r], colind+rowadr[r], /*flg_unc1=*/0); + res[r] = mju_dotSparse_avx(mat+rowadr[r], vec, rownnz[r], colind+rowadr[r]); r++; rs--; @@ -243,7 +214,7 @@ void mju_mulMatVecSparse_avx(mjtNum* res, const mjtNum* mat, const mjtNum* vec, } else { - res[r] = mju_dotSparse_avx(mat+rowadr[r], vec, rownnz[r], colind+rowadr[r], /*flg_unc1=*/0); + res[r] = mju_dotSparse_avx(mat+rowadr[r], vec, rownnz[r], colind+rowadr[r]); } } } diff --git a/test/engine/engine_core_constraint_test.cc b/test/engine/engine_core_constraint_test.cc index 3704bdfc..9f970b18 100644 --- a/test/engine/engine_core_constraint_test.cc +++ b/test/engine/engine_core_constraint_test.cc @@ -32,6 +32,7 @@ namespace mujoco { namespace { using ::testing::DoubleNear; +using ::testing::NotNull; using ::testing::Pointwise; using CoreConstraintTest = MujocoTest; @@ -291,7 +292,9 @@ static const char* const kIlslandEfcPath = // validate mj_constraintUpdate_impl TEST_F(CoreConstraintTest, ConstraintUpdateImpl) { const std::string xml_path = GetTestDataFilePath(kIlslandEfcPath); - mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, nullptr, 0); + char err[1024]; + mjModel* model = mj_loadXML(xml_path.c_str(), 0, err, 1024); + ASSERT_THAT(model, NotNull()) << err; mjData* d1 = mj_makeData(model); mjData* d2 = mj_makeData(model); diff --git a/test/engine/engine_util_sparse_test.cc b/test/engine/engine_util_sparse_test.cc index c62e3384..2f1e087f 100644 --- a/test/engine/engine_util_sparse_test.cc +++ b/test/engine/engine_util_sparse_test.cc @@ -54,57 +54,36 @@ using EngineUtilSparseTest = MujocoTest; TEST_F(EngineUtilSparseTest, MjuDot) { mjtNum a[] = {2, 3, 4, 5, 6, 7, 8}; - mjtNum u[] = {2, 1, 3, 1, 1, 4, 1, 1, 1, 5, 1, 1, 1, 6, 1, 1, 7, 1, 8}; mjtNum b[] = {8, 1, 7, 1, 1, 6, 1, 1, 1, 5, 1, 1, 1, 4, 1, 1, 3, 1, 2}; int i[] = {0, 2, 5, 9, 13, 16, 18}; // test various vector lengths as mju_dotSparse adds numbers in groups of four - // a is compressed - int flg_unc1 = 0; - EXPECT_EQ(mju_dotSparse(a, b, 0, i, flg_unc1), 0); - EXPECT_EQ(mju_dotSparse(a, b, 1, i, flg_unc1), 2*8); - EXPECT_EQ(mju_dotSparse(a, b, 2, i, flg_unc1), 2*8 + 3*7); - EXPECT_EQ(mju_dotSparse(a, b, 3, i, flg_unc1), 2*8 + 3*7 + 4*6); - EXPECT_EQ(mju_dotSparse(a, b, 4, i, flg_unc1), 2*8 + 3*7 + 4*6 + 5*5); - EXPECT_EQ(mju_dotSparse(a, b, 5, i, flg_unc1), 2*8 + 3*7 + 4*6 + 5*5 + 6*4); - EXPECT_EQ(mju_dotSparse(a, b, 6, i, flg_unc1), + EXPECT_EQ(mju_dotSparse(a, b, 0, i), 0); + EXPECT_EQ(mju_dotSparse(a, b, 1, i), 2*8); + EXPECT_EQ(mju_dotSparse(a, b, 2, i), 2*8 + 3*7); + EXPECT_EQ(mju_dotSparse(a, b, 3, i), 2*8 + 3*7 + 4*6); + EXPECT_EQ(mju_dotSparse(a, b, 4, i), 2*8 + 3*7 + 4*6 + 5*5); + EXPECT_EQ(mju_dotSparse(a, b, 5, i), 2*8 + 3*7 + 4*6 + 5*5 + 6*4); + EXPECT_EQ(mju_dotSparse(a, b, 6, i), 2*8 + 3*7 + 4*6 + 5*5 + 6*4 + 7*3); - EXPECT_EQ(mju_dotSparse(a, b, 7, i, flg_unc1), - 2*8 + 3*7 + 4*6 + 5*5 + 6*4 + 7*3 + 8*2); - - // u is compressed - flg_unc1 = 1; - EXPECT_EQ(mju_dotSparse(u, b, 0, i, flg_unc1), 0); - EXPECT_EQ(mju_dotSparse(u, b, 1, i, flg_unc1), 2*8); - EXPECT_EQ(mju_dotSparse(u, b, 2, i, flg_unc1), 2*8 + 3*7); - EXPECT_EQ(mju_dotSparse(u, b, 3, i, flg_unc1), 2*8 + 3*7 + 4*6); - EXPECT_EQ(mju_dotSparse(u, b, 4, i, flg_unc1), 2*8 + 3*7 + 4*6 + 5*5); - EXPECT_EQ(mju_dotSparse(u, b, 5, i, flg_unc1), 2*8 + 3*7 + 4*6 + 5*5 + 6*4); - EXPECT_EQ(mju_dotSparse(u, b, 6, i, flg_unc1), - 2*8 + 3*7 + 4*6 + 5*5 + 6*4 + 7*3); - EXPECT_EQ(mju_dotSparse(u, b, 7, i, flg_unc1), + EXPECT_EQ(mju_dotSparse(a, b, 7, i), 2*8 + 3*7 + 4*6 + 5*5 + 6*4 + 7*3 + 8*2); } TEST_F(EngineUtilSparseTest, MjuDot2) { constexpr int annz = 6; constexpr int bnnz = 5; - int ia[annz] = {0, 2, 5, 6, 7}; + + // values mjtNum a[annz] = {2, 3, 4, 5, 6}; - int ib[bnnz] = { 1, 2, 3, 5, 7}; mjtNum b[bnnz] = { 8, 7, 6, 5, 4}; - mjtNum u[] = {1, 8, 7, 6, 1, 5, 1, 4}; - // test various vector lengths as mju_dotSparse adds numbers in groups of four + // indices + int ia[annz] = {0, 2, 5, 6, 7}; + int ib[bnnz] = { 1, 2, 3, 5, 7}; - // a is compressed - int flg_unc2 = 0; - EXPECT_EQ(mju_dotSparse2(a, b, annz, ia, bnnz, ib, flg_unc2), 3*7+4*5+6*4); - - // u is uncompressed - flg_unc2 = 1; - EXPECT_EQ(mju_dotSparse2(a, u, annz, ia, bnnz, ib, flg_unc2), 3*7+4*5+6*4); + EXPECT_EQ(mju_dotSparse2(a, ia, annz, b, ib, bnnz), 3 * 7 + 4 * 5 + 6 * 4); } TEST_F(EngineUtilSparseTest, CombineSparseCount) { From 436b5a8e1f35b6acc5c64944b58a4834e8d38f0c Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 12 May 2025 16:18:51 -0700 Subject: [PATCH 117/191] Add `mju_addToSymSparse` - Internal engine function to add a symmetric sparse matrix to a dense matrix. - Also minor refactors to related functions. PiperOrigin-RevId: 757954336 Change-Id: I8d8a48bcbbd5d6c618ae097a87df2bc1e6f5ef1d --- src/engine/engine_support.c | 16 +++++------ src/engine/engine_support.h | 4 +-- src/engine/engine_util_blas.c | 22 ++++++++++----- src/engine/engine_util_blas.h | 4 +++ src/engine/engine_util_sparse.c | 37 +++++++++++++++++++++++++- src/engine/engine_util_sparse.h | 11 ++++++++ test/engine/engine_util_sparse_test.cc | 34 +++++++++++++++++++++++ 7 files changed, 109 insertions(+), 19 deletions(-) diff --git a/src/engine/engine_support.c b/src/engine/engine_support.c index b7036659..5eb0c9fd 100644 --- a/src/engine/engine_support.c +++ b/src/engine/engine_support.c @@ -1089,20 +1089,18 @@ void mj_addM(const mjModel* m, mjData* d, mjtNum* dst, // add inertia matrix to sparse destination matrix void mj_addMSparse(const mjModel* m, mjData* d, mjtNum* dst, - int* rownnz, int* rowadr, int* colind, mjtNum* M, - int* M_rownnz, int* M_rowadr, int* M_colind) { + int* rownnz, int* rowadr, int* colind, const mjtNum* M, + const int* M_rownnz, const int* M_rowadr, const int* M_colind) { int nv = m->nv; mj_markStack(d); + mjtNum* buf_val = mjSTACKALLOC(d, nv, mjtNum); int* buf_ind = mjSTACKALLOC(d, nv, int); - mjtNum* sparse_buf = mjSTACKALLOC(d, nv, mjtNum); - // add to destination - for (int i=0; i < nv; i++) { - rownnz[i] = mju_combineSparse(dst + rowadr[i], M + M_rowadr[i], 1, 1, - rownnz[i], M_rownnz[i], colind + rowadr[i], - M_colind + M_rowadr[i], sparse_buf, buf_ind); - } + mju_addToMatSparse(dst, rownnz, rowadr, colind, nv, + M, M_rownnz, M_rowadr, M_colind, + buf_val, buf_ind); + mj_freeStack(d); } diff --git a/src/engine/engine_support.h b/src/engine/engine_support.h index 818e9622..5933f6a7 100644 --- a/src/engine/engine_support.h +++ b/src/engine/engine_support.h @@ -137,8 +137,8 @@ MJAPI void mj_addM(const mjModel* m, mjData* d, mjtNum* dst, // add inertia matrix to sparse destination matrix MJAPI void mj_addMSparse(const mjModel* m, mjData* d, mjtNum* dst, - int* rownnz, int* rowadr, int* colind, mjtNum* M, - int* M_rownnz, int* M_rowadr, int* M_colind); + int* rownnz, int* rowadr, int* colind, const mjtNum* M, + const int* M_rownnz, const int* M_rowadr, const int* M_colind); // add inertia matrix to dense destination matrix MJAPI void mj_addMDense(const mjModel* m, mjData* d, mjtNum* dst); diff --git a/src/engine/engine_util_blas.c b/src/engine/engine_util_blas.c index c870c6fb..95055d72 100644 --- a/src/engine/engine_util_blas.c +++ b/src/engine/engine_util_blas.c @@ -843,9 +843,9 @@ void mju_mulMatMatT(mjtNum* res, const mjtNum* mat1, const mjtNum* mat2, } - -// compute M'*diag*M (diag=NULL: compute M'*M) -void mju_sqrMatTD(mjtNum* res, const mjtNum* mat, const mjtNum* diag, int nr, int nc) { +// compute M'*diag*M (diag=NULL: compute M'*M), upper triangle optional +void mju_sqrMatTD_impl(mjtNum* res, const mjtNum* mat, const mjtNum* diag, + int nr, int nc, int flg_upper) { mjtNum tmp; // half of MatMat routine: only lower triangle @@ -870,15 +870,23 @@ void mju_sqrMatTD(mjtNum* res, const mjtNum* mat, const mjtNum* diag, int nr, in } } - // make symmetric - for (int i=0; i < nc; i++) { - for (int j=i+1; j < nc; j++) { - res[i*nc+j] = res[j*nc+i]; + // flg_upper is set: make symmetric + if (flg_upper) { + for (int i=0; i < nc; i++) { + for (int j=i+1; j < nc; j++) { + res[i*nc+j] = res[j*nc+i]; + } } } } +// compute M'*diag*M (diag=NULL: compute M'*M) +void mju_sqrMatTD(mjtNum* res, const mjtNum* mat, const mjtNum* diag, int nr, int nc) { + mju_sqrMatTD_impl(res, mat, diag, nr, nc, /*flg_upper=*/ 1); +} + + // multiply matrices, first argument transposed void mju_mulMatTMat(mjtNum* res, const mjtNum* mat1, const mjtNum* mat2, diff --git a/src/engine/engine_util_blas.h b/src/engine/engine_util_blas.h index 619a3225..4b2519c9 100644 --- a/src/engine/engine_util_blas.h +++ b/src/engine/engine_util_blas.h @@ -223,6 +223,10 @@ MJAPI void mju_mulMatMatT(mjtNum* res, const mjtNum* mat1, const mjtNum* mat2, MJAPI void mju_mulMatTMat(mjtNum* res, const mjtNum* mat1, const mjtNum* mat2, int r1, int c1, int c2); +// compute M'*diag*M (diag=NULL: compute M'*M), upper triangle optional +void mju_sqrMatTD_impl(mjtNum* res, const mjtNum* mat, const mjtNum* diag, int nr, int nc, + int flg_upper); + // compute M'*diag*M (diag=NULL: compute M'*M) MJAPI void mju_sqrMatTD(mjtNum* res, const mjtNum* mat, const mjtNum* diag, int nr, int nc); diff --git a/src/engine/engine_util_sparse.c b/src/engine/engine_util_sparse.c index 250020e4..8d364c5e 100644 --- a/src/engine/engine_util_sparse.c +++ b/src/engine/engine_util_sparse.c @@ -186,6 +186,41 @@ void mju_mulMatTVecSparse(mjtNum* res, const mjtNum* mat, const mjtNum* vec, int } +// add sparse matrix M to sparse destination matrix, requires pre-allocated buffers +void mju_addToMatSparse(mjtNum* dst, int* rownnz, int* rowadr, int* colind, int nr, + const mjtNum* M, const int* M_rownnz, const int* M_rowadr, + const int* M_colind, + mjtNum* buf_val, int* buf_ind) { + for (int i=0; i < nr; i++) { + rownnz[i] = mju_combineSparse(dst + rowadr[i], M + M_rowadr[i], 1, 1, + rownnz[i], M_rownnz[i], colind + rowadr[i], + M_colind + M_rowadr[i], buf_val, buf_ind); + } +} + + +// add symmetric matrix (lower triangle) to dense matrix, upper triangle optional +void mju_addToSymSparse(mjtNum* res, const mjtNum* mat, int n, + const int* rownnz, const int* rowadr, const int* colind, int flg_upper) { + for (int i=0; i < n; i++) { + int start = rowadr[i]; + int end = start + rownnz[i]; + for (int adr=start; adr < end; adr++) { + mjtNum val = mat[adr]; + int j = colind[adr]; + + // lower + diagonal + res[i*n + j] += val; + + // strict upper + if (flg_upper && j < i) { + res[j*n + i] += val; + } + } + } +} + + // multiply symmetric matrix (only lower triangle represented) by vector: // res = (mat + strict_upper(mat')) * vec @@ -214,7 +249,7 @@ void mju_mulSymVecSparse(mjtNum* restrict res, const mjtNum* restrict mat, // off-diagonals const int* ind = colind + adr; - for (int k=0; k < diag; k++) { + for (int k=diag-1; k >= 0; k--) { int j = ind[k]; mjtNum val = row[k]; res[i] += val * vec[j]; // strict lower diff --git a/src/engine/engine_util_sparse.h b/src/engine/engine_util_sparse.h index ea26014b..616b4d21 100644 --- a/src/engine/engine_util_sparse.h +++ b/src/engine/engine_util_sparse.h @@ -50,6 +50,17 @@ MJAPI void mju_mulMatVecSparse(mjtNum* res, const mjtNum* mat, const mjtNum* vec MJAPI void mju_mulMatTVecSparse(mjtNum* res, const mjtNum* mat, const mjtNum* vec, int nr, int nc, const int* rownnz, const int* rowadr, const int* colind); +// add sparse matrix M to sparse destination matrix, requires pre-allocated buffers +MJAPI void mju_addToMatSparse(mjtNum* dst, int* rownnz, int* rowadr, int* colind, int nr, + const mjtNum* M, const int* M_rownnz, const int* M_rowadr, + const int* M_colind, + mjtNum* buf_val, int* buf_ind); + +// add symmetric matrix (only lower triangle represented) to dense matrix +MJAPI void mju_addToSymSparse(mjtNum* res, const mjtNum* mat, int n, + const int* rownnz, const int* rowadr, const int* colind, + int flg_upper); + // multiply symmetric matrix (only lower triangle represented) by vector: // res = (mat + strict_upper(mat')) * vec MJAPI void mju_mulSymVecSparse(mjtNum* res, const mjtNum* mat, const mjtNum* vec, int n, diff --git a/test/engine/engine_util_sparse_test.cc b/test/engine/engine_util_sparse_test.cc index 2f1e087f..10875a67 100644 --- a/test/engine/engine_util_sparse_test.cc +++ b/test/engine/engine_util_sparse_test.cc @@ -1036,6 +1036,40 @@ TEST_F(EngineUtilSparseTest, MjuMulMatTVec) { EXPECT_THAT(AsVector(res, 3), ElementsAre(5, 28, 24)); } +TEST_F(EngineUtilSparseTest, MjuAddToSymSparse) { + // 1 2 4 + // M = 2 3 0 + // 4 0 5 + + // only lower triangle represented + mjtNum mat[] = {1, 2, 3, 4, 5}; + int colind[] = {0, 0, 1, 0, 2}; + int rownnz[] = {1, 2, 2}; + int rowadr[] = {0, 1, 3}; + + // 0 0 0 + // A = 5 4 2 + // 4 3 2 + mjtNum A[] = {0, 0, 0, + 5, 4, 2, + 4, 3, 2}; + + mju_addToSymSparse(A, mat, 3, rownnz, rowadr, colind, /*flg_upper=*/1); + EXPECT_THAT(AsVector(A, 9), ElementsAre(1, 2, 4, + 7, 7, 2, + 8, 3, 7)); + + // same as A + mjtNum B[] = {0, 0, 0, + 5, 4, 2, + 4, 3, 2}; + + mju_addToSymSparse(B, mat, 3, rownnz, rowadr, colind, /*flg_upper=*/0); + EXPECT_THAT(AsVector(B, 9), ElementsAre(1, 0, 0, + 7, 7, 2, + 8, 3, 7)); +} + TEST_F(EngineUtilSparseTest, MjuMulSymVecSparse) { constexpr int n = 4; constexpr int nnz = 9; From 627fffdef98a6f130ceb443ee94e3977453cdb0d Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 13 May 2025 10:19:22 -0700 Subject: [PATCH 118/191] Switch `mjData.{qH,qLD}` from full ("M") to reduced ("C") inertia matrix structure. PiperOrigin-RevId: 758273074 Change-Id: If1a2e663ea70044694af985e0119afd6d58115ac --- doc/includes/references.h | 10 ++--- include/mujoco/mjdata.h | 10 ++--- include/mujoco/mjxmacro.h | 10 ++--- mjx/mujoco/mjx/_src/io.py | 6 +-- mjx/mujoco/mjx/_src/smooth_test.py | 4 +- python/mujoco/introspect/structs.py | 10 ++--- src/engine/engine_core_constraint.c | 18 ++++---- src/engine/engine_core_smooth.c | 13 +++--- src/engine/engine_forward.c | 18 ++++---- src/engine/engine_island.c | 6 +-- src/engine/engine_print.c | 6 +-- src/engine/engine_solver.c | 6 +-- src/engine/engine_support.c | 6 +-- test/benchmark/factorI_benchmark_test.cc | 6 +-- test/benchmark/inertia_benchmark_test.cc | 8 ++-- test/benchmark/solveLD_benchmark_test.cc | 5 ++- test/engine/engine_core_smooth_test.cc | 54 ++++++++++++------------ test/engine/engine_derivative_test.cc | 4 +- 18 files changed, 99 insertions(+), 101 deletions(-) diff --git a/doc/includes/references.h b/doc/includes/references.h index 402043aa..a2b820e5 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -272,7 +272,7 @@ struct mjData_ { mjtNum* qM; // total inertia (sparse) (nM x 1) // computed by mj_fwdPosition/mj_factorM - mjtNum* qLD; // L'*D*L factorization of M (sparse) (nM x 1) + mjtNum* qLD; // L'*D*L factorization of M (sparse) (nC x 1) mjtNum* qLDiagInv; // 1/diag(D) (nv x 1) // computed by mj_collisionTree @@ -305,7 +305,7 @@ struct mjData_ { mjtNum* subtree_angmom; // angular momentum about subtree com (nbody x 3) // computed by mj_Euler or mj_implicit - mjtNum* qH; // L'*D*L factorization of modified M (nM x 1) + mjtNum* qH; // L'*D*L factorization of modified M (nC x 1) mjtNum* qHDiagInv; // 1/diag(D) of modified M (nv x 1) // computed by mj_resetData @@ -396,9 +396,9 @@ struct mjData_ { int* iM_rownnz; // inertia: non-zeros in each row (nidof x 1) int* iM_rowadr; // inertia: address of each row in iM_colind (nidof x 1) int* iM_diagnum; // inertia: num of consecutive diagonal elements (nidof x 1) - int* iM_colind; // inertia: column indices of non-zeros (nM x 1) - mjtNum* iM; // total inertia (sparse) (nM x 1) - mjtNum* iLD; // L'*D*L factorization of M (sparse) (nM x 1) + int* iM_colind; // inertia: column indices of non-zeros (nC x 1) + mjtNum* iM; // total inertia (sparse) (nC x 1) + mjtNum* iLD; // L'*D*L factorization of M (sparse) (nC x 1) mjtNum* iLDiagInv; // 1/diag(D) (nidof x 1) mjtNum* iacc; // acceleration (nidof x 1) diff --git a/include/mujoco/mjdata.h b/include/mujoco/mjdata.h index 963630cc..9ac187dc 100644 --- a/include/mujoco/mjdata.h +++ b/include/mujoco/mjdata.h @@ -300,7 +300,7 @@ struct mjData_ { mjtNum* qM; // total inertia (sparse) (nM x 1) // computed by mj_fwdPosition/mj_factorM - mjtNum* qLD; // L'*D*L factorization of M (sparse) (nM x 1) + mjtNum* qLD; // L'*D*L factorization of M (sparse) (nC x 1) mjtNum* qLDiagInv; // 1/diag(D) (nv x 1) // computed by mj_collisionTree @@ -333,7 +333,7 @@ struct mjData_ { mjtNum* subtree_angmom; // angular momentum about subtree com (nbody x 3) // computed by mj_Euler or mj_implicit - mjtNum* qH; // L'*D*L factorization of modified M (nM x 1) + mjtNum* qH; // L'*D*L factorization of modified M (nC x 1) mjtNum* qHDiagInv; // 1/diag(D) of modified M (nv x 1) // computed by mj_resetData @@ -424,9 +424,9 @@ struct mjData_ { int* iM_rownnz; // inertia: non-zeros in each row (nidof x 1) int* iM_rowadr; // inertia: address of each row in iM_colind (nidof x 1) int* iM_diagnum; // inertia: num of consecutive diagonal elements (nidof x 1) - int* iM_colind; // inertia: column indices of non-zeros (nM x 1) - mjtNum* iM; // total inertia (sparse) (nM x 1) - mjtNum* iLD; // L'*D*L factorization of M (sparse) (nM x 1) + int* iM_colind; // inertia: column indices of non-zeros (nC x 1) + mjtNum* iM; // total inertia (sparse) (nC x 1) + mjtNum* iLD; // L'*D*L factorization of M (sparse) (nC x 1) mjtNum* iLDiagInv; // 1/diag(D) (nidof x 1) mjtNum* iacc; // acceleration (nidof x 1) diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index 88e6c0f7..1f96199e 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -653,7 +653,7 @@ X ( mjtNum, actuator_moment, nJmom, 1 ) \ X ( mjtNum, crb, nbody, 10 ) \ X ( mjtNum, qM, nM, 1 ) \ - X ( mjtNum, qLD, nM, 1 ) \ + X ( mjtNum, qLD, nC, 1 ) \ X ( mjtNum, qLDiagInv, nv, 1 ) \ XMJV( mjtNum, bvh_aabb_dyn, nbvhdynamic, 6 ) \ XMJV( mjtByte, bvh_active, nbvh, 1 ) \ @@ -670,7 +670,7 @@ X ( mjtNum, qfrc_passive, nv, 1 ) \ X ( mjtNum, subtree_linvel, nbody, 3 ) \ X ( mjtNum, subtree_angmom, nbody, 3 ) \ - X ( mjtNum, qH, nM, 1 ) \ + X ( mjtNum, qH, nC, 1 ) \ X ( mjtNum, qHDiagInv, nv, 1 ) \ X ( int, B_rownnz, nbody, 1 ) \ X ( int, B_rowadr, nbody, 1 ) \ @@ -758,9 +758,9 @@ X( int, iM_rownnz, MJ_D(nidof), 1 ) \ X( int, iM_rowadr, MJ_D(nidof), 1 ) \ X( int, iM_diagnum, MJ_D(nidof), 1 ) \ - X( int, iM_colind, MJ_M(nM), 1 ) \ - X( mjtNum, iM, MJ_M(nM), 1 ) \ - X( mjtNum, iLD, MJ_M(nM), 1 ) \ + X( int, iM_colind, MJ_M(nC), 1 ) \ + X( mjtNum, iM, MJ_M(nC), 1 ) \ + X( mjtNum, iLD, MJ_M(nC), 1 ) \ X( mjtNum, iLDiagInv, MJ_D(nidof), 1 ) \ X( mjtNum, iacc, MJ_D(nidof), 1 ) \ X( int, efc_island, MJ_D(nefc), 1 ) \ diff --git a/mjx/mujoco/mjx/_src/io.py b/mjx/mujoco/mjx/_src/io.py index 1d2ad234..8fc28d83 100644 --- a/mjx/mujoco/mjx/_src/io.py +++ b/mjx/mujoco/mjx/_src/io.py @@ -509,7 +509,7 @@ def _make_data_jax( 'actuator_moment': (m.nu, m.nv, float_), 'crb': (m.nbody, 10, float_), 'qM': (m.nM, float_) if support.is_sparse(m) else (m.nv, m.nv, float_), - 'qLD': (m.nM, float_) if support.is_sparse(m) else (m.nv, m.nv, float_), + 'qLD': (m.nC, float_) if support.is_sparse(m) else (m.nv, m.nv, float_), 'qLDiagInv': (m.nv, float_) if support.is_sparse(m) else (0, float_), 'ten_velocity': (m.ntendon, float_), 'actuator_velocity': (m.nu, float_), @@ -618,8 +618,8 @@ def _make_data_c( 'flexedge_velocity': (nflexedge, float_), 'crb': (m.nbody, 10, float_), 'qM': (m.nM, float_), - 'qLD': (m.nM, float_), - 'qH': (m.nM, float_), + 'qLD': (m.nC, float_), + 'qH': (m.nC, float_), 'qHDiagInv': (m.nv, float_), 'qLDiagInv': (m.nv, float_), 'ten_velocity': (m.ntendon, float_), diff --git a/mjx/mujoco/mjx/_src/smooth_test.py b/mjx/mujoco/mjx/_src/smooth_test.py index 4f182a75..9e23fb68 100644 --- a/mjx/mujoco/mjx/_src/smooth_test.py +++ b/mjx/mujoco/mjx/_src/smooth_test.py @@ -91,8 +91,8 @@ class SmoothTest(absltest.TestCase): # factor_m dx = jax.jit(mjx.factor_m)(mx, mjx.put_data(m, d)) qLDLegacy = np.zeros(mx.nM) # pylint:disable=invalid-name - for i in range(m.nM): - qLDLegacy[d.mapM2M[i]] = d.qLD[i] + for i in range(m.nC): + qLDLegacy[d.mapM2C[i]] = d.qLD[i] _assert_eq(qLDLegacy, dx._impl.qLD, 'qLD') _assert_attr_eq(d, dx._impl, 'qLDiagInv') # com_vel diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index b4e48f39..500b2d40 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -5398,7 +5398,7 @@ STRUCTS: Mapping[str, StructDecl] = dict([ inner_type=ValueType(name='mjtNum'), ), doc="L'*D*L factorization of M (sparse)", - array_extent=('nM',), + array_extent=('nC',), ), StructFieldDecl( name='qLDiagInv', @@ -5534,7 +5534,7 @@ STRUCTS: Mapping[str, StructDecl] = dict([ inner_type=ValueType(name='mjtNum'), ), doc="L'*D*L factorization of modified M", - array_extent=('nM',), + array_extent=('nC',), ), StructFieldDecl( name='qHDiagInv', @@ -6030,7 +6030,7 @@ STRUCTS: Mapping[str, StructDecl] = dict([ inner_type=ValueType(name='int'), ), doc='inertia: column indices of non-zeros', - array_extent=('nM',), + array_extent=('nC',), ), StructFieldDecl( name='iM', @@ -6038,7 +6038,7 @@ STRUCTS: Mapping[str, StructDecl] = dict([ inner_type=ValueType(name='mjtNum'), ), doc='total inertia (sparse)', - array_extent=('nM',), + array_extent=('nC',), ), StructFieldDecl( name='iLD', @@ -6046,7 +6046,7 @@ STRUCTS: Mapping[str, StructDecl] = dict([ inner_type=ValueType(name='mjtNum'), ), doc="L'*D*L factorization of M (sparse)", - array_extent=('nM',), + array_extent=('nC',), ), StructFieldDecl( name='iLDiagInv', diff --git a/src/engine/engine_core_constraint.c b/src/engine/engine_core_constraint.c index dba314d9..aa7267ed 100644 --- a/src/engine/engine_core_constraint.c +++ b/src/engine/engine_core_constraint.c @@ -2039,7 +2039,7 @@ void mj_projectConstraint(const mjModel* m, mjData* d) { // inverse square root of D from inertia LDL decomposition mjtNum* sqrtInvD = mjSTACKALLOC(d, nv, mjtNum); for (int i=0; i < nv; i++) { - int diag = d->M_rowadr[i] + d->M_rownnz[i] - 1; + int diag = d->C_rowadr[i] + d->C_rownnz[i] - 1; sqrtInvD[i] = 1 / mju_sqrt(d->qLD[diag]); } @@ -2075,11 +2075,11 @@ void mj_projectConstraint(const mjModel* m, mjData* d) { continue; } - // traverse row j of M, marking new unique nonzeros - int nnzM = d->M_rownnz[j]; - int adrM = d->M_rowadr[j]; - for (int k=0; k < nnzM; k++) { - int c = d->M_colind[adrM + k]; + // traverse row j of C, marking new unique nonzeros + int nnzC = d->C_rownnz[j]; + int adrC = d->C_rowadr[j]; + for (int k=0; k < nnzC; k++) { + int c = d->C_colind[adrC + k]; if (marker[c] != r) { marker[c] = r; nnz++; @@ -2159,10 +2159,10 @@ void mj_projectConstraint(const mjModel* m, mjData* d) { continue; } int j = B_colind[i]; - int adrM = d->M_rowadr[j]; - mju_addToSclSparseInc(B + adrB, d->qLD + adrM, + int adrC = d->C_rowadr[j]; + mju_addToSclSparseInc(B + adrB, d->qLD + adrC, nnzB, B_colind + adrB, - d->M_rownnz[j]-1, d->M_colind + adrM, -b); + d->C_rownnz[j]-1, d->C_colind + adrC, -b); } // B(r,:) <- sqrt(inv(D)) * B(r,:) diff --git a/src/engine/engine_core_smooth.c b/src/engine/engine_core_smooth.c index 112f62bd..3b5601a2 100644 --- a/src/engine/engine_core_smooth.c +++ b/src/engine/engine_core_smooth.c @@ -1653,9 +1653,8 @@ void mj_factorM(const mjModel* m, mjData* d) { TM_START; // gather LD <- M (legacy to CSR) and factorize in-place - mju_gather(d->qLD, d->qM, d->mapM2M, m->nM); - mj_factorI(d->qLD, d->qLDiagInv, m->nv, d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); - + mju_gather(d->qLD, d->qM, d->mapM2C, m->nC); + mj_factorI(d->qLD, d->qLDiagInv, m->nv, d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); TM_ADD(mjTIMER_POS_INERTIA); } @@ -1897,7 +1896,7 @@ void mj_solveM(const mjModel* m, mjData* d, mjtNum* x, const mjtNum* y, int n) { mju_copy(x, y, n*m->nv); } mj_solveLD(x, d->qLD, d->qLDiagInv, m->nv, n, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); } @@ -1908,9 +1907,9 @@ void mj_solveM2(const mjModel* m, mjData* d, mjtNum* x, const mjtNum* y, int nv = m->nv; // local copies of key variables - const int* rownnz = d->M_rownnz; - const int* rowadr = d->M_rowadr; - const int* colind = d->M_colind; + const int* rownnz = d->C_rownnz; + const int* rowadr = d->C_rowadr; + const int* colind = d->C_colind; const int* diagnum = m->dof_simplenum; const mjtNum* qLD = d->qLD; diff --git a/src/engine/engine_forward.c b/src/engine/engine_forward.c index b48b5804..0707efee 100644 --- a/src/engine/engine_forward.c +++ b/src/engine/engine_forward.c @@ -836,7 +836,7 @@ static void mj_advance(const mjModel* m, mjData* d, // Euler integrator, semi-implicit in velocity, possibly skipping factorisation void mj_EulerSkip(const mjModel* m, mjData* d, int skipfactor) { TM_START; - int nv = m->nv, nM = m->nM; + int nv = m->nv, nC = m->nC; mj_markStack(d); mjtNum* qfrc = mjSTACKALLOC(d, nv, mjtNum); mjtNum* qacc = mjSTACKALLOC(d, nv, mjtNum); @@ -861,20 +861,20 @@ void mj_EulerSkip(const mjModel* m, mjData* d, int skipfactor) { else { if (!skipfactor) { // qH = M + h*diag(B) - mju_gather(d->qH, d->qM, d->mapM2M, nM); + mju_gather(d->qH, d->qM, d->mapM2C, nC); for (int i=0; i < nv; i++) { - d->qH[d->M_rowadr[i] + d->M_rownnz[i] - 1] += m->opt.timestep * m->dof_damping[i]; + d->qH[d->C_rowadr[i] + d->C_rownnz[i] - 1] += m->opt.timestep * m->dof_damping[i]; } // factorize in-place - mj_factorI(d->qH, d->qHDiagInv, nv, d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + mj_factorI(d->qH, d->qHDiagInv, nv, d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); } // solve mju_add(qfrc, d->qfrc_smooth, d->qfrc_constraint, nv); mju_copy(qacc, qfrc, m->nv); mj_solveLD(qacc, d->qH, d->qHDiagInv, nv, 1, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); } // advance state and time @@ -1003,7 +1003,7 @@ void mj_RungeKutta(const mjModel* m, mjData* d, int N) { // fully implicit in velocity, possibly skipping factorization void mj_implicitSkip(const mjModel* m, mjData* d, int skipfactor) { TM_START; - int nv = m->nv, nM = m->nM, nD = m->nD; + int nv = m->nv, nM = m->nM, nD = m->nD, nC = m->nC; mj_markStack(d); mjtNum* qfrc = mjSTACKALLOC(d, nv, mjtNum); @@ -1047,16 +1047,16 @@ void mj_implicitSkip(const mjModel* m, mjData* d, int skipfactor) { mju_addScl(MhB, d->qM, MhB, -m->opt.timestep, nM); // gather qH <- MhB (legacy to CSR) - mju_gather(d->qH, MhB, d->mapM2M, nM); + mju_gather(d->qH, MhB, d->mapM2C, nC); // factorize in-place - mj_factorI(d->qH, d->qHDiagInv, nv, d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + mj_factorI(d->qH, d->qHDiagInv, nv, d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); } // solve for qacc: (qM - dt*qDeriv) * qacc = qfrc mju_copy(qacc, qfrc, nv); mj_solveLD(qacc, d->qH, d->qHDiagInv, nv, 1, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); } else { mjERROR("integrator must be implicit or implicitfast"); diff --git a/src/engine/engine_island.c b/src/engine/engine_island.c index 108844d8..5d04118e 100644 --- a/src/engine/engine_island.c +++ b/src/engine/engine_island.c @@ -536,12 +536,12 @@ void mj_island(const mjModel* m, mjData* d) { } // local CSR copy of qM - mjtNum* qM = mjSTACKALLOC(d, m->nM, mjtNum); - mju_gather(qM, d->qM, d->mapM2M, m->nM); + mjtNum* qM = mjSTACKALLOC(d, m->nC, mjtNum); + mju_gather(qM, d->qM, d->mapM2C, m->nC); // inertia: block-diagonalize both iLD <- qLD and iM <- qM mju_blockDiagSparse(d->iLD, d->iM_rownnz, d->iM_rowadr, d->iM_colind, - d->qLD, d->M_rownnz, d->M_rowadr, d->M_colind, + d->qLD, d->C_rownnz, d->C_rowadr, d->C_colind, nidof, nisland, d->map_idof2dof, d->map_dof2idof, d->island_idofadr, d->island_idofadr, diff --git a/src/engine/engine_print.c b/src/engine/engine_print.c index 61a9ef02..bb9f4a9a 100644 --- a/src/engine/engine_print.c +++ b/src/engine/engine_print.c @@ -1126,12 +1126,12 @@ void mj_printFormattedData(const mjModel* m, const mjData* d, const char* filena printInertia("QM", d->qM, m, fp, float_format); - printSparse("QLD", d->qLD, m->nv, d->M_rownnz, - d->M_rowadr, d->M_colind, fp, float_format); + printSparse("QLD", d->qLD, m->nv, d->C_rownnz, + d->C_rowadr, d->C_colind, fp, float_format); printArray("QLDIAGINV", m->nv, 1, d->qLDiagInv, fp, float_format); if (!mju_isZero(d->qHDiagInv, m->nv)) { - printSparse("QH", d->qH, m->nv, d->M_rownnz, d->M_rowadr, d->M_colind, fp, float_format); + printSparse("QH", d->qH, m->nv, d->C_rownnz, d->C_rowadr, d->C_colind, fp, float_format); printArray("QHDIAGINV", m->nv, 1, d->qHDiagInv, fp, float_format); } diff --git a/src/engine/engine_solver.c b/src/engine/engine_solver.c index 354386bb..c2f15709 100644 --- a/src/engine/engine_solver.c +++ b/src/engine/engine_solver.c @@ -881,10 +881,10 @@ static void CGpointers(const mjModel* m, const mjData* d, mjCGContext* ctx, int ctx->qacc = d->qacc; // inertia - ctx->M_rownnz = d->M_rownnz; - ctx->M_rowadr = d->M_rowadr; + ctx->M_rownnz = d->C_rownnz; + ctx->M_rowadr = d->C_rowadr; ctx->M_diagnum = m->dof_simplenum; - ctx->M_colind = d->M_colind; + ctx->M_colind = d->C_colind; ctx->dof_Madr = m->dof_Madr; ctx->dof_parentid = m->dof_parentid; ctx->qM = d->qM; diff --git a/src/engine/engine_support.c b/src/engine/engine_support.c index 5eb0c9fd..362292da 100644 --- a/src/engine/engine_support.c +++ b/src/engine/engine_support.c @@ -1047,14 +1047,14 @@ void mj_mulM2(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec) // non-simple: add off-diagonals if (!m->dof_simplenum[i]) { - int adr = d->M_rowadr[i]; - res[i] += mju_dotSparse(qLD+adr, vec, d->M_rownnz[i] - 1, d->M_colind+adr); + int adr = d->C_rowadr[i]; + res[i] += mju_dotSparse(qLD+adr, vec, d->C_rownnz[i] - 1, d->C_colind+adr); } } // res *= sqrt(D) for (int i=0; i < nv; i++) { - int diag = d->M_rowadr[i] + d->M_rownnz[i] - 1; + int diag = d->C_rowadr[i] + d->C_rownnz[i] - 1; res[i] *= mju_sqrt(qLD[diag]); } } diff --git a/test/benchmark/factorI_benchmark_test.cc b/test/benchmark/factorI_benchmark_test.cc index f2feea1e..998af271 100644 --- a/test/benchmark/factorI_benchmark_test.cc +++ b/test/benchmark/factorI_benchmark_test.cc @@ -45,8 +45,8 @@ static void BM_factorI(benchmark::State& state, bool legacy, bool coil) { mj_markStack(d); // M: mass matrix in CSR format - mjtNum* M = mj_stackAllocNum(d, m->nM); - mju_gather(M, d->qM, d->mapM2M, m->nM); + mjtNum* M = mj_stackAllocNum(d, m->nC); + mju_gather(M, d->qM, d->mapM2C, m->nC); // LDlegacy: legacy LD matrix (size nM) mjtNum* LDlegacy = mj_stackAllocNum(d, m->nM); @@ -59,7 +59,7 @@ static void BM_factorI(benchmark::State& state, bool legacy, bool coil) { } else { mju_copy(d->qLD, M, m->nC); mj_factorI(d->qLD, d->qLDiagInv, m->nv, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); } } } diff --git a/test/benchmark/inertia_benchmark_test.cc b/test/benchmark/inertia_benchmark_test.cc index 3bdeddfd..c8460cda 100644 --- a/test/benchmark/inertia_benchmark_test.cc +++ b/test/benchmark/inertia_benchmark_test.cc @@ -47,8 +47,8 @@ static void BM_solve(benchmark::State& state, SolveType type) { mj_markStack(d); // M: mass matrix in CSR format - mjtNum* M = mj_stackAllocNum(d, m->nM); - mju_gather(M, d->qM, d->mapM2M, m->nM); + mjtNum* M = mj_stackAllocNum(d, m->nC); + mju_gather(M, d->qM, d->mapM2C, m->nC); // LDlegacy: legacy LD matrix (size nM) mjtNum* LDlegacy = mj_stackAllocNum(d, m->nM); @@ -73,9 +73,9 @@ static void BM_solve(benchmark::State& state, SolveType type) { case SolveType::kCsr: mju_copy(d->qLD, M, m->nC); mj_factorI(d->qLD, d->qLDiagInv, m->nv, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); mj_solveLD(res, d->qLD, d->qLDiagInv, m->nv, 1, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); } } } diff --git a/test/benchmark/solveLD_benchmark_test.cc b/test/benchmark/solveLD_benchmark_test.cc index 6204425f..1ff28a4b 100644 --- a/test/benchmark/solveLD_benchmark_test.cc +++ b/test/benchmark/solveLD_benchmark_test.cc @@ -53,7 +53,8 @@ static void BM_solveLD(benchmark::State& state, bool featherstone, bool coil) { // scatter into legacy matrix mjtNum* LDlegacy = mj_stackAllocNum(d, m->nM); - mju_scatter(LDlegacy, d->qLD, d->mapM2M, m->nM); + mju_zero(LDlegacy, m->nM); + mju_scatter(LDlegacy, d->qLD, d->mapM2C, m->nC); // benchmark while (state.KeepRunningBatch(kNumBenchmarkSteps)) { @@ -63,7 +64,7 @@ static void BM_solveLD(benchmark::State& state, bool featherstone, bool coil) { mj_solveLD_legacy(m, res, 1, LDlegacy, d->qLDiagInv); } else { mj_solveLD(res, d->qLD, d->qLDiagInv, m->nv, 1, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); } } } diff --git a/test/engine/engine_core_smooth_test.cc b/test/engine/engine_core_smooth_test.cc index 7e4c113a..6adb8764 100644 --- a/test/engine/engine_core_smooth_test.cc +++ b/test/engine/engine_core_smooth_test.cc @@ -649,7 +649,7 @@ TEST_F(CoreSmoothTest, FactorI) { int nv = model->nv; vector Ldense(nv*nv, 0); mju_sparse2dense(Ldense.data(), data->qLD, nv, nv, - data->M_rownnz, data->M_rowadr, data->M_colind); + data->C_rownnz, data->C_rowadr, data->C_colind); for (int i=0; i < nv; i++) { // set diagonal to 1 Ldense[i*nv+i] = 1; @@ -658,7 +658,7 @@ TEST_F(CoreSmoothTest, FactorI) { // dense D matrix vector Ddense(nv*nv); mju_sparse2dense(Ddense.data(), data->qLD, nv, nv, - data->M_rownnz, data->M_rowadr, data->M_colind); + data->C_rownnz, data->C_rowadr, data->C_colind); for (int i=0; i < nv; i++) { for (int j=0; j < nv; j++) { // zero everything except the diagonal @@ -694,15 +694,16 @@ TEST_F(CoreSmoothTest, SolveLDs) { int nv = m->nv; int nM = m->nM; + int nC = m->nC; - // scatter M into LD: Legacy format - vector LDlegacy(nM); - mju_scatter(LDlegacy.data(), d->qLD, d->mapM2M, nM); + // copy M into LD: Legacy format + vector LDlegacy(nM, 0); + mju_scatter(LDlegacy.data(), d->qLD, d->mapM2C, nC); // compare LD and LDs densified matrices vector LDdense(nv*nv); mju_sparse2dense(LDdense.data(), d->qLD, nv, nv, - d->M_rownnz, d->M_rowadr, d->M_colind); + d->C_rownnz, d->C_rowadr, d->C_colind); vector LDdense2(nv*nv); mj_fullM(m, LDdense2.data(), LDlegacy.data()); @@ -721,7 +722,7 @@ TEST_F(CoreSmoothTest, SolveLDs) { mj_solveLD_legacy(m, vec.data(), 1, LDlegacy.data(), d->qLDiagInv); mj_solveLD(vec2.data(), d->qLD, d->qLDiagInv, nv, 1, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); // expect vectors to match up to floating point precision for (int i=0; i < nv; i++) { @@ -742,11 +743,10 @@ TEST_F(CoreSmoothTest, SolveLDmultipleVectors) { mj_forward(m, d); int nv = m->nv; - int nM = m->nM; - // scatter LD into LDlegacy: Legacy format - vector LDlegacy(nM); - mju_scatter(LDlegacy.data(), d->qLD, d->mapM2M, nM); + // copy LD into LDlegacy: Legacy format + vector LDlegacy(m->nM, 0); + mju_scatter(LDlegacy.data(), d->qLD, d->mapM2C, m->nC); // compare n LD and LDs vector solve int n = 3; @@ -757,7 +757,7 @@ TEST_F(CoreSmoothTest, SolveLDmultipleVectors) { mj_solveLD_legacy(m, vec.data(), n, LDlegacy.data(), d->qLDiagInv); mj_solveLD(vec2.data(), d->qLD, d->qLDiagInv, nv, n, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); // expect vectors to match up to floating point precision for (int i=0; i < nv*n; i++) { @@ -781,7 +781,7 @@ TEST_F(CoreSmoothTest, SolveM2) { int nv = m->nv; vector sqrtInvD(nv); for (int i=0; i < nv; i++) { - int diag = d->M_rowadr[i] + d->M_rownnz[i] - 1; + int diag = d->C_rowadr[i] + d->C_rownnz[i] - 1; sqrtInvD[i] = 1 / mju_sqrt(d->qLD[diag]); } @@ -795,7 +795,7 @@ TEST_F(CoreSmoothTest, SolveM2) { mj_solveM2(m, d, res.data(), vec.data(), sqrtInvD.data(), n); mj_solveLD(vec2.data(), d->qLD, d->qLDiagInv, nv, n, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); // expect equality of dot(v, M^-1 * v) and dot(M^-1/2 * v, M^-1/2 * v) for (int i=0; i < n; i++) { @@ -816,27 +816,25 @@ TEST_F(CoreSmoothTest, FactorIs) { mjData* d = mj_makeData(m); mj_forward(m, d); - int nM = m->nM, nv = m->nv; + int nC = m->nC, nM = m->nM, nv = m->nv; // copy qM into into qLDlegacy and factorize vector qLDlegacy(nM); mj_factorI_legacy(m, d, d->qM, qLDlegacy.data(), d->qLDiagInv); // copy qLDlegacy into qLDexpected: CSR format - vector qLDexpected(nM); - for (int i=0; i < nM; i++) { - qLDexpected[i] = qLDlegacy[d->mapM2M[i]]; - } + vector qLDexpected(nC); + mju_gather(qLDexpected.data(), qLDlegacy.data(), d->mapM2C, nC); - // gather qM into qLD: CSR format - vector qLD(nM); - mju_gather(qLD.data(), d->qM, d->mapM2M, nM); + // copy qM into qLD: CSR format + vector qLD(nC); + mju_gather(qLD.data(), d->qM, d->mapM2C, nC); vector qLDiagInvExpected(d->qLDiagInv, d->qLDiagInv + nv); vector qLDiagInv(nv, 0); mj_factorI(qLD.data(), qLDiagInv.data(), nv, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); // expect outputs to match to floating point precision EXPECT_THAT(qLD, Pointwise(DoubleNear(1e-12), qLDexpected)); @@ -846,12 +844,12 @@ TEST_F(CoreSmoothTest, FactorIs) { vector LDdense(nv*nv); mju_sparse2dense(LDdense.data(), qLDexpected.data(), nv, nv, - d->M_rownnz, d->M_rowadr, d->M_colind); - PrintMatrix(LDdense.data(), nv, nv, 2, "qLDexpected"); + d->C_rownnz, d->C_rowadr, d->C_colind); + PrintMatrix(LDdense.data(), nv, nv, 2); - mju_sparse2dense(LDdense.data(), qLD.data(), nv, nv, - d->M_rownnz, d->M_rowadr, d->M_colind); - PrintMatrix(LDdense.data(), nv, nv, 2, "qLD"); + mju_sparse2dense(LDdense.data(), qLDs.data(), nv, nv, + d->C_rownnz, d->C_rowadr, d->C_colind); + PrintMatrix(LDdense.data(), nv, nv, 2); */ mj_deleteData(d); diff --git a/test/engine/engine_derivative_test.cc b/test/engine/engine_derivative_test.cc index 027578e7..12b9ca88 100644 --- a/test/engine/engine_derivative_test.cc +++ b/test/engine/engine_derivative_test.cc @@ -436,7 +436,7 @@ static void LinearSystem(const mjModel* m, mjData* d, mjtNum* A, mjtNum* B) { Ac[nv*nv + i*nv + i] = -m->dof_damping[i]; } mj_solveLD(Ac, d->qH, d->qHDiagInv, nv, 2*nv, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); // A = [dt*Ac; Ac] mju_transpose(A, Ac, 2*nv, nv); @@ -464,7 +464,7 @@ static void LinearSystem(const mjModel* m, mjData* d, mjtNum* A, mjtNum* B) { mju_sparse2dense(Bc, d->actuator_moment, nu, nv, d->moment_rownnz, d->moment_rowadr, d->moment_colind); mj_solveLD(Bc, d->qH, d->qHDiagInv, nv, nu, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); mju_transpose(BcT, Bc, nu, nv); mju_scl(B, BcT, dt*dt, nu*nv); mju_scl(B+nu*nv, BcT, dt, nu*nv); From 6bc0bbf7df3058ebfe384022c880ff96239ea1d3 Mon Sep 17 00:00:00 2001 From: Baruch Tabanpour Date: Tue, 13 May 2025 10:51:49 -0700 Subject: [PATCH 119/191] internal change PiperOrigin-RevId: 758287280 Change-Id: I8b99a5d44910d5b2e7857b808b1696f244778a26 --- mjx/mujoco/mjx/_src/collision_driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mjx/mujoco/mjx/_src/collision_driver.py b/mjx/mujoco/mjx/_src/collision_driver.py index e618fe19..04a4a3d6 100644 --- a/mjx/mujoco/mjx/_src/collision_driver.py +++ b/mjx/mujoco/mjx/_src/collision_driver.py @@ -429,4 +429,4 @@ def collision(m: Model, d: Data) -> Data: contacts = sum([condim_groups[k] for k in sorted(condim_groups)], []) contact = jax.tree_util.tree_map(lambda *x: jp.concatenate(x), *contacts) - return d.replace(_impl=d._impl.replace(contact=contact)) # pytype: disable=attribute-error + return d.tree_replace({'_impl.contact': contact}) From 1e02d66c3d431aadb3e5fbe3c7bf5ac642ac9e90 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 13 May 2025 11:46:24 -0700 Subject: [PATCH 120/191] Remove `M` matrix structure from `mjData`. Introduced in March 2025 as a temporary measure, this is not considered a breaking change. PiperOrigin-RevId: 758311298 Change-Id: Ia08857c18b791f6a62a2d8d136b45fc075507b93 --- doc/includes/references.h | 4 ---- include/mujoco/mjdata.h | 4 ---- include/mujoco/mjxmacro.h | 4 ---- mjx/mujoco/mjx/_src/io.py | 4 ---- mjx/mujoco/mjx/_src/io_test.py | 2 +- mjx/mujoco/mjx/_src/types.py | 4 ---- python/mujoco/introspect/structs.py | 32 ---------------------------- src/engine/engine_io.c | 17 +-------------- src/engine/engine_print.c | 31 --------------------------- unity/Runtime/Bindings/MjBindings.cs | 4 ---- 10 files changed, 2 insertions(+), 104 deletions(-) diff --git a/doc/includes/references.h b/doc/includes/references.h index a2b820e5..ede6874f 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -312,10 +312,6 @@ struct mjData_ { int* B_rownnz; // body-dof: non-zeros in each row (nbody x 1) int* B_rowadr; // body-dof: address of each row in B_colind (nbody x 1) int* B_colind; // body-dof: column indices of non-zeros (nB x 1) - int* M_rownnz; // inertia: non-zeros in each row (nv x 1) - int* M_rowadr; // inertia: address of each row in M_colind (nv x 1) - int* M_colind; // inertia: column indices of non-zeros (nM x 1) - int* mapM2M; // index mapping from M (legacy) to M (CSR) (nM x 1) int* C_rownnz; // reduced dof-dof: non-zeros in each row (nv x 1) int* C_rowadr; // reduced dof-dof: address of each row in C_colind (nv x 1) int* C_colind; // reduced dof-dof: column indices of non-zeros (nC x 1) diff --git a/include/mujoco/mjdata.h b/include/mujoco/mjdata.h index 9ac187dc..de09cbce 100644 --- a/include/mujoco/mjdata.h +++ b/include/mujoco/mjdata.h @@ -340,10 +340,6 @@ struct mjData_ { int* B_rownnz; // body-dof: non-zeros in each row (nbody x 1) int* B_rowadr; // body-dof: address of each row in B_colind (nbody x 1) int* B_colind; // body-dof: column indices of non-zeros (nB x 1) - int* M_rownnz; // inertia: non-zeros in each row (nv x 1) - int* M_rowadr; // inertia: address of each row in M_colind (nv x 1) - int* M_colind; // inertia: column indices of non-zeros (nM x 1) - int* mapM2M; // index mapping from M (legacy) to M (CSR) (nM x 1) int* C_rownnz; // reduced dof-dof: non-zeros in each row (nv x 1) int* C_rowadr; // reduced dof-dof: address of each row in C_colind (nv x 1) int* C_colind; // reduced dof-dof: column indices of non-zeros (nC x 1) diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index 1f96199e..99cafaa2 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -675,10 +675,6 @@ X ( int, B_rownnz, nbody, 1 ) \ X ( int, B_rowadr, nbody, 1 ) \ X ( int, B_colind, nB, 1 ) \ - X ( int, M_rownnz, nv, 1 ) \ - X ( int, M_rowadr, nv, 1 ) \ - X ( int, M_colind, nM, 1 ) \ - X ( int, mapM2M, nM, 1 ) \ X ( int, C_rownnz, nv, 1 ) \ X ( int, C_rowadr, nv, 1 ) \ X ( int, C_colind, nC, 1 ) \ diff --git a/mjx/mujoco/mjx/_src/io.py b/mjx/mujoco/mjx/_src/io.py index 8fc28d83..664a7b8a 100644 --- a/mjx/mujoco/mjx/_src/io.py +++ b/mjx/mujoco/mjx/_src/io.py @@ -627,10 +627,6 @@ def _make_data_c( 'B_rownnz': (m.nbody, np.int32), 'B_rowadr': (m.nbody, np.int32), 'B_colind': (m.nB, np.int32), - 'M_rownnz': (m.nv, np.int32), - 'M_rowadr': (m.nv, np.int32), - 'M_colind': (m.nM, np.int32), - 'mapM2M': (m.nM, np.int32), 'C_rownnz': (m.nv, np.int32), 'C_rowadr': (m.nv, np.int32), 'C_colind': (m.nC, np.int32), diff --git a/mjx/mujoco/mjx/_src/io_test.py b/mjx/mujoco/mjx/_src/io_test.py index 41d812d3..78f81e3f 100644 --- a/mjx/mujoco/mjx/_src/io_test.py +++ b/mjx/mujoco/mjx/_src/io_test.py @@ -471,7 +471,7 @@ class DataIOTest(parameterized.TestCase): if backend_impl == 'c': # check fields specific to the C implementation - np.testing.assert_allclose(d_2.M_rownnz, d.M_rownnz) + np.testing.assert_allclose(d_2.bvh_active, d.bvh_active) def test_get_data_runs(self): xml = """ diff --git a/mjx/mujoco/mjx/_src/types.py b/mjx/mujoco/mjx/_src/types.py index c70d71a2..38dbcd71 100644 --- a/mjx/mujoco/mjx/_src/types.py +++ b/mjx/mujoco/mjx/_src/types.py @@ -984,10 +984,6 @@ class DataC(PyTreeNode): B_rownnz: jax.Array # pylint:disable=invalid-name B_rowadr: jax.Array # pylint:disable=invalid-name B_colind: jax.Array # pylint:disable=invalid-name - M_rownnz: jax.Array # pylint:disable=invalid-name - M_rowadr: jax.Array # pylint:disable=invalid-name - M_colind: jax.Array # pylint:disable=invalid-name - mapM2M: jax.Array # pylint:disable=invalid-name C_rownnz: jax.Array # pylint:disable=invalid-name C_rowadr: jax.Array # pylint:disable=invalid-name C_colind: jax.Array # pylint:disable=invalid-name diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index 500b2d40..bbfc3998 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -5568,38 +5568,6 @@ STRUCTS: Mapping[str, StructDecl] = dict([ doc='body-dof: column indices of non-zeros', array_extent=('nB',), ), - StructFieldDecl( - name='M_rownnz', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='inertia: non-zeros in each row', - array_extent=('nv',), - ), - StructFieldDecl( - name='M_rowadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='inertia: address of each row in M_colind', - array_extent=('nv',), - ), - StructFieldDecl( - name='M_colind', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='inertia: column indices of non-zeros', - array_extent=('nM',), - ), - StructFieldDecl( - name='mapM2M', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='index mapping from M (legacy) to M (CSR)', - array_extent=('nM',), - ), StructFieldDecl( name='C_rownnz', type=PointerType( diff --git a/src/engine/engine_io.c b/src/engine/engine_io.c index 31084c46..56e7d43f 100644 --- a/src/engine/engine_io.c +++ b/src/engine/engine_io.c @@ -1141,9 +1141,6 @@ static void copyM2Sparse(const mjModel* m, mjData* d, int* dst, const int* src, if (reduced && !upper) { rownnz = d->C_rownnz; rowadr = d->C_rowadr; - } else if (!reduced && !upper) { - rownnz = d->M_rownnz; - rowadr = d->M_rowadr; } else if (!reduced && upper) { rownnz = d->D_rownnz; rowadr = d->D_rowadr; @@ -1261,17 +1258,6 @@ static void makeDofDofmaps(const mjModel* m, mjData* d) { } } - // make mapM2M - for (int i=0; i < nM; i++) d->mapM2M[i] = -1; - copyM2Sparse(m, d, d->mapM2M, M, /*reduced=*/0, /*upper=*/0); - - // check that all indices are filled in - for (int i=0; i < nM; i++) { - if (d->mapM2M[i] < 0) { - mjERROR("unassigned index in mapM2M"); - } - } - mj_freeStack(d); } @@ -1989,8 +1975,7 @@ static void _resetData(const mjModel* m, mjData* d, unsigned char debug_value) { makeBSparse(m, d); checkDBSparse(m, d); - // make M, C - makeDofDofSparse(m, d, d->M_rownnz, d->M_rowadr, NULL, d->M_colind, /*reduced=*/0, /*upper=*/0); + // make C makeDofDofSparse(m, d, d->C_rownnz, d->C_rowadr, NULL, d->C_colind, /*reduced=*/1, /*upper=*/0); // make index mappings: mapM2D, mapD2M, mapM2C, mapM2M diff --git a/src/engine/engine_print.c b/src/engine/engine_print.c index bb9f4a9a..63010c82 100644 --- a/src/engine/engine_print.c +++ b/src/engine/engine_print.c @@ -1160,37 +1160,6 @@ void mj_printFormattedData(const mjModel* m, const mjData* d, const char* filena } fprintf(fp, "\n\n"); - // M sparse structure - mj_printSparsity("M: inertia matrix", m->nv, m->nv, d->M_rowadr, NULL, d->M_rownnz, - NULL, d->M_colind, fp); - - fprintf(fp, NAME_FORMAT, "M_rownnz"); - for (int i = 0; i < m->nv; i++) { - fprintf(fp, " %d", d->M_rownnz[i]); - } - fprintf(fp, "\n\n"); - - // M_rowadr - fprintf(fp, NAME_FORMAT, "M_rowadr"); - for (int i = 0; i < m->nv; i++) { - fprintf(fp, " %d", d->M_rowadr[i]); - } - fprintf(fp, "\n\n"); - - // M_colind - fprintf(fp, NAME_FORMAT, "M_colind"); - for (int i = 0; i < m->nM; i++) { - fprintf(fp, " %d", d->M_colind[i]); - } - fprintf(fp, "\n\n"); - - // mapM2M - fprintf(fp, NAME_FORMAT, "mapM2M"); - for (int i = 0; i < m->nM; i++) { - fprintf(fp, " %d", d->mapM2M[i]); - } - fprintf(fp, "\n\n"); - // C sparse structure mj_printSparsity("C: reduced dof-dof matrix", m->nv, m->nv, d->C_rowadr, NULL, d->C_rownnz, NULL, d->C_colind, fp); diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index c1042de6..21a96fde 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -4951,10 +4951,6 @@ public unsafe struct mjData_ { public int* B_rownnz; public int* B_rowadr; public int* B_colind; - public int* M_rownnz; - public int* M_rowadr; - public int* M_colind; - public int* mapM2M; public int* C_rownnz; public int* C_rowadr; public int* C_colind; From 79c74d7eae056a3fe04bb302af0bbf7ad62e6748 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 13 May 2025 13:27:01 -0700 Subject: [PATCH 121/191] Add `mjData.M`, not yet used. PiperOrigin-RevId: 758351796 Change-Id: I0786966b2f2c2e699db845cda6940a8821b23f49 --- doc/includes/references.h | 1 + include/mujoco/mjdata.h | 1 + include/mujoco/mjxmacro.h | 1 + mjx/mujoco/mjx/_src/io.py | 2 ++ mjx/mujoco/mjx/_src/types.py | 2 ++ python/mujoco/introspect/structs.py | 8 ++++++++ unity/Runtime/Bindings/MjBindings.cs | 1 + 7 files changed, 16 insertions(+) diff --git a/doc/includes/references.h b/doc/includes/references.h index ede6874f..eada1572 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -270,6 +270,7 @@ struct mjData_ { // computed by mj_fwdPosition/mj_crb mjtNum* crb; // com-based composite inertia and mass (nbody x 10) mjtNum* qM; // total inertia (sparse) (nM x 1) + mjtNum* M; // total inertia (compressed sparse row) (nC x 1) // computed by mj_fwdPosition/mj_factorM mjtNum* qLD; // L'*D*L factorization of M (sparse) (nC x 1) diff --git a/include/mujoco/mjdata.h b/include/mujoco/mjdata.h index de09cbce..3d7514a6 100644 --- a/include/mujoco/mjdata.h +++ b/include/mujoco/mjdata.h @@ -298,6 +298,7 @@ struct mjData_ { // computed by mj_fwdPosition/mj_crb mjtNum* crb; // com-based composite inertia and mass (nbody x 10) mjtNum* qM; // total inertia (sparse) (nM x 1) + mjtNum* M; // total inertia (compressed sparse row) (nC x 1) // computed by mj_fwdPosition/mj_factorM mjtNum* qLD; // L'*D*L factorization of M (sparse) (nC x 1) diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index 99cafaa2..aa55b3d7 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -653,6 +653,7 @@ X ( mjtNum, actuator_moment, nJmom, 1 ) \ X ( mjtNum, crb, nbody, 10 ) \ X ( mjtNum, qM, nM, 1 ) \ + X ( mjtNum, M, nC, 1 ) \ X ( mjtNum, qLD, nC, 1 ) \ X ( mjtNum, qLDiagInv, nv, 1 ) \ XMJV( mjtNum, bvh_aabb_dyn, nbvhdynamic, 6 ) \ diff --git a/mjx/mujoco/mjx/_src/io.py b/mjx/mujoco/mjx/_src/io.py index 664a7b8a..66919152 100644 --- a/mjx/mujoco/mjx/_src/io.py +++ b/mjx/mujoco/mjx/_src/io.py @@ -509,6 +509,7 @@ def _make_data_jax( 'actuator_moment': (m.nu, m.nv, float_), 'crb': (m.nbody, 10, float_), 'qM': (m.nM, float_) if support.is_sparse(m) else (m.nv, m.nv, float_), + 'M': (m.nC, float_), 'qLD': (m.nC, float_) if support.is_sparse(m) else (m.nv, m.nv, float_), 'qLDiagInv': (m.nv, float_) if support.is_sparse(m) else (0, float_), 'ten_velocity': (m.ntendon, float_), @@ -618,6 +619,7 @@ def _make_data_c( 'flexedge_velocity': (nflexedge, float_), 'crb': (m.nbody, 10, float_), 'qM': (m.nM, float_), + 'M': (m.nC, float_), 'qLD': (m.nC, float_), 'qH': (m.nC, float_), 'qHDiagInv': (m.nv, float_), diff --git a/mjx/mujoco/mjx/_src/types.py b/mjx/mujoco/mjx/_src/types.py index 38dbcd71..22211378 100644 --- a/mjx/mujoco/mjx/_src/types.py +++ b/mjx/mujoco/mjx/_src/types.py @@ -970,6 +970,7 @@ class DataC(PyTreeNode): actuator_moment: jax.Array crb: jax.Array qM: jax.Array # pylint:disable=invalid-name + M: jax.Array # pylint:disable=invalid-name qLD: jax.Array # pylint:disable=invalid-name qLDiagInv: jax.Array # pylint:disable=invalid-name bvh_aabb_dyn: jax.Array @@ -1037,6 +1038,7 @@ class DataJAX(PyTreeNode): actuator_moment: jax.Array crb: jax.Array qM: jax.Array # pylint:disable=invalid-name + M: jax.Array # pylint:disable=invalid-name qLD: jax.Array # pylint:disable=invalid-name qLDiagInv: jax.Array # pylint:disable=invalid-name ten_velocity: jax.Array diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index bbfc3998..e9e6f801 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -5392,6 +5392,14 @@ STRUCTS: Mapping[str, StructDecl] = dict([ doc='total inertia (sparse)', array_extent=('nM',), ), + StructFieldDecl( + name='M', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='total inertia (compressed sparse row)', + array_extent=('nC',), + ), StructFieldDecl( name='qLD', type=PointerType( diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index 21a96fde..d26592a9 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -4929,6 +4929,7 @@ public unsafe struct mjData_ { public double* actuator_moment; public double* crb; public double* qM; + public double* M; public double* qLD; public double* qLDiagInv; public double* bvh_aabb_dyn; From 418658973880c883c47cf4b6b153fc7e7b3c4dd3 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 13 May 2025 14:21:41 -0700 Subject: [PATCH 122/191] Use `mjData.M` where appropriate PiperOrigin-RevId: 758374391 Change-Id: I9de7af7be8e41b5c300d0a04ea99082b4cefdec6 --- src/engine/engine_core_smooth.c | 5 ++-- src/engine/engine_forward.c | 2 +- src/engine/engine_island.c | 6 +---- src/engine/engine_print.c | 4 +-- src/engine/engine_solver.c | 48 ++++++++++++--------------------- 5 files changed, 23 insertions(+), 42 deletions(-) diff --git a/src/engine/engine_core_smooth.c b/src/engine/engine_core_smooth.c index 3b5601a2..6e5b03f7 100644 --- a/src/engine/engine_core_smooth.c +++ b/src/engine/engine_core_smooth.c @@ -1579,6 +1579,7 @@ void mj_makeM(const mjModel* m, mjData* d) { TM_START; mj_crb(m, d); mj_tendonArmature(m, d); + mju_gather(d->M, d->qM, d->mapM2C, m->nC); TM_END(mjTIMER_POS_INERTIA); } @@ -1651,9 +1652,7 @@ void mj_factorI_legacy(const mjModel* m, mjData* d, const mjtNum* M, mjtNum* qLD // sparse L'*D*L factorizaton of the inertia matrix M, assumed spd void mj_factorM(const mjModel* m, mjData* d) { TM_START; - - // gather LD <- M (legacy to CSR) and factorize in-place - mju_gather(d->qLD, d->qM, d->mapM2C, m->nC); + mju_copy(d->qLD, d->M, m->nC); mj_factorI(d->qLD, d->qLDiagInv, m->nv, d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); TM_ADD(mjTIMER_POS_INERTIA); } diff --git a/src/engine/engine_forward.c b/src/engine/engine_forward.c index 0707efee..6b23111f 100644 --- a/src/engine/engine_forward.c +++ b/src/engine/engine_forward.c @@ -861,7 +861,7 @@ void mj_EulerSkip(const mjModel* m, mjData* d, int skipfactor) { else { if (!skipfactor) { // qH = M + h*diag(B) - mju_gather(d->qH, d->qM, d->mapM2C, nC); + mju_copy(d->qH, d->M, nC); for (int i=0; i < nv; i++) { d->qH[d->C_rowadr[i] + d->C_rownnz[i] - 1] += m->opt.timestep * m->dof_damping[i]; } diff --git a/src/engine/engine_island.c b/src/engine/engine_island.c index 5d04118e..4d162972 100644 --- a/src/engine/engine_island.c +++ b/src/engine/engine_island.c @@ -535,17 +535,13 @@ void mj_island(const mjModel* m, mjData* d) { d->island_dofadr[i] = d->map_idof2dof[d->island_idofadr[i]]; } - // local CSR copy of qM - mjtNum* qM = mjSTACKALLOC(d, m->nC, mjtNum); - mju_gather(qM, d->qM, d->mapM2C, m->nC); - // inertia: block-diagonalize both iLD <- qLD and iM <- qM mju_blockDiagSparse(d->iLD, d->iM_rownnz, d->iM_rowadr, d->iM_colind, d->qLD, d->C_rownnz, d->C_rowadr, d->C_colind, nidof, nisland, d->map_idof2dof, d->map_dof2idof, d->island_idofadr, d->island_idofadr, - d->iM, qM); + d->iM, d->M); mju_gather(d->iLDiagInv, d->qLDiagInv, d->map_idof2dof, nidof); // compute iM_diagnum (dof_simplenum per island) diff --git a/src/engine/engine_print.c b/src/engine/engine_print.c index 63010c82..ef02d1ed 100644 --- a/src/engine/engine_print.c +++ b/src/engine/engine_print.c @@ -1123,9 +1123,9 @@ void mj_printFormattedData(const mjModel* m, const mjData* d, const char* filena printSparse("ACTUATOR_MOMENT", d->actuator_moment, m->nu, d->moment_rownnz, d->moment_rowadr, d->moment_colind, fp, float_format); printArray("CRB", m->nbody, 10, d->crb, fp, float_format); - printInertia("QM", d->qM, m, fp, float_format); - + printSparse("M", d->M, m->nv, d->C_rownnz, + d->C_rowadr, d->C_colind, fp, float_format); printSparse("QLD", d->qLD, m->nv, d->C_rownnz, d->C_rowadr, d->C_colind, fp, float_format); printArray("QLDIAGINV", m->nv, 1, d->qLDiagInv, fp, float_format); diff --git a/src/engine/engine_solver.c b/src/engine/engine_solver.c index c2f15709..b6a3fae3 100644 --- a/src/engine/engine_solver.c +++ b/src/engine/engine_solver.c @@ -787,9 +787,7 @@ struct _mjCGContext { const int* M_rowadr; const int* M_diagnum; const int* M_colind; - const int* dof_Madr; - const int* dof_parentid; - const mjtNum* qM; + const mjtNum* M; const mjtNum* qLD; const mjtNum* qLDiagInv; @@ -827,7 +825,6 @@ struct _mjCGContext { // Newton arrays, known-size (CGallocate) mjtNum* D; // constraint inertia (nefc x 1) - mjtNum* C; // reduced sparse inertia matrix (nC x 1) int* H_rowadr; // Hessian row addresses (nv x 1) int* H_rownnz; // Hessian row nonzeros (nv x 1) int* H_lowernnz; // Hessian lower triangle row nonzeros (nv x 1) @@ -885,9 +882,7 @@ static void CGpointers(const mjModel* m, const mjData* d, mjCGContext* ctx, int ctx->M_rowadr = d->C_rowadr; ctx->M_diagnum = m->dof_simplenum; ctx->M_colind = d->C_colind; - ctx->dof_Madr = m->dof_Madr; - ctx->dof_parentid = m->dof_parentid; - ctx->qM = d->qM; + ctx->M = d->M; ctx->qLD = d->qLD; ctx->qLDiagInv = d->qLDiagInv; @@ -936,7 +931,7 @@ static void CGpointers(const mjModel* m, const mjData* d, mjCGContext* ctx, int ctx->M_rowadr = d->iM_rowadr + idofadr; ctx->M_diagnum = d->iM_diagnum + idofadr; ctx->M_colind = d->iM_colind; - ctx->qM = d->iM; + ctx->M = d->iM; ctx->qLD = d->iLD; ctx->qLDiagInv = d->iLDiagInv + idofadr; @@ -1001,7 +996,6 @@ static void CGallocate(const mjModel* m, mjData* d, mjCGContext* ctx, int island // sparse Newton only if (mj_isSparse(m)) { - ctx->C = mjSTACKALLOC(d, m->nC, mjtNum); ctx->H_rowadr = mjSTACKALLOC(d, nv, int); ctx->H_rownnz = mjSTACKALLOC(d, nv, int); ctx->H_lowernnz = mjSTACKALLOC(d, nv, int); @@ -1359,14 +1353,9 @@ static mjtNum CGsearch(mjCGContext* ctx, mjtNum tolerance, mjtNum ls_iterations) mjtNum gtol = tolerance * snorm / ctx->scale; mjtNum slopescl = ctx->scale / snorm; - // compute Mv = M * v (island or monolithic) - if (ctx->island >= 0) { - mju_mulSymVecSparse(ctx->Mv, ctx->qM, ctx->search, nv, - ctx->M_rownnz, ctx->M_rowadr, ctx->M_diagnum, ctx->M_colind); - } else { - mj_mulM_impl(ctx->Mv, ctx->search, nv, ctx->qM, - ctx->dof_Madr, ctx->dof_parentid, ctx->M_diagnum); - } + // compute Mv = M * v + mju_mulSymVecSparse(ctx->Mv, ctx->M, ctx->search, nv, + ctx->M_rownnz, ctx->M_rowadr, ctx->M_diagnum, ctx->M_colind); // compute Jv = J * search (dense or sparse) if (!ctx->J_rowadr) { @@ -1545,9 +1534,6 @@ static void MakeHessian(const mjModel* m, mjData* d, mjCGContext* ctx) { // sparse if (mj_isSparse(m)) { - // gather C <- qM (legacy to CSR) - mju_gather(ctx->C, d->qM, d->mapM2C, m->nC); - // initialize Hessian rowadr, rownnz mju_sqrMatTDSparseCount(ctx->H_rownnz, ctx->H_rowadr, nv, d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind, @@ -1577,7 +1563,7 @@ static void MakeHessian(const mjModel* m, mjData* d, mjCGContext* ctx) { // add mass matrix: H = J'*D*J + C mj_addMSparse(m, d, ctx->H, ctx->H_rownnz, ctx->H_rowadr, ctx->H_colind, - ctx->C, d->C_rownnz, d->C_rowadr, d->C_colind); + ctx->M, d->C_rownnz, d->C_rowadr, d->C_colind); // transiently compute H'; mju_cholFactorNNZ is memory-contiguous in upper triangle layout mj_markStack(d); @@ -1637,7 +1623,9 @@ static void MakeHessian(const mjModel* m, mjData* d, mjCGContext* ctx) { // compute H = M + J'*D*J mju_sqrMatTD(ctx->L, d->efc_J, ctx->D, nefc, nv); - mj_addMDense(m, d, ctx->L); + mju_addToSymSparse(ctx->L, ctx->M, ctx->nv, + ctx->M_rownnz, ctx->M_rowadr, ctx->M_colind, + /*flg_upper=*/ 1); } } @@ -1671,7 +1659,7 @@ static void FactorizeHessian(const mjModel* m, mjData* d, mjCGContext* ctx, // add mass matrix: H = J'*D*J + C mj_addMSparse(m, d, ctx->H, ctx->H_rownnz, ctx->H_rowadr, ctx->H_colind, - ctx->C, d->C_rownnz, d->C_rowadr, d->C_colind); + ctx->M, d->C_rownnz, d->C_rowadr, d->C_colind); } // copy H lower-triangle into L, fill-in already accounted for @@ -1702,7 +1690,9 @@ static void FactorizeHessian(const mjModel* m, mjData* d, mjCGContext* ctx, // maybe compute H = M + J'*D*J if (flg_recompute) { mju_sqrMatTD(ctx->L, d->efc_J, ctx->D, nefc, nv); - mj_addMDense(m, d, ctx->L); + mju_addToSymSparse(ctx->L, ctx->M, ctx->nv, + ctx->M_rownnz, ctx->M_rowadr, ctx->M_colind, + /*flg_upper=*/ 1); } // factorize H @@ -1891,13 +1881,9 @@ static void mj_solCGNewton(const mjModel* m, mjData* d, int island, int maxiter, int* oldstate = mjSTACKALLOC(d, nefc, int); // compute Ma = M * qacc (island or monolithic) - if (island >= 0) { - mju_mulSymVecSparse(ctx.Ma, ctx.qM, ctx.qacc, nv, - ctx.M_rownnz, ctx.M_rowadr, ctx.M_diagnum, ctx.M_colind); - } else { - mj_mulM_impl(ctx.Ma, ctx.qacc, nv, ctx.qM, - ctx.dof_Madr, ctx.dof_parentid, ctx.M_diagnum); - } + mju_mulSymVecSparse(ctx.Ma, ctx.M, ctx.qacc, nv, + ctx.M_rownnz, ctx.M_rowadr, ctx.M_diagnum, ctx.M_colind); + // compute Jaref = J * qacc - aref (dense or sparse) if (!ctx.J_rownnz) { From 23865c7491b10691fce94d658bf37905cbdf49c4 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Tue, 13 May 2025 16:09:28 -0700 Subject: [PATCH 123/191] Skip cow.xml from xml_native_writer_test. Including it caused tests to become flaky. PiperOrigin-RevId: 758414305 Change-Id: I6a5b6b20df7faf736bb3dd8610019b9c9a23d633 --- test/xml/xml_native_writer_test.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/test/xml/xml_native_writer_test.cc b/test/xml/xml_native_writer_test.cc index d3a2b59b..c938b465 100644 --- a/test/xml/xml_native_writer_test.cc +++ b/test/xml/xml_native_writer_test.cc @@ -1383,6 +1383,7 @@ TEST_F(XMLWriterTest, WriteReadCompare) { // if file is meant to fail, skip it if (absl::StrContains(p.path().string(), "100_humanoids") || absl::StrContains(p.path().string(), "malformed_") || + absl::StrContains(p.path().string(), "cow") || absl::StrContains(p.path().string(), "gmsh_") || absl::StrContains(p.path().string(), "shark_") || absl::StrContains(p.path().string(), "frameless_contact_hfield") || From 2386dfd7da41dc4f97e5d3d00bef4f130047c2c3 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Wed, 14 May 2025 00:45:05 -0700 Subject: [PATCH 124/191] Add touch sensor for estimating the surface contact stresses from the SDF. PiperOrigin-RevId: 758556339 Change-Id: Ib56d76fab084b530fd68d61a29294cb6f1d05f3f --- model/plugin/sensor/touch_stress.xml | 64 +++ plugin/sensor/CMakeLists.txt | 4 +- plugin/sensor/README.md | 66 ++- plugin/sensor/images/normal.png | Bin 0 -> 132370 bytes plugin/sensor/images/tangential1.png | Bin 0 -> 91230 bytes plugin/sensor/images/tangential2.png | Bin 0 -> 94863 bytes plugin/sensor/{sensor.cc => register.cc} | 6 +- plugin/sensor/touch_stress.cc | 557 +++++++++++++++++++++++ plugin/sensor/touch_stress.h | 80 ++++ src/engine/engine_collision_sdf.c | 8 +- test/engine/engine_plugin_test.cc | 2 +- 11 files changed, 779 insertions(+), 8 deletions(-) create mode 100644 model/plugin/sensor/touch_stress.xml create mode 100644 plugin/sensor/images/normal.png create mode 100644 plugin/sensor/images/tangential1.png create mode 100644 plugin/sensor/images/tangential2.png rename plugin/sensor/{sensor.cc => register.cc} (86%) create mode 100644 plugin/sensor/touch_stress.cc create mode 100644 plugin/sensor/touch_stress.h diff --git a/model/plugin/sensor/touch_stress.xml b/model/plugin/sensor/touch_stress.xml new file mode 100644 index 00000000..025777a4 --- /dev/null +++ b/model/plugin/sensor/touch_stress.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/sensor/CMakeLists.txt b/plugin/sensor/CMakeLists.txt index 4dac6d0a..d5fde827 100644 --- a/plugin/sensor/CMakeLists.txt +++ b/plugin/sensor/CMakeLists.txt @@ -18,9 +18,11 @@ set(MUJOCO_SENSOR_INCLUDE ) set(MUJOCO_SENSOR_SRCS - sensor.cc + register.cc touch_grid.h touch_grid.cc + touch_stress.h + touch_stress.cc ) add_library(sensor SHARED) diff --git a/plugin/sensor/README.md b/plugin/sensor/README.md index e6b4e9d3..1b72db7d 100644 --- a/plugin/sensor/README.md +++ b/plugin/sensor/README.md @@ -9,6 +9,9 @@ plugins](https://mujoco.readthedocs.io/en/latest/programming/extension.html#engi - [Illustration of foveal deformation](#illustration-of-foveal-deformation) - [Illustration combining resolution, fields-of-view and foveal deformation](#illustration-combining-resolution-fields-of-view-and-foveal-deformation) +- [Touch Stress](#touch-stress) + - [Example model with analytical SDF](#example-model-with-analytical-sdf) + ## [Touch Grid](touch_grid.h) This sensor aggregates contact forces into "taxels": a rectangular array of pixel-like elements. @@ -23,7 +26,7 @@ The output of the sensor is a stack of 1 to 6 "touch images" corresponding to fo and torques in the frame of the sensor. Forces and torques are in the in [z, x, y] order, corresponding to the ordering in contact frames: [normal, tangent, tangent] and [torsional, rolling, rolling]. Each "taxel" corresponds to an angular bin -in spherical coordinates, and aggregates all the forces occuring inside this bin, which occur +in spherical coordinates, and aggregates all the forces occurring inside this bin, which occur between the body in which the sensor's site is defined and any other body. The sensor is parametrized by 6 numbers: @@ -84,3 +87,64 @@ See [touch_grid.xml](../../model/plugin/sensor/touch_grid.xml) to play with the ### Illustration combining resolution, fields-of-view and foveal deformation [![touch grid illustration](https://img.youtube.com/vi/YScjmR8LwQI/0.jpg)](https://www.youtube.com/watch?v=YScjmR8LwQI) + +## [Touch Stress](touch_stress.h) + +This sensor is based on similar concepts and parametrization as the `touch_grid`, +while overcoming some of its limitations. In particular, the `touch_grid` can +only provide sparse information, depending on the number of contact points +generated. The `touch_stress` sensor can instead generate a high-resolution +touch image. In order to do this, it requires a signed distance function (SDF) +of the object that is in contact with the sensor. This is handled internally for +primitives or it must be declared explicitly in the model using SDF plugins. + +There is one important difference with respect to the `touch_grid`: in this case, +the force is computed in the local taxel frame and not in the frame of the sensor. +This allows for a more intuitive interpretation of normal and tangential stresses, +as shown in the images below. + +Note that in this case, the absolute values of the stresses reported by the +sensor are unrelated to the contact forces. They are purely based on geometric +and kinematic considerations, i.e. the SDF for the normal stress and the sliding +velocity for the tangential contributions. + +### Example model with analytical SDF + +```xml + + + + + + + + + + + + + + + ... + + + + + + ... + + + + + ... + + + + +``` +The images below show a static sphere over a gear described by an analytic SDF +and the same sphere dragged along the x and y axes. + + + + diff --git a/plugin/sensor/images/normal.png b/plugin/sensor/images/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..f40dd0c380cbb96833b9a613c7837b543b596c3c GIT binary patch literal 132370 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%0D6mi{16fmTE85_L1z(Vx_VcR$ZJ~idi(NMzA)K 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 literal 0 HcmV?d00001 diff --git a/plugin/sensor/images/tangential1.png b/plugin/sensor/images/tangential1.png new file mode 100644 index 0000000000000000000000000000000000000000..50d3c9624c3f36a613d28a2154b78ff5bcbca553 GIT binary patch literal 91230 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

Hl0bnr2$+|IidsB{g# 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 literal 0 HcmV?d00001 diff --git a/plugin/sensor/images/tangential2.png b/plugin/sensor/images/tangential2.png new file mode 100644 index 0000000000000000000000000000000000000000..68018d8e84fb1ba47470662a15e81b3b4744d261 GIT binary patch literal 94863 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

&n{{<`ojp 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

%D35RbBxfJx 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 #include "touch_grid.h" +#include "touch_stress.h" namespace mujoco::plugin::sensor { -mjPLUGIN_LIB_INIT { TouchGrid::RegisterPlugin(); } +mjPLUGIN_LIB_INIT { + TouchGrid::RegisterPlugin(); + TouchStress::RegisterPlugin(); +} } // namespace mujoco::plugin::sensor diff --git a/plugin/sensor/touch_stress.cc b/plugin/sensor/touch_stress.cc new file mode 100644 index 00000000..ba7b1d4c --- /dev/null +++ b/plugin/sensor/touch_stress.cc @@ -0,0 +1,557 @@ +// Copyright 2023 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. + +#include "touch_stress.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace mujoco::plugin::sensor { + +namespace { + +// Checks that a plugin config attribute exists. +bool CheckAttr(const std::string& input) { + char* end; + std::string value = input; + value.erase(std::remove_if(value.begin(), value.end(), isspace), value.end()); + strtod(value.c_str(), &end); + return end == value.data() + value.size(); +} + +// Converts a string into a numeric vector +template +void ReadVector(std::vector& output, const std::string& input) { + std::stringstream ss(input); + std::string item; + char delim = ' '; + while (getline(ss, item, delim)) { + CheckAttr(item); + output.push_back(strtod(item.c_str(), nullptr)); + } +} + +// Evenly spaced numbers over a specified interval. +void LinSpace(mjtNum lower, mjtNum upper, int n, mjtNum array[]) { + mjtNum increment = n > 1 ? (upper - lower) / (n - 1) : 0; + for (int i = 0; i < n; ++i) { + *array = lower; + ++array; + lower += increment; + } +} + +// Parametrized linear/quintic interpolated nonlinearity. +mjtNum Fovea(mjtNum x, mjtNum gamma) { + // Quick return. + if (!gamma) return x; + + // Foveal deformation. + mjtNum g = mjMAX(0, mjMIN(1, gamma)); + return g*mju_pow(x, 5) + (1 - g)*x; +} + +// Make bin edges. +void BinEdges(mjtNum* x_edges, mjtNum* y_edges, int size[2], mjtNum fov[2], + mjtNum gamma) { + // Make unit bin edges. + LinSpace(-1, 1, size[0] + 1, x_edges); + LinSpace(-1, 1, size[1] + 1, y_edges); + + // Apply foveal deformation. + for (int i = 0; i < size[0] + 1; i++) { + x_edges[i] = Fovea(x_edges[i], gamma); + } + for (int i = 0; i < size[1] + 1; i++) { + y_edges[i] = Fovea(y_edges[i], gamma); + } + + // Scale by field-of-view. + mju_scl(x_edges, x_edges, fov[0]*mjPI / 180, size[0] + 1); + mju_scl(y_edges, y_edges, fov[1]*mjPI / 180, size[1] + 1); +} + +// Permute 3-vector from 0,1,2 to 2,0,1. +static void xyz2zxy(mjtNum* x) { + mjtNum z = x[2]; + x[2] = x[1]; + x[1] = x[0]; + x[0] = z; +} + +// Transform spherical (azimuth, elevation, radius) to Cartesian (x,y,z). +void SphericalToCartesian(const mjtNum aer[3], mjtNum xyz[3]) { + mjtNum a = aer[0], e = aer[1], r = aer[2]; + xyz[0] = r * mju_cos(e) * mju_sin(a); + xyz[1] = r * mju_sin(e); + xyz[2] = -r * mju_cos(e) * mju_cos(a); +} + +// Tangent frame in Cartesian coordinates. +void TangentFrame(const mjtNum aer[3], mjtNum mat[9]) { + mjtNum a = aer[0], e = aer[1], r = aer[2]; + mjtNum ta[3] = {r * mju_cos(e) * mju_cos(a), 0, + r * mju_cos(e) * mju_sin(a)}; + mjtNum te[3] = {-r * mju_sin(e) * mju_sin(a), r * mju_cos(e), + r * mju_sin(e) * mju_cos(a)}; + mju_normalize3(ta); + mju_normalize3(te); + mju_copy3(mat, ta); + mju_copy3(mat+3, te); + mju_cross(mat+6, te, ta); +} + +} // namespace + +// Creates a TouchStress instance if all config attributes are defined and +// within their allowed bounds. +TouchStress* TouchStress::Create(const mjModel* m, mjData* d, + int instance) { + if (CheckAttr(std::string(mj_getPluginConfig(m, instance, "gamma"))) && + CheckAttr(std::string(mj_getPluginConfig(m, instance, "nchannel")))) { + // nchannel + int nchannel = strtod(mj_getPluginConfig(m, instance, "nchannel"), nullptr); + if (!nchannel) nchannel = 1; + if (nchannel < 1 || nchannel > 3) { + mju_error("nchannel must be between 1 and 3"); + return nullptr; + } + + // size + std::vector size; + std::string size_str = std::string(mj_getPluginConfig(m, instance, "size")); + ReadVector(size, size_str.c_str()); + if (size.size()!= 2) { + mju_error("Both horizontal and vertical resolutions must be specified"); + return nullptr; + } + if (size[0] <= 0 || size[1] <= 0) { + mju_error("Horizontal and vertical resolutions must be positive"); + return nullptr; + } + + // field of view + std::vector fov; + std::string fov_str = std::string(mj_getPluginConfig(m, instance, "fov")); + ReadVector(fov, fov_str.c_str()); + if (fov.size()!= 2) { + mju_error( + "Both horizontal and vertical fields of view must be specified"); + return nullptr; + } + if (fov[0] <= 0 || fov[0] > 180) { + mju_error("`fov[0]` must be a float between (0, 180] degrees"); + return nullptr; + } + if (fov[1] <= 0 || fov[1] > 90) { + mju_error("`fov[1]` must be a float between (0, 90] degrees"); + return nullptr; + } + + // gamma + mjtNum gamma = strtod(mj_getPluginConfig(m, instance, "gamma"), nullptr); + if (gamma < 0 || gamma > 1) { + mju_error("`gamma` must be a nonnegative float between [0, 1]"); + return nullptr; + } + + return new TouchStress(m, d, instance, nchannel, size.data(), fov.data(), + gamma); + } else { + mju_error("Invalid or missing parameters in touch_grid sensor plugin"); + return nullptr; + } +} + +TouchStress::TouchStress(const mjModel* m, mjData* d, int instance, + int nchannel, int size[2], mjtNum fov[2], mjtNum gamma) + : nchannel_(nchannel), + size_{size[0], size[1]}, + fov_{fov[0], fov[1]}, + gamma_(gamma) { + // Make sure sensor is attached to a site. + for (int i = 0; i < m->nsensor; ++i) { + if (m->sensor_type[i] == mjSENS_PLUGIN && m->sensor_plugin[i] == instance) { + if (m->sensor_objtype[i] != mjOBJ_SITE) { + mju_error("Touch Grid sensor must be attached to a site"); + } + } + } + + // Get sensor id. + for (id_ = 0; id_ < m->nsensor; ++id_) { + if (m->sensor_type[id_] == mjSENS_PLUGIN && + m->sensor_plugin[id_] == instance) { + break; + } + } + + // Get parent weld id. + int site_id = m->sensor_objid[id_]; + int parent_body = m->body_weldid[m->site_bodyid[site_id]]; + parent_weld_ = m->body_weldid[parent_body]; + + // Get geom id. + if (m->body_geomnum[parent_body] != 1) { + mju_error("Touch sensor must be attached to a body with exactly one geom"); + } + geom_id_ = m->body_geomadr[parent_body]; + + // Create bin edges. + x_edges_.assign(size[0] + 1, 0); + y_edges_.assign(size[1] + 1, 0); + BinEdges(x_edges_.data(), y_edges_.data(), size_, fov_, gamma_); + dist_.resize(size[0]*size[1], 0); + pos_.resize(3*size[0]*size[1], 0); + mat_.resize(9*size[0]*size[1], 0); + + // Precompute spherical coordinates. + for (int i = 0; i < size[0]; i++) { + for (int j = 0; j < size[1]; j++) { + mjtNum aer[3]; + aer[0] = 0.5*(x_edges_[i+1]+x_edges_[i]); + aer[1] = 0.5*(y_edges_[j+1]+y_edges_[j]); + aer[2] = m->geom_size[3*geom_id_]; + SphericalToCartesian(aer, pos_.data() + 3 * (i * size[1] + j)); + dist_[i*size[1]+j] = mju_abs(aer[2]); + TangentFrame(aer, mat_.data() + 9 * (i * size[1] + j)); + } + } +} + +void TouchStress::Reset(const mjModel* m, int instance) {} + +void TouchStress::Compute(const mjModel* m, mjData* d, int instance) { + mj_markStack(d); + + // Clear sensordata and distance matrix. + mjtNum* sensordata = d->sensordata + m->sensor_adr[id_]; + mju_zero(sensordata, m->sensor_dim[id_]); + + // Get site id. + int site_id = m->sensor_objid[id_]; + + // Count contacts and get contact geom ids. + std::unordered_set contact_geom_ids; + for (int i = 0; i < d->ncon; i++) { + int body1 = m->body_weldid[m->geom_bodyid[d->contact[i].geom1]]; + int body2 = m->body_weldid[m->geom_bodyid[d->contact[i].geom2]]; + if (body1 == parent_weld_) { + contact_geom_ids.insert(d->contact[i].geom2); + } + if (body2 == parent_weld_) { + contact_geom_ids.insert(d->contact[i].geom1); + } + } + + // No contacts, return. + if (contact_geom_ids.empty()) { + mj_freeStack(d); + return; + } + + // All of the quadrature points are contact points. + int ncon = size_[0]*size_[1]; + + // Get site frame. + mjtNum* site_pos = d->site_xpos + 3*site_id; + mjtNum* site_mat = d->site_xmat + 9*site_id; + + // Allocate contact forces and positions. + mjtNum* forces = mj_stackAllocNum(d, ncon*3); + mjtNum* forcesT = mj_stackAllocNum(d, ncon*3); + + // Iterate over colliding geoms. + for (auto geom : contact_geom_ids) { + int body = m->geom_bodyid[geom]; + + // Get sdf plugin of the geoms. + int sdf_instance[2] = {-1, geom_id_}; + mjtGeom geomtype[2] = {mjGEOM_SDF, mjGEOM_SPHERE}; + const mjpPlugin* sdf_ptr[2] = {NULL, NULL}; + if (m->geom_type[geom] == mjGEOM_SDF) { + sdf_instance[0] = m->geom_plugin[geom]; + sdf_ptr[0] = mjc_getSDF(m, geom); + } else { + sdf_instance[0] = geom; + geomtype[0] = (mjtGeom)m->geom_type[geom]; + } + + // Set SDF parameters. + mjSDF geom_sdf; + geom_sdf.id = &sdf_instance[0]; + geom_sdf.type = mjSDFTYPE_SINGLE; + geom_sdf.plugin = &sdf_ptr[0]; + geom_sdf.geomtype = &geomtype[0]; + + mjSDF sensor_sdf; + sensor_sdf.id = &sdf_instance[1]; + sensor_sdf.type = mjSDFTYPE_SINGLE; + sensor_sdf.plugin = &sdf_ptr[1]; + sensor_sdf.geomtype = &geomtype[1]; + + // Get forces and positions in spherical coordinates. + int node = 0; + for (int j = 0; j < size_[1]; j++) { + for (int i = 0; i < size_[0]; i++) { + // Position in site frame. + mjtNum* pos = pos_.data() + 3*(i*size_[1] + j); + mjtNum* mat = mat_.data() + 9*(i*size_[1] + j); + + // Position in global frame. + mjtNum xpos[3]; + mju_mulMatVec3(xpos, site_mat, pos); + mju_addTo3(xpos, site_pos); + + // Position in other geom frame. + mjtNum lpos[3], tmp[3]; + mju_sub3(tmp, xpos, d->geom_xpos + 3*geom); + mju_mulMatTVec3(lpos, d->geom_xmat + 9*geom, tmp); + + // Add mesh position if needed. + if (m->geom_type[geom] == mjGEOM_MESH || + m->geom_type[geom] == mjGEOM_SDF) { + mjtNum mesh_mat[9]; + mju_quat2Mat(mesh_mat, m->mesh_quat + 4 * m->geom_dataid[geom]); + mju_mulMatVec3(lpos, mesh_mat, lpos); + mju_addTo3(lpos, m->mesh_pos + 3 * m->geom_dataid[geom]); + } + + // Compute distance. + mjtNum depth = mju_min(mjc_distance(m, d, &geom_sdf, lpos), 0); + if (depth == 0) { + mju_zero3(forces + 3*node); + node++; + continue; + } + + // Get velocity in global frame. + mjtNum vel_sensor[6], vel_other[6], vel_rel[3]; + mju_transformSpatial( + vel_sensor, d->cvel + 6 * parent_weld_, 0, xpos, + d->subtree_com + 3 * m->body_rootid[parent_weld_], NULL); + mju_transformSpatial( + vel_other, d->cvel + 6 * body, 0, d->geom_xpos + 3 * geom, + d->subtree_com + 3 * m->body_rootid[body], NULL); + mju_sub3(vel_rel, vel_sensor+3, vel_other+3); + + // Get contact force/torque, rotate into node frame. + mjtNum tmp_force[3], normal[3]; + mjtNum kMaxDepth = 0.05; + mjtNum pressure = 1 / (kMaxDepth - depth) - 1 / kMaxDepth; + mjc_gradient(m, d, &sensor_sdf, normal, pos); + mju_scl3(tmp_force, normal, pressure); + mju_mulMatTVec3(forces + 3*node, mat, tmp_force); + forces[3*node+0] = mju_abs(mju_dot3(vel_rel, mat + 0)); + forces[3*node+1] = mju_abs(mju_dot3(vel_rel, mat + 3)); + + // Permute forces from x,y,z to z,x,y (normal, tangent, tangent) + xyz2zxy(forces + 3*node); + node++; + } + } + + // Transpose forces. + mju_transpose(forcesT, forces, ncon, 3); + + // Compute sensor output. + for (int c = 0; c < nchannel_; c++) { + if (!mju_isZero(forcesT + c*ncon, ncon)) { + mju_addTo(sensordata + c*ncon, forcesT + c*ncon, size_[0]*size_[1]); + } + } + } + + mj_freeStack(d); +} + +// Thickness of taxel-visualization boxes relative to contact distance. +static const mjtNum kRelativeThickness = 0.02; + +void TouchStress::Visualize(const mjModel* m, mjData* d, const mjvOption* opt, + mjvScene* scn, int instance) { + mj_markStack(d); + + // Get sensor data. + mjtNum* sensordata = d->sensordata + m->sensor_adr[id_]; + + // Get maximum absolute normal force. + mjtNum maxval = 0; + int frame = size_[0]*size_[1]; + for (int j=0; j < frame; j++) { + maxval = mju_max(maxval, mju_abs(sensordata[j])); + } + + // If no normal force readings, quick return. + if (!maxval) { + mj_freeStack(d); + return; + } + + // Get site id and frame. + int site_id = m->sensor_objid[id_]; + mjtNum* site_pos = d->site_xpos + 3*site_id; + mjtNum* site_mat = d->site_xmat + 9*site_id; + mjtNum site_quat[4]; + mju_mat2Quat(site_quat, site_mat); + + // Draw geoms. + for (int i=0; i < size_[0]; i++) { + for (int j=0; j < size_[1]; j++) { + mjtNum dist = dist_[i*size_[1]+j]; + if (!dist) { + continue; + } + if (scn->ngeom >= scn->maxgeom) { + mj_warning(d, mjWARN_VGEOMFULL, scn->maxgeom); + mj_freeStack(d); + return; + } else { + // size + mjtNum size[3]; + size[0] = dist*0.5*(x_edges_[i+1]-x_edges_[i]); + size[1] = dist*0.5*(y_edges_[j+1]-y_edges_[j]); + size[2] = dist*kRelativeThickness; + + // position + mjtNum pos[3]; + mjtNum aer[3]; + aer[0] = 0.5*(x_edges_[i+1]+x_edges_[i]); + aer[1] = 0.5*(y_edges_[j+1]+y_edges_[j]); + aer[2] = dist*(1-kRelativeThickness); + SphericalToCartesian(aer, pos); + mju_mulMatVec3(pos, site_mat, pos); + mju_addTo3(pos, site_pos); + + // orientation + mjtNum a_quat[4]; + mjtNum site_y[3] = {-site_mat[1], -site_mat[4], -site_mat[7]}; + mju_axisAngle2Quat(a_quat, site_y, aer[0]); + mjtNum e_quat[4]; + mjtNum site_x[3] = {site_mat[0], site_mat[3], site_mat[6]}; + mju_axisAngle2Quat(e_quat, site_x, aer[1]); + mjtNum quat[4]; + mju_mulQuat(quat, e_quat, site_quat); + mju_mulQuat(quat, a_quat, quat); + mjtNum mat[9]; + mju_quat2Mat(mat, quat); + + // color + float rgba[4] = {1, 1, 1, 1.0}; + for (int k=0; k < mjMIN(nchannel_, 3); k++) { + rgba[k] = mju_abs(sensordata[k*frame + j*size_[0] + i]) / maxval; + } + + // draw box geom + mjvGeom* thisgeom = scn->geoms + scn->ngeom; + mjv_initGeom(thisgeom, mjGEOM_BOX, size, pos, mat, rgba); + thisgeom->objtype = mjOBJ_UNKNOWN; + thisgeom->objid = id_; + thisgeom->category = mjCAT_DECOR; + thisgeom->segid = scn->ngeom; + scn->ngeom++; + } + } + } + + mj_freeStack(d); +} + + +void TouchStress::RegisterPlugin() { + mjpPlugin plugin; + mjp_defaultPlugin(&plugin); + + plugin.name = "mujoco.sensor.touch_stress"; + plugin.capabilityflags |= mjPLUGIN_SENSOR; + + // Parameterized by 4 attributes. + const char* attributes[] = {"nchannel", "size", "fov", "gamma"}; + plugin.nattribute = sizeof(attributes) / sizeof(attributes[0]); + plugin.attributes = attributes; + + // Stateless. + plugin.nstate = +[](const mjModel* m, int instance) { return 0; }; + + // Sensor dimension = nchannel * size[0] * size[1] + plugin.nsensordata = +[](const mjModel* m, int instance, int sensor_id) { + int nchannel = strtod(mj_getPluginConfig(m, instance, "nchannel"), nullptr); + if (!nchannel) nchannel = 1; + std::vector size; + std::string size_str = std::string(mj_getPluginConfig(m, instance, "size")); + ReadVector(size, size_str.c_str()); + return nchannel * size[0] * size[1]; + }; + + // Can only run after forces have been computed. + plugin.needstage = mjSTAGE_ACC; + + // Initialization callback. + plugin.init = +[](const mjModel* m, mjData* d, int instance) { + auto* TouchStress = TouchStress::Create(m, d, instance); + if (!TouchStress) { + return -1; + } + d->plugin_data[instance] = reinterpret_cast(TouchStress); + return 0; + }; + + // Destruction callback. + plugin.destroy = +[](mjData* d, int instance) { + delete reinterpret_cast(d->plugin_data[instance]); + d->plugin_data[instance] = 0; + }; + + // Reset callback. + plugin.reset = +[](const mjModel* m, mjtNum* plugin_state, void* plugin_data, + int instance) { + auto* TouchStress = reinterpret_cast(plugin_data); + TouchStress->Reset(m, instance); + }; + + // Compute callback. + plugin.compute = + +[](const mjModel* m, mjData* d, int instance, int capability_bit) { + auto* TouchStress = + reinterpret_cast(d->plugin_data[instance]); + TouchStress->Compute(m, d, instance); + }; + + // Visualization callback. + plugin.visualize = +[](const mjModel* m, mjData* d, const mjvOption* opt, + mjvScene* scn, int instance) { + auto* TouchStress = + reinterpret_cast(d->plugin_data[instance]); + TouchStress->Visualize(m, d, opt, scn, instance); + }; + + // Register the plugin. + mjp_registerPlugin(&plugin); +} + +} // namespace mujoco::plugin::sensor diff --git a/plugin/sensor/touch_stress.h b/plugin/sensor/touch_stress.h new file mode 100644 index 00000000..7453d87a --- /dev/null +++ b/plugin/sensor/touch_stress.h @@ -0,0 +1,80 @@ +// Copyright 2025 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. + +#ifndef MUJOCO_PLUGIN_SENSOR_TOUCH_STRESS_H_ +#define MUJOCO_PLUGIN_SENSOR_TOUCH_STRESS_H_ + +#include + +#include +#include +#include +#include + +namespace mujoco::plugin::sensor { + +// A touch grid sensor is associated with a site and senses contact stresses +// between the site's parent body and all other bodies. The site's +// frame determines the orientation of the sensor with the same convention used +// for cameras and lights: the sensor points in the frame's negative-Z +// direction, so the X and Y axes correspond to horizontal and vertical +// directions respectively. +// +// The output of the sensor is a stack of 3 "touch images" corresponding to +// forces in the local frame of the taxels. Forces are in the [z, x, y] order, +// corresponding to the ordering in contact frames: [normal, tangent, tangent]. +// +// The sensor has 6 parameters: +// 1. (int) Number of channels [1-3]. Defaults to 1. +// 2. (int) Horizontal resolution. +// 3. (int) Vertical resolution. +// 4. (float) Horizontal field-of-view (fov_x), in degrees. +// 5. (float) Vertical field-of-view (fov_y), in degrees. +// 6. (float) Foveal deformation. Defaults to 0. +class TouchStress { + public: + static TouchStress* Create(const mjModel* m, mjData* d, int instance); + TouchStress(TouchStress&&) = default; + ~TouchStress() = default; + + void Reset(const mjModel* m, int instance); + void Compute(const mjModel* m, mjData* d, int instance); + void Visualize(const mjModel* m, mjData* d, const mjvOption* opt, + mjvScene* scn, int instance); + + static void RegisterPlugin(); + + int nchannel_; // number of channels (1-3) + int size_[2]; // horizontal and vertical resolution + mjtNum fov_[2]; // horizontal and vertical field of view, in degrees + mjtNum gamma_; // foveal deformation + + private: + TouchStress(const mjModel* m, mjData* d, int instance, int nchannel, + int* size, mjtNum* fov_x, mjtNum gamma); + + std::vector x_edges_; + std::vector y_edges_; + std::vector dist_; + std::vector pos_; + std::vector mat_; + + int id_; + int parent_weld_; + int geom_id_; +}; + +} // namespace mujoco::plugin::sensor + +#endif // MUJOCO_PLUGIN_SENSOR_TOUCH_STRESS_H_ diff --git a/src/engine/engine_collision_sdf.c b/src/engine/engine_collision_sdf.c index f58f414a..591c96b8 100644 --- a/src/engine/engine_collision_sdf.c +++ b/src/engine/engine_collision_sdf.c @@ -265,7 +265,7 @@ void mjc_gradient(const mjModel* m, const mjData* d, const mjSDF* s, } // get sdf from geom id -static const mjpPlugin* getSDF(const mjModel* m, int id) { +const mjpPlugin* mjc_getSDF(const mjModel* m, int id) { int instance = m->geom_plugin[id]; const int nslot = mjp_pluginCount(); const int slot = m->plugin[instance]; @@ -585,7 +585,7 @@ int mjc_MeshSDF(const mjModel* m, const mjData* d, mjContact* con, int g1, int g // get sdf plugin int instance = m->geom_plugin[g2]; - const mjpPlugin* sdf_ptr = getSDF(m, g2); + const mjpPlugin* sdf_ptr = mjc_getSDF(m, g2); mjtGeom geomtype = mjGEOM_SDF; // copy into data @@ -727,12 +727,12 @@ int mjc_SDF(const mjModel* m, const mjData* d, mjContact* con, int g1, int g2, m mjtGeom geomtypes[2] = {m->geom_type[g2], m->geom_type[g1]}; instance[0] = m->geom_plugin[g2]; - sdf_ptr[0] = getSDF(m, g2); + sdf_ptr[0] = mjc_getSDF(m, g2); // get sdf plugins if (m->geom_type[g1] == mjGEOM_SDF) { instance[1] = m->geom_plugin[g1]; - sdf_ptr[1] = getSDF(m, g1); + sdf_ptr[1] = mjc_getSDF(m, g1); } else { instance[1] = g1; sdf_ptr[1] = NULL; diff --git a/test/engine/engine_plugin_test.cc b/test/engine/engine_plugin_test.cc index 5463f1e2..a1b5f959 100644 --- a/test/engine/engine_plugin_test.cc +++ b/test/engine/engine_plugin_test.cc @@ -37,7 +37,7 @@ using ::testing::DoubleNear; using ::testing::HasSubstr; using ::testing::NotNull; -constexpr int kNumTruePlugins = 10; +constexpr int kNumTruePlugins = 11; constexpr int kNumFakePlugins = 30; constexpr int kNumTestPlugins = 4; From 34e8ff1aad4a6311be6455d703a3f497d4cfa077 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Wed, 14 May 2025 02:49:46 -0700 Subject: [PATCH 125/191] Add flex edge flap connectivity to mjModel. PiperOrigin-RevId: 758593152 Change-Id: I79836df961972c9eae8037e5fcbfc3d0122645de --- doc/includes/references.h | 1 + include/mujoco/mjmodel.h | 1 + include/mujoco/mjxmacro.h | 1 + plugin/elasticity/shell.cc | 105 ++++++---------------- plugin/elasticity/shell.h | 13 +-- python/mujoco/introspect/structs.py | 8 ++ src/user/user_mesh.cc | 68 ++++++++++++-- src/user/user_model.cc | 7 ++ src/user/user_objects.h | 6 ++ test/plugin/elasticity/elasticity_test.cc | 6 +- unity/Runtime/Bindings/MjBindings.cs | 1 + 11 files changed, 122 insertions(+), 95 deletions(-) diff --git a/doc/includes/references.h b/doc/includes/references.h index eada1572..ffada3b8 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -1229,6 +1229,7 @@ struct mjModel_ { int* flex_nodebodyid; // node body ids (nflexnode x 1) int* flex_vertbodyid; // vertex body ids (nflexvert x 1) int* flex_edge; // edge vertex ids (2 per edge) (nflexedge x 2) + int* flex_edgeflap; // adjacent vertex ids (dim=2 only) (nflexedge x 2) int* flex_elem; // element vertex ids (dim+1 per elem) (nflexelemdata x 1) int* flex_elemtexcoord; // element texture coordinates (dim+1) (nflexelemdata x 1) int* flex_elemedge; // element edge ids (nflexelemedge x 1) diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index ceaa76d1..fd63b272 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -896,6 +896,7 @@ struct mjModel_ { int* flex_nodebodyid; // node body ids (nflexnode x 1) int* flex_vertbodyid; // vertex body ids (nflexvert x 1) int* flex_edge; // edge vertex ids (2 per edge) (nflexedge x 2) + int* flex_edgeflap; // adjacent vertex ids (dim=2 only) (nflexedge x 2) int* flex_elem; // element vertex ids (dim+1 per elem) (nflexelemdata x 1) int* flex_elemtexcoord; // element texture coordinates (dim+1) (nflexelemdata x 1) int* flex_elemedge; // element edge ids (nflexelemedge x 1) diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index aa55b3d7..f46b4ff5 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -349,6 +349,7 @@ XMJV( int, flex_nodebodyid, nflexnode, 1 ) \ X ( int, flex_vertbodyid, nflexvert, 1 ) \ X ( int, flex_edge, nflexedge, 2 ) \ + X ( int, flex_edgeflap, nflexedge, 2 ) \ XMJV( int, flex_elem, nflexelemdata, 1 ) \ XMJV( int, flex_elemtexcoord, nflexelemdata, 1 ) \ X ( int, flex_elemedge, nflexelemedge, 1 ) \ diff --git a/plugin/elasticity/shell.cc b/plugin/elasticity/shell.cc index 30b02051..772b06eb 100644 --- a/plugin/elasticity/shell.cc +++ b/plugin/elasticity/shell.cc @@ -12,14 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include #include #include #include #include #include -#include -#include #include #include @@ -34,9 +31,7 @@ namespace mujoco::plugin::elasticity { namespace { // local tetrahedron numbering -constexpr int kNumEdges = Stencil2D::kNumEdges; constexpr int kNumVerts = Stencil2D::kNumVerts; -constexpr int edge[kNumEdges][2] = {{1, 2}, {2, 0}, {0, 1}}; // cotangent between two edges mjtNum cot(mjtNum* x, int v0, int v1, int v2) { @@ -68,81 +63,26 @@ mjtNum ComputeVolume(const mjtNum* x, const int v[kNumVerts]) { // factory function std::optional Shell::Create(const mjModel* m, mjData* d, int instance) { - if (CheckAttr("face", m, instance) && - CheckAttr("edge", m, instance) && - CheckAttr("poisson", m, instance) && + if (CheckAttr("poisson", m, instance) && CheckAttr("young", m, instance) && CheckAttr("thickness", m, instance)) { mjtNum nu = strtod(mj_getPluginConfig(m, instance, "poisson"), nullptr); mjtNum E = strtod(mj_getPluginConfig(m, instance, "young"), nullptr); mjtNum thick = strtod(mj_getPluginConfig(m, instance, "thickness"), nullptr); - std::vector face, edge; - String2Vector(mj_getPluginConfig(m, instance, "face"), face); - String2Vector(mj_getPluginConfig(m, instance, "edge"), edge); - return Shell(m, d, instance, nu, E, thick, face, edge); + return Shell(m, d, instance, nu, E, thick); } else { mju_warning("Invalid parameter specification in shell plugin"); return std::nullopt; } } -// create map from triangles to vertices and edges and from edges to vertices -void Shell::CreateStencils(const std::vector& simplex, - const std::vector& edgeidx) { - // populate stencil - nt = simplex.size() / kNumVerts; - elements.resize(nt); - for (int t = 0; t < nt; t++) { - for (int v = 0; v < kNumVerts; v++) { - elements[t].vertices[v] = simplex[kNumVerts*t+v]; - } - } - - // map from edge vertices to their index in `edges` vector - std::unordered_map, int, PairHash> edge_indices; - - // loop over all triangles - for (int t = 0; t < nt; t++) { - int* v = elements[t].vertices; - - // compute edges to vertices map for fast computations - for (int e = 0; e < kNumEdges; e++) { - auto pair = std::pair( - std::min(v[edge[e][0]], v[edge[e][1]]), - std::max(v[edge[e][0]], v[edge[e][1]]) - ); - - // if edge is already present in the vector only store its index - auto [it, inserted] = edge_indices.insert({pair, ne}); - - if (inserted) { - StencilFlap flap; - flap.vertices[0] = v[edge[e][0]]; - flap.vertices[1] = v[edge[e][1]]; - flap.vertices[2] = v[(edge[e][1]+1) % 3]; - flap.vertices[3] = -1; - flaps.push_back(flap); - elements[t].edges[e] = ne++; - } else { - elements[t].edges[e] = it->second; - flaps[it->second].vertices[3] = v[(edge[e][1]+1) % 3]; - } - - if (!edgeidx.empty()) { - assert(elements[t].edges[e] == edgeidx[kNumEdges*t+e]); - } - } - } -} - // plugin constructor Shell::Shell(const mjModel* m, mjData* d, int instance, mjtNum nu, mjtNum E, - mjtNum thick, const std::vector& face, - const std::vector& edgeidx) - : thickness(thick) { + mjtNum thick) + : f0(-1), thickness(thick) { // count plugin bodies - nv = ne = 0; + nv = 0; for (int i = 1; i < m->nbody; i++) { if (m->body_plugin[i] == instance) { if (!nv++) { @@ -151,15 +91,25 @@ Shell::Shell(const mjModel* m, mjData* d, int instance, mjtNum nu, mjtNum E, } } - // generate triangles from the vertices - CreateStencils(face, edgeidx); + // count flexes + for (int i = 0; i < m->nflex; i++) { + for (int j = 0; j < m->flex_vertnum[i]; j++) { + if (m->flex_vertbodyid[m->flex_vertadr[i]+j] == i0) { + f0 = i; + nv = m->flex_vertnum[f0]; + if (m->flex_dim[i] != 2) { // SHOULD NOT OCCUR + mju_error("mujoco.elasticity.shell requires a 2D mesh"); + } + } + } + } // material parameters mjtNum mu = E / (2*(1+nu)); // loop over all triangles - for (int t = 0; t < nt; t++) { - int* v = elements[t].vertices; + for (int t = 0; t < m->flex_elemnum[f0]; t++) { + int* v = m->flex_elem + 3*(t+m->flex_elemadr[f0]); for (int i = 0; i < kNumVerts; i++) { if (m->body_plugin[i0+v[i]] != instance) { mju_error("This body does not have the requested plugin instance"); @@ -169,14 +119,16 @@ Shell::Shell(const mjModel* m, mjData* d, int instance, mjtNum nu, mjtNum E, // allocate array position.assign(nv*3, 0); - bending.assign(ne*16, 0); + bending.assign(m->flex_edgenum[f0]*16, 0); // store previous positions mju_copy(position.data(), m->body_pos+3*i0, 3*nv); // assemble bending Hessian - for (int e = 0; e < ne; e++) { - int* v = flaps[e].vertices; + for (int e = 0; e < m->flex_edgenum[f0]; e++) { + int* edge = m->flex_edge + 2*(e+m->flex_edgeadr[f0]); + int* flap = m->flex_edgeflap + 2*(e+m->flex_edgeadr[f0]); + int v[4] = {edge[0], edge[1], flap[0], flap[1]}; int vadj[3] = {v[1], v[0], v[3]}; if (v[3]== -1) { @@ -205,8 +157,10 @@ Shell::Shell(const mjModel* m, mjData* d, int instance, mjtNum nu, mjtNum E, } void Shell::Compute(const mjModel* m, mjData* d, int instance) { - for (int e = 0; e < ne; e++) { - int* v = flaps[e].vertices; + for (int e = 0; e < m->flex_edgenum[f0]; e++) { + int* edge = m->flex_edge + 2*(e+m->flex_edgeadr[f0]); + int* flap = m->flex_edgeflap + 2*(e+m->flex_edgeadr[f0]); + int v[4] = {edge[0], edge[1], flap[0], flap[1]}; mjtNum force[12] = {0}; if (v[3] == -1) { // skip boundary edges @@ -241,8 +195,7 @@ void Shell::RegisterPlugin() { plugin.name = "mujoco.elasticity.shell"; plugin.capabilityflags |= mjPLUGIN_PASSIVE; - const char* attributes[] = {"face", "edge", "young", - "poisson", "thickness", "damping"}; + const char* attributes[] = {"young", "poisson", "thickness", "damping"}; plugin.nattribute = sizeof(attributes) / sizeof(attributes[0]); plugin.attributes = attributes; plugin.nstate = +[](const mjModel* m, int instance) { return 0; }; diff --git a/plugin/elasticity/shell.h b/plugin/elasticity/shell.h index f92fffbb..e8496e70 100644 --- a/plugin/elasticity/shell.h +++ b/plugin/elasticity/shell.h @@ -45,14 +45,9 @@ class Shell { static void RegisterPlugin(); int i0; // index of first body + int f0; // index of corresponding flex int nc; // number of quads in the grid int nv; // number of vertices (bodies) in the Shell - int nt; // number of area elements (triangles) - int ne; // number of edges in the Shell - - // connectivity info for mapping tetrahedra to edges and vertices - std::vector elements; // triangles (nt x 6) - std::vector flaps; // adjacent triangles (ne x 4) // precomputed quantities std::vector position; // previous-step positions (nv x 3) @@ -62,11 +57,7 @@ class Shell { private: Shell(const mjModel* m, mjData* d, int instance, mjtNum nu, mjtNum E, - mjtNum thick, const std::vector& face, - const std::vector& edgeidx); - - void CreateStencils(const std::vector& simplex, - const std::vector& edgeidx); + mjtNum thick); }; } // namespace mujoco::plugin::elasticity diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index e9e6f801..4efca1ee 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -2619,6 +2619,14 @@ STRUCTS: Mapping[str, StructDecl] = dict([ doc='edge vertex ids (2 per edge)', array_extent=('nflexedge', 2), ), + StructFieldDecl( + name='flex_edgeflap', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='adjacent vertex ids (dim=2 only)', + array_extent=('nflexedge', 2), + ), StructFieldDecl( name='flex_elem', type=PointerType( diff --git a/src/user/user_mesh.cc b/src/user/user_mesh.cc index 93c10004..b44ecff5 100644 --- a/src/user/user_mesh.cc +++ b/src/user/user_mesh.cc @@ -3018,6 +3018,63 @@ void inline ComputeStiffness(std::vector& stiffness, MetricTensor(stiffness.data(), t, mu, la, basis); } +// local tetrahedron numbering +constexpr int kNumEdges = Stencil2D::kNumEdges; +constexpr int kNumVerts = Stencil2D::kNumVerts; +constexpr int edge[kNumEdges][2] = {{1, 2}, {2, 0}, {0, 1}}; + +// create map from triangles to vertices and edges and from edges to vertices +static void CreateFlapStencil(std::vector& flaps, + const std::vector& simplex, + const std::vector& edgeidx) { + // populate stencil + int ne = 0; + int nt = simplex.size() / kNumVerts; + std::vector elements(nt); + for (int t = 0; t < nt; t++) { + for (int v = 0; v < kNumVerts; v++) { + elements[t].vertices[v] = simplex[kNumVerts * t + v]; + } + } + + // map from edge vertices to their index in `edges` vector + std::unordered_map, int, PairHash> edge_indices; + + // loop over all triangles + for (int t = 0; t < nt; t++) { + int* v = elements[t].vertices; + + // compute edges to vertices map for fast computations + for (int e = 0; e < kNumEdges; e++) { + auto pair = std::pair(std::min(v[edge[e][0]], v[edge[e][1]]), + std::max(v[edge[e][0]], v[edge[e][1]])); + + // if edge is already present in the vector only store its index + auto [it, inserted] = edge_indices.insert({pair, ne}); + + if (inserted) { + StencilFlap flap; + flap.vertices[0] = v[edge[e][0]]; + flap.vertices[1] = v[edge[e][1]]; + flap.vertices[2] = v[(edge[e][1] + 1) % 3]; + flap.vertices[3] = -1; + flaps.push_back(flap); + elements[t].edges[e] = ne++; + } else { + elements[t].edges[e] = it->second; + flaps[it->second].vertices[3] = v[(edge[e][1] + 1) % 3]; + } + + // double check that the edge indices are consistent + if (!edgeidx.empty()) { + if (elements[t].edges[e] != edgeidx[kNumEdges * t + e]) { + mju_error("edge indices do not match in CreateFlapStencil"); + } + } + } + } +} + //----------------------------- linear elasticity -------------------------------------------------- // Gauss Legendre quadrature points in 1 dimension on the interval [a, b] @@ -3543,10 +3600,6 @@ void mjCFlex::Compile(const mjVFS* vfs) { } // add plugins - std::string userface, useredge; - userface = VectorToString(elem_); - useredge = VectorToString(edgeidx_); - for (const auto& vbodyid : vertbodyid) { if (vbodyid < 0) { continue; @@ -3557,11 +3610,14 @@ void mjCFlex::Compile(const mjVFS* vfs) { if (damping > 0) { plugin_instance->config_attribs["damping"] = std::to_string(damping); } - plugin_instance->config_attribs["face"] = userface; - plugin_instance->config_attribs["edge"] = useredge; } } + // create flap stencil + if (dim == 2) { + CreateFlapStencil(flaps, elem_, edgeidx_); + } + // create shell fragments and element-vertex collision pairs CreateShellPair(); diff --git a/src/user/user_model.cc b/src/user/user_model.cc index ff78e190..93449339 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -3158,6 +3158,13 @@ void mjCModel::CopyObjects(mjModel* m) { for (int k=0; k < pfl->nedge; k++) { m->flex_edge[2*(edge_adr+k)] = pfl->edge[k].first; m->flex_edge[2*(edge_adr+k)+1] = pfl->edge[k].second; + if (pfl->dim == 2) { + m->flex_edgeflap[2*(edge_adr+k)+0] = pfl->flaps[k].vertices[2]; + m->flex_edgeflap[2*(edge_adr+k)+1] = pfl->flaps[k].vertices[3]; + } else { + m->flex_edgeflap[2*(edge_adr+k)+0] = -1; + m->flex_edgeflap[2*(edge_adr+k)+1] = -1; + } if (pfl->rigid) { m->flexedge_rigid[edge_adr+k] = 1; diff --git a/src/user/user_objects.h b/src/user/user_objects.h index 9cdae417..585d6abb 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -830,6 +830,11 @@ class mjCLight : public mjCLight_, private mjsLight { //------------------------- class mjCFlex ---------------------------------------------------------- // Describes a flex +struct StencilFlap { + static constexpr int kNumVerts = 4; + int vertices[kNumVerts]; +}; + class mjCFlex_ : public mjCBase { protected: int nvert; // number of vertices @@ -846,6 +851,7 @@ class mjCFlex_ : public mjCBase { std::vector shell; // shell fragment vertex ids (dim per fragment) std::vector elemlayer; // element layer (distance from border) std::vector evpair; // element-vertex pairs + std::vector flaps; // adjacent triangles std::vector vertxpos; // global vertex positions mjCBoundingVolumeHierarchy tree; // bounding volume hierarchy std::vector elemaabb_; // element bounding volume diff --git a/test/plugin/elasticity/elasticity_test.cc b/test/plugin/elasticity/elasticity_test.cc index f38559b5..16273900 100644 --- a/test/plugin/elasticity/elasticity_test.cc +++ b/test/plugin/elasticity/elasticity_test.cc @@ -83,8 +83,10 @@ TEST_F(ElasticityTest, ElasticEnergyShell) { // check that a plane is in the kernel of the energy for (mjtNum scale = 1; scale < 4; scale++) { - for (int e = 0; e < shell->ne; e++) { - int* v = shell->flaps[e].vertices; + for (int e = 0; e < m->flex_edgenum[0]; e++) { + int* edge = m->flex_edge + 2*(m->flex_edgeadr[0] + e); + int* flap = m->flex_edgeflap + 2*(m->flex_edgeadr[0] + e); + int v[4] = {edge[0], edge[1], flap[0], flap[1]}; if (v[3]== -1) { continue; } diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index d26592a9..d743436c 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -5480,6 +5480,7 @@ public unsafe struct mjModel_ { public int* flex_nodebodyid; public int* flex_vertbodyid; public int* flex_edge; + public int* flex_edgeflap; public int* flex_elem; public int* flex_elemtexcoord; public int* flex_elemedge; From a6c3a287d6f62ec240c13fdb9988309ee240bfeb Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Wed, 14 May 2025 03:16:41 -0700 Subject: [PATCH 126/191] Prepare Newton solver for island support PiperOrigin-RevId: 758600631 Change-Id: Iac354588ad91f404a30fa035893560db60db733f --- src/engine/engine_solver.c | 178 +++++++++++++++++++------------------ 1 file changed, 92 insertions(+), 86 deletions(-) diff --git a/src/engine/engine_solver.c b/src/engine/engine_solver.c index b6a3fae3..adb1e23d 100644 --- a/src/engine/engine_solver.c +++ b/src/engine/engine_solver.c @@ -764,7 +764,8 @@ void mj_solNoSlip(const mjModel* m, mjData* d, int maxiter) { // CG context struct _mjCGContext { - int flg_Newton; // 1: Newton, 0: CG + int is_sparse; // 1: sparse, 0: dense + int is_elliptic; // 1: elliptic, 0: pyramidal int island; // current island index, -1 if monolithic // sizes @@ -830,6 +831,8 @@ struct _mjCGContext { int* H_lowernnz; // Hessian lower triangle row nonzeros (nv x 1) int* L_rownnz; // Hessian factor row nonzeros (nv x 1) int* L_rowadr; // Hessian factor row addresses (nv x 1) + int* buf_ind; // index buffer for sparse addition (nv x 1) + mjtNum* buf_val; // value buffer for sparse addition (nv x 1) // Newton arrays, computed-size (MakeHessian) int nH; // number of nonzeros in Hessian H @@ -859,7 +862,12 @@ typedef struct _mjCGContext mjCGContext; // set sizes and pointers to mjData arrays in mjCGContext static void CGpointers(const mjModel* m, const mjData* d, mjCGContext* ctx, int island) { - int is_sparse = mj_isSparse(m); + // clear everything + memset(ctx, 0, sizeof(mjCGContext)); + + // globals + ctx->is_sparse = mj_isSparse(m); + ctx->is_elliptic = (m->opt.cone == mjCONE_ELLIPTIC); ctx->contact = d->contact; ctx->island = island; @@ -898,7 +906,7 @@ static void CGpointers(const mjModel* m, const mjData* d, mjCGContext* ctx, int // Jacobians ctx->J = d->efc_J; - if (is_sparse) { + if (ctx->is_sparse) { ctx->J_rownnz = d->efc_J_rownnz; ctx->J_rowadr = d->efc_J_rowadr; ctx->J_rowsuper = d->efc_J_rowsuper; @@ -947,7 +955,7 @@ static void CGpointers(const mjModel* m, const mjData* d, mjCGContext* ctx, int ctx->efc_state = d->iefc_state + iefcadr; // Jacobians - if (!is_sparse) { + if (!ctx->is_sparse) { ctx->J = d->iefc_J + d->nidof * iefcadr; } else { ctx->J_rownnz = d->iefc_J_rownnz + iefcadr; @@ -968,13 +976,7 @@ static void CGpointers(const mjModel* m, const mjData* d, mjCGContext* ctx, int // allocate fixed-size arrays in mjCGContext // mj_{mark/free}Stack in calling function! -static void CGallocate(const mjModel* m, mjData* d, mjCGContext* ctx, int island, int flg_Newton) { - // clear everything - memset(ctx, 0, sizeof(mjCGContext)); - - // set sizes and pointers - CGpointers(m, d, ctx, island); - +static void CGallocate(mjData* d, mjCGContext* ctx, int flg_Newton) { // local sizes int nv = ctx->nv; int nefc = ctx->nefc; @@ -990,17 +992,18 @@ static void CGallocate(const mjModel* m, mjData* d, mjCGContext* ctx, int island ctx->quad = mjSTACKALLOC(d, nefc*3, mjtNum); // Newton only, known-size arrays - ctx->flg_Newton = flg_Newton; if (flg_Newton) { ctx->D = mjSTACKALLOC(d, nefc, mjtNum); // sparse Newton only - if (mj_isSparse(m)) { + if (ctx->is_sparse) { ctx->H_rowadr = mjSTACKALLOC(d, nv, int); ctx->H_rownnz = mjSTACKALLOC(d, nv, int); ctx->H_lowernnz = mjSTACKALLOC(d, nv, int); ctx->L_rownnz = mjSTACKALLOC(d, nv, int); ctx->L_rowadr = mjSTACKALLOC(d, nv, int); + ctx->buf_val = mjSTACKALLOC(d, nv, mjtNum); + ctx->buf_ind = mjSTACKALLOC(d, nv, int); } } } @@ -1008,17 +1011,17 @@ static void CGallocate(const mjModel* m, mjData* d, mjCGContext* ctx, int island // update efc_force, qfrc_constraint, cost-related -static void CGupdateConstraint(mjCGContext* ctx) { +static void CGupdateConstraint(mjCGContext* ctx, int flg_HessianCone) { int nefc = ctx->nefc, nv = ctx->nv; // update constraints mj_constraintUpdate_impl(ctx->ne, ctx->nf, ctx->nefc, ctx->efc_D, ctx->efc_R, ctx->efc_frictionloss, ctx->Jaref, ctx->efc_type, ctx->efc_id, ctx->contact, ctx->efc_state, ctx->efc_force, - &(ctx->cost), ctx->flg_Newton); + &(ctx->cost), flg_HessianCone); // compute qfrc_constraint (dense or sparse) - if (!ctx->JT) { + if (!ctx->is_sparse) { mju_mulMatTVec(ctx->qfrc_constraint, ctx->J, ctx->efc_force, nefc, nv); } else { mju_mulMatVecSparse(ctx->qfrc_constraint, ctx->JT, ctx->efc_force, nv, @@ -1046,7 +1049,7 @@ static void CGupdateConstraint(mjCGContext* ctx) { // update grad, Mgrad -static void CGupdateGradient(mjCGContext* ctx) { +static void CGupdateGradient(mjCGContext* ctx, int flg_Newton) { int nv = ctx->nv; // grad = M*qacc - qfrc_smooth - qfrc_constraint @@ -1056,8 +1059,8 @@ static void CGupdateGradient(mjCGContext* ctx) { // Newton: Mgrad = H \ grad // TODO: b/295296178 - add island support to Newton solver - if (ctx->flg_Newton) { - if (ctx->L_rowadr) { + if (flg_Newton) { + if (ctx->is_sparse) { mju_cholSolveSparse(ctx->Mgrad, (ctx->ncone ? ctx->Lcone : ctx->L), ctx->grad, nv, ctx->L_rownnz, ctx->L_rowadr, ctx->L_colind); } else { @@ -1358,7 +1361,7 @@ static mjtNum CGsearch(mjCGContext* ctx, mjtNum tolerance, mjtNum ls_iterations) ctx->M_rownnz, ctx->M_rowadr, ctx->M_diagnum, ctx->M_colind); // compute Jv = J * search (dense or sparse) - if (!ctx->J_rowadr) { + if (!ctx->is_sparse) { mju_mulMatVec(ctx->Jv, ctx->J, ctx->search, nefc, nv); } else { mju_mulMatVecSparse(ctx->Jv, ctx->J, ctx->search, nefc, @@ -1524,29 +1527,30 @@ static mjtNum CGsearch(mjCGContext* ctx, mjtNum tolerance, mjtNum ls_iterations) // allocate and compute Hessian given efc_state // mj_{mark/free}Stack in caller function! -static void MakeHessian(const mjModel* m, mjData* d, mjCGContext* ctx) { - int nv = m->nv, nefc = d->nefc; +static void MakeHessian(mjData* d, mjCGContext* ctx) { + int nv = ctx->nv, nefc = ctx->nefc; // compute constraint inertia for (int i=0; i < nefc; i++) { - ctx->D[i] = d->efc_state[i] == mjCNSTRSTATE_QUADRATIC ? d->efc_D[i] : 0; + ctx->D[i] = ctx->efc_state[i] == mjCNSTRSTATE_QUADRATIC ? ctx->efc_D[i] : 0; } // sparse - if (mj_isSparse(m)) { + if (ctx->is_sparse) { // initialize Hessian rowadr, rownnz mju_sqrMatTDSparseCount(ctx->H_rownnz, ctx->H_rowadr, nv, - d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind, - d->efc_JT_rownnz, d->efc_JT_rowadr, d->efc_JT_colind, - d->efc_JT_rowsuper, d, /*flg_upper=*/0); + ctx->J_rownnz, ctx->J_rowadr, ctx->J_colind, + ctx->JT_rownnz, ctx->JT_rowadr, ctx->JT_colind, + ctx->JT_rowsuper, d, /*flg_upper=*/0); // add nC to Hessian total nonzeros (unavoidable overcounting since H_colind is still unknown) - ctx->nH = m->nC + ctx->H_rowadr[nv - 1] + ctx->H_rownnz[nv - 1]; + ctx->nH = ctx->M_rowadr[nv - 1] + ctx->M_rownnz[nv - 1] + + ctx->H_rowadr[nv - 1] + ctx->H_rownnz[nv - 1]; // shift H row addresses to make room for C int shift = 0; for (int r = 0; r < nv - 1; r++) { - shift += d->C_rownnz[r]; + shift += ctx->M_rownnz[r]; ctx->H_rowadr[r + 1] += shift; } @@ -1555,15 +1559,16 @@ static void MakeHessian(const mjModel* m, mjData* d, mjCGContext* ctx) { ctx->H = mjSTACKALLOC(d, ctx->nH, mjtNum); // compute H = J'*D*J - mju_sqrMatTDSparse(ctx->H, d->efc_J, d->efc_JT, ctx->D, nefc, nv, + mju_sqrMatTDSparse(ctx->H, ctx->J, ctx->JT, ctx->D, nefc, nv, ctx->H_rownnz, ctx->H_rowadr, ctx->H_colind, - d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind, NULL, - d->efc_JT_rownnz, d->efc_JT_rowadr, d->efc_JT_colind, d->efc_JT_rowsuper, + ctx->J_rownnz, ctx->J_rowadr, ctx->J_colind, NULL, + ctx->JT_rownnz, ctx->JT_rowadr, ctx->JT_colind, ctx->JT_rowsuper, d, /*diagind=*/NULL); // add mass matrix: H = J'*D*J + C - mj_addMSparse(m, d, ctx->H, ctx->H_rownnz, ctx->H_rowadr, ctx->H_colind, - ctx->M, d->C_rownnz, d->C_rowadr, d->C_colind); + mju_addToMatSparse(ctx->H, ctx->H_rownnz, ctx->H_rowadr, ctx->H_colind, nv, + ctx->M, ctx->M_rownnz, ctx->M_rowadr, ctx->M_colind, + ctx->buf_val, ctx->buf_ind); // transiently compute H'; mju_cholFactorNNZ is memory-contiguous in upper triangle layout mj_markStack(d); @@ -1587,7 +1592,7 @@ static void MakeHessian(const mjModel* m, mjData* d, mjCGContext* ctx) { // allocate L_colind, L, Lcone ctx->L_colind = mjSTACKALLOC(d, ctx->nL, int); ctx->L = mjSTACKALLOC(d, ctx->nL, mjtNum); - if (m->opt.cone == mjCONE_ELLIPTIC) { + if (ctx->is_elliptic) { ctx->Lcone = mjSTACKALLOC(d, ctx->nL, mjtNum); } @@ -1617,49 +1622,49 @@ static void MakeHessian(const mjModel* m, mjData* d, mjCGContext* ctx) { // allocate L, Lcone ctx->nL = nv*nv; ctx->L = mjSTACKALLOC(d, ctx->nL, mjtNum); - if (m->opt.cone == mjCONE_ELLIPTIC) { + if (ctx->is_elliptic) { ctx->Lcone = mjSTACKALLOC(d, ctx->nL, mjtNum); } // compute H = M + J'*D*J - mju_sqrMatTD(ctx->L, d->efc_J, ctx->D, nefc, nv); + mju_sqrMatTD_impl(ctx->L, ctx->J, ctx->D, nefc, nv, /*flg_upper=*/ 0); mju_addToSymSparse(ctx->L, ctx->M, ctx->nv, - ctx->M_rownnz, ctx->M_rowadr, ctx->M_colind, - /*flg_upper=*/ 1); + ctx->M_rownnz, ctx->M_rowadr, ctx->M_colind, + /*flg_upper=*/ 0); } } // forward declaration of HessianCone (readability) -static void HessianCone(const mjModel* m, mjData* d, mjCGContext* ctx); +static void HessianCone(mjData* d, mjCGContext* ctx); // factorize Hessian: L = chol(H), maybe (re)compute H given efc_state -static void FactorizeHessian(const mjModel* m, mjData* d, mjCGContext* ctx, - int flg_recompute) { - int nv = m->nv, nefc = d->nefc; +static void FactorizeHessian(mjData* d, mjCGContext* ctx, int flg_recompute) { + int nv = ctx->nv, nefc = ctx->nefc; // maybe compute constraint inertia if (flg_recompute) { for (int i=0; i < nefc; i++) { - ctx->D[i] = d->efc_state[i] == mjCNSTRSTATE_QUADRATIC ? d->efc_D[i] : 0; + ctx->D[i] = ctx->efc_state[i] == mjCNSTRSTATE_QUADRATIC ? ctx->efc_D[i] : 0; } } // sparse - if (mj_isSparse(m)) { + if (ctx->is_sparse) { // maybe compute H = M + J'*D*J if (flg_recompute) { // compute H = J'*D*J - mju_sqrMatTDSparse(ctx->H, d->efc_J, d->efc_JT, ctx->D, nefc, nv, + mju_sqrMatTDSparse(ctx->H, ctx->J, ctx->JT, ctx->D, nefc, nv, ctx->H_rownnz, ctx->H_rowadr, ctx->H_colind, - d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind, NULL, - d->efc_JT_rownnz, d->efc_JT_rowadr, d->efc_JT_colind, d->efc_JT_rowsuper, + ctx->J_rownnz, ctx->J_rowadr, ctx->J_colind, NULL, + ctx->JT_rownnz, ctx->JT_rowadr, ctx->JT_colind, ctx->JT_rowsuper, d, /*diagind=*/NULL); // add mass matrix: H = J'*D*J + C - mj_addMSparse(m, d, ctx->H, ctx->H_rownnz, ctx->H_rowadr, ctx->H_colind, - ctx->M, d->C_rownnz, d->C_rowadr, d->C_colind); + mju_addToMatSparse(ctx->H, ctx->H_rownnz, ctx->H_rowadr, ctx->H_colind, nv, + ctx->M, ctx->M_rownnz, ctx->M_rowadr, ctx->M_colind, + ctx->buf_val, ctx->buf_ind); } // copy H lower-triangle into L, fill-in already accounted for @@ -1689,10 +1694,10 @@ static void FactorizeHessian(const mjModel* m, mjData* d, mjCGContext* ctx, else { // maybe compute H = M + J'*D*J if (flg_recompute) { - mju_sqrMatTD(ctx->L, d->efc_J, ctx->D, nefc, nv); + mju_sqrMatTD_impl(ctx->L, ctx->J, ctx->D, nefc, nv, /*flg_upper=*/ 0); mju_addToSymSparse(ctx->L, ctx->M, ctx->nv, ctx->M_rownnz, ctx->M_rowadr, ctx->M_colind, - /*flg_upper=*/ 1); + /*flg_upper=*/ 0); } // factorize H @@ -1701,7 +1706,7 @@ static void FactorizeHessian(const mjModel* m, mjData* d, mjCGContext* ctx, // add cones to factor if present if (ctx->ncone) { - HessianCone(m, d, ctx); + HessianCone(d, ctx); } // mark full update @@ -1711,8 +1716,8 @@ static void FactorizeHessian(const mjModel* m, mjData* d, mjCGContext* ctx, // elliptic case: Hcone = H + cone_contributions -static void HessianCone(const mjModel* m, mjData* d, mjCGContext* ctx) { - int nv = m->nv, nefc = d->nefc; +static void HessianCone(mjData* d, mjCGContext* ctx) { + int nv = ctx->nv, nefc = ctx->nefc; mjtNum local[36]; // start with Hcone = H @@ -1727,8 +1732,8 @@ static void HessianCone(const mjModel* m, mjData* d, mjCGContext* ctx) { // add contributions for (int i=0; i < nefc; i++) { - if (d->efc_state[i] == mjCNSTRSTATE_CONE) { - mjContact* con = d->contact + d->efc_id[i]; + if (ctx->efc_state[i] == mjCNSTRSTATE_CONE) { + mjContact* con = ctx->contact + ctx->efc_id[i]; int dim = con->dim; // Cholesky of local Hessian @@ -1736,15 +1741,15 @@ static void HessianCone(const mjModel* m, mjData* d, mjCGContext* ctx) { mju_cholFactor(local, dim, mjMINVAL); // sparse - if (mj_isSparse(m)) { + if (ctx->is_sparse) { // get nnz for row i (same for all rows in contact) - const int nnz = d->efc_J_rownnz[i]; + const int nnz = ctx->J_rownnz[i]; // compute LTJ = L'*J for this contact mju_zero(LTJ, dim*nnz); for (int r=0; r < dim; r++) { for (int c=0; c <= r; c++) { - mju_addToScl(LTJ+c*nnz, d->efc_J+d->efc_J_rowadr[i+r], local[r*dim+c], nnz); + mju_addToScl(LTJ+c*nnz, ctx->J+ctx->J_rowadr[i+r], local[r*dim+c], nnz); } } @@ -1752,7 +1757,7 @@ static void HessianCone(const mjModel* m, mjData* d, mjCGContext* ctx) { for (int r=0; r < dim; r++) { // copy data for this row mju_copy(LTJ_row, LTJ+r*nnz, nnz); - mju_copyInt(LTJ_ind, d->efc_J_colind+d->efc_J_rowadr[i+r], nnz); + mju_copyInt(LTJ_ind, ctx->J_colind+ctx->J_rowadr[i+r], nnz); // update mju_cholUpdateSparse(ctx->Lcone, LTJ_row, nv, 1, @@ -1766,7 +1771,7 @@ static void HessianCone(const mjModel* m, mjData* d, mjCGContext* ctx) { mju_zero(LTJ, dim*nv); for (int r=0; r < dim; r++) { for (int c=0; c <= r; c++) { - mju_addToScl(LTJ+c*nv, d->efc_J+(i+r)*nv, local[r*dim+c], nv); + mju_addToScl(LTJ+c*nv, ctx->J+(i+r)*nv, local[r*dim+c], nv); } } @@ -1790,8 +1795,8 @@ static void HessianCone(const mjModel* m, mjData* d, mjCGContext* ctx) { // incremental update to Hessian factor due to changes in efc_state -static void HessianIncremental(const mjModel* m, mjData* d, mjCGContext* ctx, const int* oldstate) { - int rank, nv = m->nv, nefc = d->nefc; +static void HessianIncremental(mjData* d, mjCGContext* ctx, const int* oldstate) { + int rank, nv = ctx->nv, nefc = ctx->nefc; mj_markStack(d); // local space @@ -1806,32 +1811,32 @@ static void HessianIncremental(const mjModel* m, mjData* d, mjCGContext* ctx, co int flag_update = -1; // add quad - if (oldstate[i] != mjCNSTRSTATE_QUADRATIC && d->efc_state[i] == mjCNSTRSTATE_QUADRATIC) { + if (oldstate[i] != mjCNSTRSTATE_QUADRATIC && ctx->efc_state[i] == mjCNSTRSTATE_QUADRATIC) { flag_update = 1; } // subtract quad - else if (oldstate[i] == mjCNSTRSTATE_QUADRATIC && d->efc_state[i] != mjCNSTRSTATE_QUADRATIC) { + else if (oldstate[i] == mjCNSTRSTATE_QUADRATIC && ctx->efc_state[i] != mjCNSTRSTATE_QUADRATIC) { flag_update = 0; } // perform update if flagged if (flag_update != -1) { // update with vec = J(i,:)*sqrt(D[i])) - if (mj_isSparse(m)) { + if (ctx->is_sparse) { // get nnz and adr of row i - const int nnz = d->efc_J_rownnz[i], adr = d->efc_J_rowadr[i]; + const int nnz = ctx->J_rownnz[i], adr = ctx->J_rowadr[i]; // scale vec, copy colind - mju_scl(vec, d->efc_J+adr, mju_sqrt(d->efc_D[i]), nnz); - mju_copyInt(vec_ind, d->efc_J_colind+adr, nnz); + mju_scl(vec, ctx->J+adr, mju_sqrt(ctx->efc_D[i]), nnz); + mju_copyInt(vec_ind, ctx->J_colind+adr, nnz); // sparse update or downdate rank = mju_cholUpdateSparse(ctx->L, vec, nv, flag_update, ctx->L_rownnz, ctx->L_rowadr, ctx->L_colind, nnz, vec_ind, d); } else { - mju_scl(vec, d->efc_J+i*nv, mju_sqrt(d->efc_D[i]), nv); + mju_scl(vec, ctx->J+i*nv, mju_sqrt(ctx->efc_D[i]), nv); rank = mju_cholUpdate(ctx->L, vec, nv, flag_update); } ctx->nupdate++; @@ -1839,7 +1844,7 @@ static void HessianIncremental(const mjModel* m, mjData* d, mjCGContext* ctx, co // recompute H directly if accuracy lost if (rank < nv) { mj_freeStack(d); - FactorizeHessian(m, d, ctx, /*flg_recompute=*/1); + FactorizeHessian(d, ctx, /*flg_recompute=*/1); // nothing else to do return; @@ -1849,7 +1854,7 @@ static void HessianIncremental(const mjModel* m, mjData* d, mjCGContext* ctx, co // add cones if present if (ctx->ncone) { - HessianCone(m, d, ctx); + HessianCone(d, ctx); } mj_freeStack(d); @@ -1865,8 +1870,9 @@ static void mj_solCGNewton(const mjModel* m, mjData* d, int island, int maxiter, mjCGContext ctx; mj_markStack(d); - // allocate context - CGallocate(m, d, &ctx, island, flg_Newton); + // make context + CGpointers(m, d, &ctx, island); + CGallocate(d, &ctx, flg_Newton); // local copies int nv = ctx.nv; @@ -1880,13 +1886,13 @@ static void mj_solCGNewton(const mjModel* m, mjData* d, int island, int maxiter, } int* oldstate = mjSTACKALLOC(d, nefc, int); - // compute Ma = M * qacc (island or monolithic) + // compute Ma = M * qacc mju_mulSymVecSparse(ctx.Ma, ctx.M, ctx.qacc, nv, ctx.M_rownnz, ctx.M_rowadr, ctx.M_diagnum, ctx.M_colind); // compute Jaref = J * qacc - aref (dense or sparse) - if (!ctx.J_rownnz) { + if (!ctx.is_sparse) { mju_mulMatVec(ctx.Jaref, ctx.J, ctx.qacc, nefc, nv); } else { mju_mulMatVecSparse(ctx.Jaref, ctx.J, ctx.qacc, nefc, @@ -1895,13 +1901,13 @@ static void mj_solCGNewton(const mjModel* m, mjData* d, int island, int maxiter, mju_subFrom(ctx.Jaref, ctx.efc_aref, nefc); // first update - CGupdateConstraint(&ctx); + CGupdateConstraint(&ctx, flg_Newton & (m->opt.cone == mjCONE_ELLIPTIC)); if (flg_Newton) { // compute and factorize Hessian - MakeHessian(m, d, &ctx); - FactorizeHessian(m, d, &ctx, /*flg_recompute=*/0); + MakeHessian(d, &ctx); + FactorizeHessian(d, &ctx, /*flg_recompute=*/0); } - CGupdateGradient(&ctx); + CGupdateGradient(&ctx, flg_Newton); // start both with preconditioned gradient mju_scl(ctx.search, ctx.Mgrad, -1, nv); @@ -1913,8 +1919,8 @@ static void mj_solCGNewton(const mjModel* m, mjData* d, int island, int maxiter, } else { mjtNum island_inertia = 0; for (int i=0; i < nv; i++) { - int* map2dof = d->map_idof2dof + d->island_idofadr[island]; - island_inertia += d->qM[m->dof_Madr[map2dof[i]]]; + int diag_i = ctx.M_rowadr[i] + ctx.M_rownnz[i] - 1; + island_inertia += ctx.M[diag_i]; } scale = 1 / island_inertia; } @@ -1944,11 +1950,11 @@ static void mj_solCGNewton(const mjModel* m, mjData* d, int island, int maxiter, mjtNum oldcost = ctx.cost; // update - CGupdateConstraint(&ctx); + CGupdateConstraint(&ctx, flg_Newton & (m->opt.cone == mjCONE_ELLIPTIC)); if (flg_Newton) { - HessianIncremental(m, d, &ctx, oldstate); + HessianIncremental(d, &ctx, oldstate); } - CGupdateGradient(&ctx); + CGupdateGradient(&ctx, flg_Newton); // count state changes int nchange = 0; From fa95fe0c59bd9200c78697eb9c0d528cae70b032 Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Wed, 14 May 2025 03:27:06 -0700 Subject: [PATCH 127/191] Add `intensity` and `range` parameters to lights. PiperOrigin-RevId: 758603199 Change-Id: I74e88bdfeedd9224e84d953f237d68eca79e6cb7 --- doc/XMLreference.rst | 94 +++++++++++++++++----------- doc/XMLschema.rst | 14 +++-- doc/includes/references.h | 10 ++- include/mujoco/mjmodel.h | 2 + include/mujoco/mjspec.h | 4 +- include/mujoco/mjvisualize.h | 4 ++ include/mujoco/mjxmacro.h | 2 + python/mujoco/introspect/structs.py | 52 ++++++++++++++- python/mujoco/structs.cc | 2 + src/engine/engine_vis_visualize.c | 2 + src/user/user_init.c | 3 +- src/user/user_model.cc | 2 + src/xml/xml_native_reader.cc | 13 ++-- src/xml/xml_native_writer.cc | 2 + unity/Runtime/Bindings/MjBindings.cs | 6 ++ 15 files changed, 160 insertions(+), 52 deletions(-) diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index efbecc2f..ef01da9d 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -2866,11 +2866,15 @@ and the +Y axis points up. Thus the frame position and orientation are the key a ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This element creates a light, which moves with the body where it is defined. To create a fixed light, define it in the -world body. The lights created here are in addition to the default headlight which is always defined and is adjusted via -the :ref:`visual ` element. MuJoCo relies on the standard lighting model in OpenGL (fixed functionality) -augmented with shadow mapping. The effects of lights are additive, thus adding a light always makes the scene brighter. -The maximum number of lights that can be active simultaneously is 8, counting the headlight. The light is shining along -the direction specified by the dir attribute. It does not have a full spatial frame with three orthogonal axes. +world body. The lights created here are in addition to the headlight which is always defined and is configured via the +:ref:`visual ` element. Lights shine along the direction specified by the dir attribute. They do not have +a full spatial frame with three orthogonal axes. + +By default, MuJoCo uses the standard OpenGL (fixed functional) Phong lighting model for its rendering, with augmented +with shadow mapping. (See the OpenGL documentation for more information, including details about various attributes.) + +MJCF also supports alternative lighting models (e.g. physically-based rendering) by providing additional attributes. +Attributes may be applied or ignored depending on the lighting model being used. .. _body-light-name: @@ -2919,12 +2923,6 @@ the direction specified by the dir attribute. It does not have a full spatial fr these clipping planes bound the cone or box shadow volume in the light direction. As a result, some shadows (especially those very close to the light) may be clipped. -.. _body-light-bulbradius: - -:at:`radius`: :at-val:`real, "0.02"` - Radius of the light, affects shadow softness. This attribute has no effect in MuJoCo's native renderer, but it can be - useful when rendering scenes with an external renderer. - .. _body-light-active: :at:`active`: :at-val:`[false, true], "true"` @@ -2941,11 +2939,45 @@ the direction specified by the dir attribute. It does not have a full spatial fr :at:`dir`: :at-val:`real(3), "0 0 -1"` Direction of the light. +.. _body-light-diffuse: + +:at:`diffuse`: :at-val:`real(3), "0.7 0.7 0.7"` + The color of the light. For the Phong (default) lighting model, this defines the diffuse color of + the light. + +.. _body-light-intensity: + +:at:`intensity`: :at-val:`real, "1000.0"` + The intensity of the light source, measured in candela, used for physically-based lighting models. + This is unused by the default Phong lighting model. + +.. _body-light-ambient: + +:at:`ambient`: :at-val:`real(3), "0 0 0"` + The ambient color of the light, used by the default Phong lighting model. + +.. _body-light-specular: + +:at:`specular`: :at-val:`real(3), "0.3 0.3 0.3"` + The specular color of the light, used by the default Phong lighting model. + +.. _body-light-range: + +:at:`range`: :at-val:`real, "10.0"` + The effective range of the light. Objects further than this distance from the light position + will not be illuminated by this light. This only applies to spotlights. + +.. _body-light-bulbradius: + +:at:`bulbradius`: :at-val:`real, "0.02"` + The radius of the light source which can affect shadow softness depending on the + renderer. This only applies to spotlights. + .. _body-light-attenuation: :at:`attenuation`: :at-val:`real(3), "1 0 0"` - These are the constant, linear and quadratic attenuation coefficients in OpenGL. The default corresponds to no - attenuation. See the OpenGL documentation for more information on this and all other OpenGL-related properties. + These are the constant, linear and quadratic attenuation coefficients for Phong lighting. + The default corresponds to no attenuation. .. _body-light-cutoff: @@ -2957,22 +2989,6 @@ the direction specified by the dir attribute. It does not have a full spatial fr :at:`exponent`: :at-val:`real, "10"` Exponent for spotlights. This setting controls the softness of the spotlight cutoff. -.. _body-light-ambient: - -:at:`ambient`: :at-val:`real(3), "0 0 0"` - The ambient color of the light. - -.. _body-light-diffuse: - -:at:`diffuse`: :at-val:`real(3), "0.7 0.7 0.7"` - The diffuse color of the light. - -.. _body-light-specular: - -:at:`specular`: :at-val:`real(3), "0.3 0.3 0.3"` - The specular color of the light. - - .. _body-composite: :el-prefix:`body/` |-| **composite** (*) @@ -8257,26 +8273,30 @@ if omitted. .. _default-light-dir: -.. _default-light-bulbradius: - .. _default-light-directional: .. _default-light-castshadow: .. _default-light-active: +.. _default-light-diffuse: + +.. _default-light-intensity: + +.. _default-light-ambient: + +.. _default-light-specular: + +.. _default-light-bulbradius: + +.. _default-light-range: + .. _default-light-attenuation: .. _default-light-cutoff: .. _default-light-exponent: -.. _default-light-ambient: - -.. _default-light-diffuse: - -.. _default-light-specular: - .. _default-light-mode: :el-prefix:`default/` |-| **light** (?) diff --git a/doc/XMLschema.rst b/doc/XMLschema.rst index 5b32d4bb..4935a883 100644 --- a/doc/XMLschema.rst +++ b/doc/XMLschema.rst @@ -317,9 +317,11 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | | | | | :ref:`active` | :ref:`pos` | :ref:`dir` | :ref:`bulbradius` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`attenuation` | :ref:`cutoff` | :ref:`exponent` | :ref:`ambient` | | +| | | | :ref:`intensity` | :ref:`range` | :ref:`attenuation` | :ref:`cutoff` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`diffuse` | :ref:`specular` | :ref:`mode` | :ref:`target` | | +| | | | :ref:`exponent` | :ref:`ambient` | :ref:`diffuse` | :ref:`specular` | | +| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +| | | | :ref:`mode` | :ref:`target` | | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| body |br| |_| |L| | | .. table:: | @@ -1457,13 +1459,13 @@ | :ref:`light | ? | :class: mjcf-attributes | | ` | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`pos` | :ref:`dir` | :ref:`bulbradius` | :ref:`directional` | | +| | | | :ref:`pos` | :ref:`dir` | :ref:`bulbradius` | :ref:`intensity` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`castshadow` | :ref:`active` | :ref:`attenuation` | :ref:`cutoff` | | +| | | | :ref:`range` | :ref:`directional` | :ref:`castshadow` | :ref:`active` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`exponent` | :ref:`ambient` | :ref:`diffuse` | :ref:`specular` | | +| | | | :ref:`attenuation` | :ref:`cutoff` | :ref:`exponent` | :ref:`ambient` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`mode` | | | | | +| | | | :ref:`diffuse` | :ref:`specular` | :ref:`mode` | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| default |br| |_| |L| | | .. table:: | diff --git a/doc/includes/references.h b/doc/includes/references.h index ffada3b8..c8349418 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -1178,6 +1178,8 @@ struct mjModel_ { mjtByte* light_directional; // directional light (nlight x 1) mjtByte* light_castshadow; // does light cast shadows (nlight x 1) float* light_bulbradius; // light radius for soft shadows (nlight x 1) + float* light_intensity; // intensity, in candela (nlight x 1) + float* light_range; // range of effectiveness (nlight x 1) mjtByte* light_active; // is light on (nlight x 1) mjtNum* light_pos; // position rel. to body frame (nlight x 3) mjtNum* light_dir; // direction rel. to body frame (nlight x 3) @@ -2030,7 +2032,9 @@ typedef struct mjsLight_ { // light specification mjtByte active; // is light active mjtByte directional; // is light directional or spot mjtByte castshadow; // does light cast shadows - double bulbradius; // bulb radius, for soft shadows + float bulbradius; // bulb radius, for soft shadows + float intensity; // intensity, in candelas + float range; // range of effectiveness float attenuation[3]; // OpenGL attenuation (quadratic model) float cutoff; // OpenGL cutoff float exponent; // OpenGL exponent @@ -2838,6 +2842,8 @@ struct mjvLight_ { // OpenGL light mjtByte directional; // directional light mjtByte castshadow; // does light cast shadows float bulbradius; // bulb radius for soft shadows + float intensity; // intensity, in candelas + float range; // range of effectiveness }; typedef struct mjvLight_ mjvLight; struct mjvOption_ { // abstract visualization options @@ -3055,6 +3061,8 @@ struct mjvSceneState_ { mjtByte* light_directional; mjtByte* light_castshadow; float* light_bulbradius; + float* light_intensity; + float* light_range; mjtByte* light_active; float* light_attenuation; float* light_cutoff; diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index fd63b272..8c00e062 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -845,6 +845,8 @@ struct mjModel_ { mjtByte* light_directional; // directional light (nlight x 1) mjtByte* light_castshadow; // does light cast shadows (nlight x 1) float* light_bulbradius; // light radius for soft shadows (nlight x 1) + float* light_intensity; // intensity, in candela (nlight x 1) + float* light_range; // range of effectiveness (nlight x 1) mjtByte* light_active; // is light on (nlight x 1) mjtNum* light_pos; // position rel. to body frame (nlight x 3) mjtNum* light_dir; // direction rel. to body frame (nlight x 3) diff --git a/include/mujoco/mjspec.h b/include/mujoco/mjspec.h index fb95afad..512e7c29 100644 --- a/include/mujoco/mjspec.h +++ b/include/mujoco/mjspec.h @@ -396,7 +396,9 @@ typedef struct mjsLight_ { // light specification mjtByte active; // is light active mjtByte directional; // is light directional or spot mjtByte castshadow; // does light cast shadows - double bulbradius; // bulb radius, for soft shadows + float bulbradius; // bulb radius, for soft shadows + float intensity; // intensity, in candelas + float range; // range of effectiveness float attenuation[3]; // OpenGL attenuation (quadratic model) float cutoff; // OpenGL cutoff float exponent; // OpenGL exponent diff --git a/include/mujoco/mjvisualize.h b/include/mujoco/mjvisualize.h index fa0aec4f..0093216b 100644 --- a/include/mujoco/mjvisualize.h +++ b/include/mujoco/mjvisualize.h @@ -271,6 +271,8 @@ struct mjvLight_ { // OpenGL light mjtByte directional; // directional light mjtByte castshadow; // does light cast shadows float bulbradius; // bulb radius for soft shadows + float intensity; // intensity, in candelas + float range; // range of effectiveness }; typedef struct mjvLight_ mjvLight; @@ -504,6 +506,8 @@ struct mjvSceneState_ { mjtByte* light_directional; mjtByte* light_castshadow; float* light_bulbradius; + float* light_intensity; + float* light_range; mjtByte* light_active; float* light_attenuation; float* light_cutoff; diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index f46b4ff5..bbd79ad1 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -302,6 +302,8 @@ XMJV( mjtByte, light_directional, nlight, 1 ) \ XMJV( mjtByte, light_castshadow, nlight, 1 ) \ XMJV( float, light_bulbradius, nlight, 1 ) \ + XMJV( float, light_intensity, nlight, 1 ) \ + XMJV( float, light_range, nlight, 1 ) \ XMJV( mjtByte, light_active, nlight, 1 ) \ X ( mjtNum, light_pos, nlight, 3 ) \ X ( mjtNum, light_dir, nlight, 3 ) \ diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index 4efca1ee..e8044c71 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -2243,6 +2243,22 @@ STRUCTS: Mapping[str, StructDecl] = dict([ doc='light radius for soft shadows', array_extent=('nlight',), ), + StructFieldDecl( + name='light_intensity', + type=PointerType( + inner_type=ValueType(name='float'), + ), + doc='intensity, in candela', + array_extent=('nlight',), + ), + StructFieldDecl( + name='light_range', + type=PointerType( + inner_type=ValueType(name='float'), + ), + doc='range of effectiveness', + array_extent=('nlight',), + ), StructFieldDecl( name='light_active', type=PointerType( @@ -6735,6 +6751,16 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='float'), doc='bulb radius for soft shadows', ), + StructFieldDecl( + name='intensity', + type=ValueType(name='float'), + doc='intensity, in candelas', + ), + StructFieldDecl( + name='range', + type=ValueType(name='float'), + doc='range of effectiveness', + ), ), )), ('mjvOption', @@ -7851,6 +7877,20 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), doc='', ), + StructFieldDecl( + name='light_intensity', + type=PointerType( + inner_type=ValueType(name='float'), + ), + doc='', + ), + StructFieldDecl( + name='light_range', + type=PointerType( + inner_type=ValueType(name='float'), + ), + doc='', + ), StructFieldDecl( name='light_active', type=PointerType( @@ -10600,9 +10640,19 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), StructFieldDecl( name='bulbradius', - type=ValueType(name='double'), + type=ValueType(name='float'), doc='bulb radius, for soft shadows', ), + StructFieldDecl( + name='intensity', + type=ValueType(name='float'), + doc='intensity, in candelas', + ), + StructFieldDecl( + name='range', + type=ValueType(name='float'), + doc='range of effectiveness', + ), StructFieldDecl( name='attenuation', type=ArrayType( diff --git a/python/mujoco/structs.cc b/python/mujoco/structs.cc index 3cf6eb82..735e0a0b 100644 --- a/python/mujoco/structs.cc +++ b/python/mujoco/structs.cc @@ -1076,6 +1076,8 @@ This is useful for example when the MJB is not available as a file on disk.)")); X(directional); X(castshadow); X(bulbradius); + X(intensity); + X(range); #undef X #define X(var) DefinePyArray(mjvLight, #var, &MjvLightWrapper::var) diff --git a/src/engine/engine_vis_visualize.c b/src/engine/engine_vis_visualize.c index 012cb966..0425b85c 100644 --- a/src/engine/engine_vis_visualize.c +++ b/src/engine/engine_vis_visualize.c @@ -2172,6 +2172,8 @@ void mjv_makeLights(const mjModel* m, const mjData* d, mjvScene* scn) { thislight->directional = m->light_directional[i]; thislight->castshadow = m->light_castshadow[i]; thislight->bulbradius = m->light_bulbradius[i]; + thislight->intensity = m->light_intensity[i]; + thislight->range = m->light_range[i]; if (!thislight->directional) { f2f(thislight->attenuation, m->light_attenuation+3*i, 3); thislight->exponent = m->light_exponent[i]; diff --git a/src/user/user_init.c b/src/user/user_init.c index 183034ce..01f4a8b9 100644 --- a/src/user/user_init.c +++ b/src/user/user_init.c @@ -203,6 +203,8 @@ void mjs_defaultLight(mjsLight* light) { // intrinsics light->castshadow = 1; light->bulbradius = 0.02; + light->intensity = 1000.0; + light->range = 10.0; light->active = 1; light->attenuation[0] = 1; light->cutoff = 45; @@ -404,4 +406,3 @@ void mjs_defaultKey(mjsKey* key) { void mjs_defaultPlugin(mjsPlugin* plugin) { memset(plugin, 0, sizeof(mjsPlugin)); } - diff --git a/src/user/user_model.cc b/src/user/user_model.cc index 93449339..f705551c 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -2675,6 +2675,8 @@ void mjCModel::CopyTree(mjModel* m) { mjuu_copyvec(m->light_pos+3*lid, pl->pos, 3); mjuu_copyvec(m->light_dir+3*lid, pl->dir, 3); m->light_bulbradius[lid] = pl->bulbradius; + m->light_intensity[lid] = pl->intensity; + m->light_range[lid] = pl->range; mjuu_copyvec(m->light_attenuation+3*lid, pl->attenuation, 3); m->light_cutoff[lid] = pl->cutoff; m->light_exponent[lid] = pl->exponent; diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 8404364a..85723a6e 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -167,8 +167,9 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { {"camera", "?", "17", "orthographic", "fovy", "ipd", "resolution", "pos", "quat", "axisangle", "xyaxes", "zaxis", "euler", "mode", "focal", "focalpixel", "principal", "principalpixel", "sensorsize", "user"}, - {"light", "?", "13", "pos", "dir", "bulbradius", "directional", "castshadow", "active", - "attenuation", "cutoff", "exponent", "ambient", "diffuse", "specular", "mode"}, + {"light", "?", "15", "pos", "dir", "bulbradius", "intensity", "range", + "directional", "castshadow", "active", "attenuation", "cutoff", "exponent", + "ambient", "diffuse", "specular", "mode"}, {"pair", "?", "7", "condim", "friction", "solref", "solreffriction", "solimp", "gap", "margin"}, {"equality", "?", "3", "active", "solref", "solimp"}, @@ -280,9 +281,9 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { {"camera", "*", "20", "name", "class", "orthographic", "fovy", "ipd", "resolution", "pos", "quat", "axisangle", "xyaxes", "zaxis", "euler", "mode", "target", "focal", "focalpixel", "principal", "principalpixel", "sensorsize", "user"}, - {"light", "*", "16", "name", "class", "directional", "castshadow", "active", - "pos", "dir", "bulbradius", "attenuation", "cutoff", "exponent", "ambient", "diffuse", - "specular", "mode", "target"}, + {"light", "*", "18", "name", "class", "directional", "castshadow", "active", + "pos", "dir", "bulbradius", "intensity", "range", "attenuation", "cutoff", + "exponent", "ambient", "diffuse", "specular", "mode", "target"}, {"plugin", "*", "2", "plugin", "instance"}, {"<"}, {"config", "*", "2", "key", "value"}, @@ -1856,6 +1857,8 @@ void mjXReader::OneLight(XMLElement* elem, mjsLight* light) { ReadAttr(elem, "pos", 3, light->pos, text); ReadAttr(elem, "dir", 3, light->dir, text); ReadAttr(elem, "bulbradius", 1, &light->bulbradius, text); + ReadAttr(elem, "intensity", 1, &light->intensity, text); + ReadAttr(elem, "range", 1, &light->range, text); ReadAttr(elem, "attenuation", 3, light->attenuation, text); ReadAttr(elem, "cutoff", 1, &light->cutoff, text); ReadAttr(elem, "exponent", 1, &light->exponent, text); diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index bf5093c5..a7f27871 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -604,6 +604,8 @@ void mjXWriter::OneLight(XMLElement* elem, const mjCLight* light, mjCDef* def, // defaults and regular WriteAttr(elem, "bulbradius", 1, &light->bulbradius, &def->Light().bulbradius); + WriteAttr(elem, "intensity", 1, &light->intensity, &def->Light().intensity); + WriteAttr(elem, "range", 1, &light->range, &def->Light().range); WriteAttrKey(elem, "directional", bool_map, 2, light->directional, def->Light().directional); WriteAttrKey(elem, "castshadow", bool_map, 2, light->castshadow, def->Light().castshadow); WriteAttrKey(elem, "active", bool_map, 2, light->active, def->Light().active); diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index d743436c..a77daf2e 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -5433,6 +5433,8 @@ public unsafe struct mjModel_ { public byte* light_directional; public byte* light_castshadow; public float* light_bulbradius; + public float* light_intensity; + public float* light_range; public byte* light_active; public double* light_pos; public double* light_dir; @@ -6077,6 +6079,8 @@ public unsafe struct mjvLight_ { public byte directional; public byte castshadow; public float bulbradius; + public float intensity; + public float range; } [StructLayout(LayoutKind.Sequential)] @@ -6357,6 +6361,8 @@ public unsafe struct model { public byte* light_directional; public byte* light_castshadow; public float* light_bulbradius; + public float* light_intensity; + public float* light_range; public byte* light_active; public float* light_attenuation; public float* light_cutoff; From 1165018f7131edabe3caf5ef2bc68df9a6c87abb Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Wed, 14 May 2025 04:19:46 -0700 Subject: [PATCH 128/191] Add island support to Newton solver PiperOrigin-RevId: 758618486 Change-Id: I257d78c7dc9aa4dbf7cd6d8849d8ec48201edf24 --- src/engine/engine_forward.c | 28 +++++++++++++++++----------- src/engine/engine_solver.c | 8 +++++++- src/engine/engine_solver.h | 4 ++++ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/engine/engine_forward.c b/src/engine/engine_forward.c index 6b23111f..65dad884 100644 --- a/src/engine/engine_forward.c +++ b/src/engine/engine_forward.c @@ -723,16 +723,16 @@ void mj_fwdConstraint(const mjModel* m, mjData* d) { mju_zeroInt(d->solver_niter, mjNISLAND); // check if islands are supported - int islands_supported = mjENABLED(mjENBL_ISLAND) && - nisland > 0 && - m->opt.solver == mjSOL_CG && - m->opt.noslip_iterations == 0; + int islands_supported = mjENABLED(mjENBL_ISLAND) && + nisland > 0 && + m->opt.noslip_iterations == 0 && + (m->opt.solver == mjSOL_CG || m->opt.solver == mjSOL_NEWTON); // run solver over constraint islands if (islands_supported) { int nidof = d->nidof; - // copy CG inputs to islands (vel+acc deps, pos-dependent already copied in mj_island) + // copy inputs to islands (vel+acc deps, pos-dependent already copied in mj_island) mju_gather(d->ifrc_smooth, d->qfrc_smooth, d->map_idof2dof, nidof); mju_gather(d->ifrc_constraint, d->qfrc_constraint, d->map_idof2dof, nidof); mju_gather(d->iacc_smooth, d->qacc_smooth, d->map_idof2dof, nidof); @@ -741,14 +741,20 @@ void mj_fwdConstraint(const mjModel* m, mjData* d) { mju_gather(d->iefc_aref, d->efc_aref, d->map_iefc2efc, nefc); // solve per island - if (!d->threadpool) { - // no threadpool, loop over islands - for (int island=0; island < nisland; island++) { - mj_solCG_island(m, d, island, m->opt.iterations); + if (m->opt.solver == mjSOL_CG) { + if (!d->threadpool) { + // no threadpool, loop over islands + for (int island=0; island < nisland; island++) { + mj_solCG_island(m, d, island, m->opt.iterations); + } + } else { + // have threadpool, solve using threads + mj_solCG_island_multithreaded(m, d); } } else { - // have threadpool, solve using threads - mj_solCG_island_multithreaded(m, d); + for (int island=0; island < nisland; island++) { + mj_solNewton_island(m, d, island, m->opt.iterations); + } } // copy back solver outputs (scatter dofs since ni <= nv) diff --git a/src/engine/engine_solver.c b/src/engine/engine_solver.c index adb1e23d..be4c5c3b 100644 --- a/src/engine/engine_solver.c +++ b/src/engine/engine_solver.c @@ -1058,7 +1058,6 @@ static void CGupdateGradient(mjCGContext* ctx, int flg_Newton) { } // Newton: Mgrad = H \ grad - // TODO: b/295296178 - add island support to Newton solver if (flg_Newton) { if (ctx->is_sparse) { mju_cholSolveSparse(ctx->Mgrad, (ctx->ncone ? ctx->Lcone : ctx->L), @@ -2042,3 +2041,10 @@ void mj_solCG_island(const mjModel* m, mjData* d, int island, int maxiter) { void mj_solNewton(const mjModel* m, mjData* d, int maxiter) { mj_solCGNewton(m, d, /*island=*/-1, maxiter, /*flg_Newton=*/1); } + + + +// Newton entry point (one island) +void mj_solNewton_island(const mjModel* m, mjData* d, int island, int maxiter) { + mj_solCGNewton(m, d, island, maxiter, /*flg_Newton=*/1); +} diff --git a/src/engine/engine_solver.h b/src/engine/engine_solver.h index 6947489a..7ee007de 100644 --- a/src/engine/engine_solver.h +++ b/src/engine/engine_solver.h @@ -32,9 +32,13 @@ void mj_solCG(const mjModel* m, mjData* d, int maxiter); // Newton solver void mj_solNewton(const mjModel* m, mjData* d, int maxiter); + //------------------------------ per-island solvers ------------------------------------------------ // CG solver void mj_solCG_island(const mjModel* m, mjData* d, int island, int maxiter); +// Newton entry point +void mj_solNewton_island(const mjModel* m, mjData* d, int island, int maxiter); + #endif // MUJOCO_SRC_ENGINE_ENGINE_SOLVER_H_ From 4ba04586ae78fe811fac14e3ba48c14469aeda85 Mon Sep 17 00:00:00 2001 From: Silvia Cruciani Date: Wed, 14 May 2025 04:35:56 -0700 Subject: [PATCH 129/191] Fix setting slices of multidimensional arrays for multiple targets PiperOrigin-RevId: 758623598 Change-Id: Ie05b36c25be74ddf7451a95377caf1ed1d692141 --- mjx/mujoco/mjx/_src/support.py | 3 ++- mjx/mujoco/mjx/_src/support_test.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/mjx/mujoco/mjx/_src/support.py b/mjx/mujoco/mjx/_src/support.py index d68c0407..aea5bced 100644 --- a/mjx/mujoco/mjx/_src/support.py +++ b/mjx/mujoco/mjx/_src/support.py @@ -499,12 +499,13 @@ class BindData(object): adr = [adr] num = [num] elif isinstance(self.id, list): - adr = self.id * dim + adr = (np.array(self.id) * dim).tolist() num = [dim for _ in range(len(self.id))] else: adr = [self.id * dim] num = [dim] i = 0 + value = jax.numpy.array(value).flatten() for a, n in zip(adr, num): shape = array.shape array = array.flatten().at[a : a + n].set(value[i : i + n]).reshape(shape) diff --git a/mjx/mujoco/mjx/_src/support_test.py b/mjx/mujoco/mjx/_src/support_test.py index 5f060bc2..38392d38 100644 --- a/mjx/mujoco/mjx/_src/support_test.py +++ b/mjx/mujoco/mjx/_src/support_test.py @@ -308,6 +308,16 @@ class SupportTest(parameterized.TestCase): np.testing.assert_array_equal( dx7.bind(mx, body).xfrc_applied, [0, 0, 0, 0, 0, 0] ) + dx10 = dx.bind(mx, s.bodies[0:2]).set( + 'xfrc_applied', + np.array([np.array([1, 1, 1, 1, 1, 1]), np.array([2, 2, 2, 2, 2, 2])]), + ) + np.testing.assert_array_equal( + dx10.bind(mx, s.bodies[0]).xfrc_applied, [1, 1, 1, 1, 1, 1] + ) + np.testing.assert_array_equal( + dx10.bind(mx, s.bodies[1]).xfrc_applied, [2, 2, 2, 2, 2, 2] + ) # test attribute and type mismatches with self.assertRaisesRegex( From dd28b887d4b94c68de9f998cada3a643b5666d2b Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Wed, 14 May 2025 05:25:11 -0700 Subject: [PATCH 130/191] Rename `C` sparse structure to `M` in `mjdata`, improve docstrings. PiperOrigin-RevId: 758636638 Change-Id: If78acc423601d2911f514929b27f7b6d0af9ef58 --- doc/includes/references.h | 24 ++++++++--------- include/mujoco/mjdata.h | 24 ++++++++--------- include/mujoco/mjxmacro.h | 8 +++--- mjx/mujoco/mjx/_src/io.py | 8 +++--- mjx/mujoco/mjx/_src/smooth_test.py | 2 +- mjx/mujoco/mjx/_src/types.py | 8 +++--- python/mujoco/introspect/structs.py | 32 +++++++++++----------- src/engine/engine_core_constraint.c | 12 ++++----- src/engine/engine_core_smooth.c | 12 ++++----- src/engine/engine_forward.c | 12 ++++----- src/engine/engine_io.c | 12 ++++----- src/engine/engine_island.c | 2 +- src/engine/engine_print.c | 34 ++++++++++++------------ src/engine/engine_solver.c | 6 ++--- src/engine/engine_support.c | 10 +++---- test/benchmark/factorI_benchmark_test.cc | 4 +-- test/benchmark/inertia_benchmark_test.cc | 6 ++--- test/benchmark/solveLD_benchmark_test.cc | 4 +-- test/engine/engine_core_smooth_test.cc | 24 ++++++++--------- test/engine/engine_derivative_test.cc | 4 +-- unity/Runtime/Bindings/MjBindings.cs | 8 +++--- 21 files changed, 128 insertions(+), 128 deletions(-) diff --git a/doc/includes/references.h b/doc/includes/references.h index c8349418..0622ff60 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -269,8 +269,8 @@ struct mjData_ { // computed by mj_fwdPosition/mj_crb mjtNum* crb; // com-based composite inertia and mass (nbody x 10) - mjtNum* qM; // total inertia (sparse) (nM x 1) - mjtNum* M; // total inertia (compressed sparse row) (nC x 1) + mjtNum* qM; // inertia (sparse) (nM x 1) + mjtNum* M; // reduced inertia (compressed sparse row) (nC x 1) // computed by mj_fwdPosition/mj_factorM mjtNum* qLD; // L'*D*L factorization of M (sparse) (nC x 1) @@ -313,16 +313,16 @@ struct mjData_ { int* B_rownnz; // body-dof: non-zeros in each row (nbody x 1) int* B_rowadr; // body-dof: address of each row in B_colind (nbody x 1) int* B_colind; // body-dof: column indices of non-zeros (nB x 1) - int* C_rownnz; // reduced dof-dof: non-zeros in each row (nv x 1) - int* C_rowadr; // reduced dof-dof: address of each row in C_colind (nv x 1) - int* C_colind; // reduced dof-dof: column indices of non-zeros (nC x 1) - int* mapM2C; // index mapping from M to C (nC x 1) - int* D_rownnz; // dof-dof: non-zeros in each row (nv x 1) - int* D_rowadr; // dof-dof: address of each row in D_colind (nv x 1) - int* D_diag; // dof-dof: index of diagonal element (nv x 1) - int* D_colind; // dof-dof: column indices of non-zeros (nD x 1) - int* mapM2D; // index mapping from M to D (nD x 1) - int* mapD2M; // index mapping from D to M (nM x 1) + int* M_rownnz; // reduced inertia: non-zeros in each row (nv x 1) + int* M_rowadr; // reduced inertia: address of each row in M_colind (nv x 1) + int* M_colind; // reduced inertia: column indices of non-zeros (nC x 1) + int* mapM2M; // index mapping from qM to M (nC x 1) + int* D_rownnz; // full inertia: non-zeros in each row (nv x 1) + int* D_rowadr; // full inertia: address of each row in D_colind (nv x 1) + int* D_diag; // full inertia: index of diagonal element (nv x 1) + int* D_colind; // full inertia: column indices of non-zeros (nD x 1) + int* mapM2D; // index mapping from qM to D (nD x 1) + int* mapD2M; // index mapping from D to qM (nM x 1) // computed by mj_implicit/mj_derivative mjtNum* qDeriv; // d (passive + actuator - bias) / d qvel (nD x 1) diff --git a/include/mujoco/mjdata.h b/include/mujoco/mjdata.h index 3d7514a6..ac780c82 100644 --- a/include/mujoco/mjdata.h +++ b/include/mujoco/mjdata.h @@ -297,8 +297,8 @@ struct mjData_ { // computed by mj_fwdPosition/mj_crb mjtNum* crb; // com-based composite inertia and mass (nbody x 10) - mjtNum* qM; // total inertia (sparse) (nM x 1) - mjtNum* M; // total inertia (compressed sparse row) (nC x 1) + mjtNum* qM; // inertia (sparse) (nM x 1) + mjtNum* M; // reduced inertia (compressed sparse row) (nC x 1) // computed by mj_fwdPosition/mj_factorM mjtNum* qLD; // L'*D*L factorization of M (sparse) (nC x 1) @@ -341,16 +341,16 @@ struct mjData_ { int* B_rownnz; // body-dof: non-zeros in each row (nbody x 1) int* B_rowadr; // body-dof: address of each row in B_colind (nbody x 1) int* B_colind; // body-dof: column indices of non-zeros (nB x 1) - int* C_rownnz; // reduced dof-dof: non-zeros in each row (nv x 1) - int* C_rowadr; // reduced dof-dof: address of each row in C_colind (nv x 1) - int* C_colind; // reduced dof-dof: column indices of non-zeros (nC x 1) - int* mapM2C; // index mapping from M to C (nC x 1) - int* D_rownnz; // dof-dof: non-zeros in each row (nv x 1) - int* D_rowadr; // dof-dof: address of each row in D_colind (nv x 1) - int* D_diag; // dof-dof: index of diagonal element (nv x 1) - int* D_colind; // dof-dof: column indices of non-zeros (nD x 1) - int* mapM2D; // index mapping from M to D (nD x 1) - int* mapD2M; // index mapping from D to M (nM x 1) + int* M_rownnz; // reduced inertia: non-zeros in each row (nv x 1) + int* M_rowadr; // reduced inertia: address of each row in M_colind (nv x 1) + int* M_colind; // reduced inertia: column indices of non-zeros (nC x 1) + int* mapM2M; // index mapping from qM to M (nC x 1) + int* D_rownnz; // full inertia: non-zeros in each row (nv x 1) + int* D_rowadr; // full inertia: address of each row in D_colind (nv x 1) + int* D_diag; // full inertia: index of diagonal element (nv x 1) + int* D_colind; // full inertia: column indices of non-zeros (nD x 1) + int* mapM2D; // index mapping from qM to D (nD x 1) + int* mapD2M; // index mapping from D to qM (nM x 1) // computed by mj_implicit/mj_derivative mjtNum* qDeriv; // d (passive + actuator - bias) / d qvel (nD x 1) diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index bbd79ad1..c7ce4861 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -679,10 +679,10 @@ X ( int, B_rownnz, nbody, 1 ) \ X ( int, B_rowadr, nbody, 1 ) \ X ( int, B_colind, nB, 1 ) \ - X ( int, C_rownnz, nv, 1 ) \ - X ( int, C_rowadr, nv, 1 ) \ - X ( int, C_colind, nC, 1 ) \ - X ( int, mapM2C, nC, 1 ) \ + X ( int, M_rownnz, nv, 1 ) \ + X ( int, M_rowadr, nv, 1 ) \ + X ( int, M_colind, nC, 1 ) \ + X ( int, mapM2M, nC, 1 ) \ X ( int, D_rownnz, nv, 1 ) \ X ( int, D_rowadr, nv, 1 ) \ X ( int, D_diag, nv, 1 ) \ diff --git a/mjx/mujoco/mjx/_src/io.py b/mjx/mujoco/mjx/_src/io.py index 66919152..c919f7f4 100644 --- a/mjx/mujoco/mjx/_src/io.py +++ b/mjx/mujoco/mjx/_src/io.py @@ -629,10 +629,10 @@ def _make_data_c( 'B_rownnz': (m.nbody, np.int32), 'B_rowadr': (m.nbody, np.int32), 'B_colind': (m.nB, np.int32), - 'C_rownnz': (m.nv, np.int32), - 'C_rowadr': (m.nv, np.int32), - 'C_colind': (m.nC, np.int32), - 'mapM2C': (m.nC, np.int32), + 'M_rownnz': (m.nv, np.int32), + 'M_rowadr': (m.nv, np.int32), + 'M_colind': (m.nC, np.int32), + 'mapM2M': (m.nC, np.int32), 'D_rownnz': (m.nv, np.int32), 'D_rowadr': (m.nv, np.int32), 'D_diag': (m.nv, np.int32), diff --git a/mjx/mujoco/mjx/_src/smooth_test.py b/mjx/mujoco/mjx/_src/smooth_test.py index 9e23fb68..96470a21 100644 --- a/mjx/mujoco/mjx/_src/smooth_test.py +++ b/mjx/mujoco/mjx/_src/smooth_test.py @@ -92,7 +92,7 @@ class SmoothTest(absltest.TestCase): dx = jax.jit(mjx.factor_m)(mx, mjx.put_data(m, d)) qLDLegacy = np.zeros(mx.nM) # pylint:disable=invalid-name for i in range(m.nC): - qLDLegacy[d.mapM2C[i]] = d.qLD[i] + qLDLegacy[d.mapM2M[i]] = d.qLD[i] _assert_eq(qLDLegacy, dx._impl.qLD, 'qLD') _assert_attr_eq(d, dx._impl, 'qLDiagInv') # com_vel diff --git a/mjx/mujoco/mjx/_src/types.py b/mjx/mujoco/mjx/_src/types.py index 22211378..2950fb4a 100644 --- a/mjx/mujoco/mjx/_src/types.py +++ b/mjx/mujoco/mjx/_src/types.py @@ -985,10 +985,10 @@ class DataC(PyTreeNode): B_rownnz: jax.Array # pylint:disable=invalid-name B_rowadr: jax.Array # pylint:disable=invalid-name B_colind: jax.Array # pylint:disable=invalid-name - C_rownnz: jax.Array # pylint:disable=invalid-name - C_rowadr: jax.Array # pylint:disable=invalid-name - C_colind: jax.Array # pylint:disable=invalid-name - mapM2C: jax.Array # pylint:disable=invalid-name + M_rownnz: jax.Array # pylint:disable=invalid-name + M_rowadr: jax.Array # pylint:disable=invalid-name + M_colind: jax.Array # pylint:disable=invalid-name + mapM2M: jax.Array # pylint:disable=invalid-name D_rownnz: jax.Array # pylint:disable=invalid-name D_rowadr: jax.Array # pylint:disable=invalid-name D_diag: jax.Array # pylint:disable=invalid-name diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index e8044c71..4d45dc9e 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -5413,7 +5413,7 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=PointerType( inner_type=ValueType(name='mjtNum'), ), - doc='total inertia (sparse)', + doc='inertia (sparse)', array_extent=('nM',), ), StructFieldDecl( @@ -5421,7 +5421,7 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=PointerType( inner_type=ValueType(name='mjtNum'), ), - doc='total inertia (compressed sparse row)', + doc='reduced inertia (compressed sparse row)', array_extent=('nC',), ), StructFieldDecl( @@ -5601,35 +5601,35 @@ STRUCTS: Mapping[str, StructDecl] = dict([ array_extent=('nB',), ), StructFieldDecl( - name='C_rownnz', + name='M_rownnz', type=PointerType( inner_type=ValueType(name='int'), ), - doc='reduced dof-dof: non-zeros in each row', + doc='reduced inertia: non-zeros in each row', array_extent=('nv',), ), StructFieldDecl( - name='C_rowadr', + name='M_rowadr', type=PointerType( inner_type=ValueType(name='int'), ), - doc='reduced dof-dof: address of each row in C_colind', + doc='reduced inertia: address of each row in M_colind', array_extent=('nv',), ), StructFieldDecl( - name='C_colind', + name='M_colind', type=PointerType( inner_type=ValueType(name='int'), ), - doc='reduced dof-dof: column indices of non-zeros', + doc='reduced inertia: column indices of non-zeros', array_extent=('nC',), ), StructFieldDecl( - name='mapM2C', + name='mapM2M', type=PointerType( inner_type=ValueType(name='int'), ), - doc='index mapping from M to C', + doc='index mapping from qM to M', array_extent=('nC',), ), StructFieldDecl( @@ -5637,7 +5637,7 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=PointerType( inner_type=ValueType(name='int'), ), - doc='dof-dof: non-zeros in each row', + doc='full inertia: non-zeros in each row', array_extent=('nv',), ), StructFieldDecl( @@ -5645,7 +5645,7 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=PointerType( inner_type=ValueType(name='int'), ), - doc='dof-dof: address of each row in D_colind', + doc='full inertia: address of each row in D_colind', array_extent=('nv',), ), StructFieldDecl( @@ -5653,7 +5653,7 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=PointerType( inner_type=ValueType(name='int'), ), - doc='dof-dof: index of diagonal element', + doc='full inertia: index of diagonal element', array_extent=('nv',), ), StructFieldDecl( @@ -5661,7 +5661,7 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=PointerType( inner_type=ValueType(name='int'), ), - doc='dof-dof: column indices of non-zeros', + doc='full inertia: column indices of non-zeros', array_extent=('nD',), ), StructFieldDecl( @@ -5669,7 +5669,7 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=PointerType( inner_type=ValueType(name='int'), ), - doc='index mapping from M to D', + doc='index mapping from qM to D', array_extent=('nD',), ), StructFieldDecl( @@ -5677,7 +5677,7 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=PointerType( inner_type=ValueType(name='int'), ), - doc='index mapping from D to M', + doc='index mapping from D to qM', array_extent=('nM',), ), StructFieldDecl( diff --git a/src/engine/engine_core_constraint.c b/src/engine/engine_core_constraint.c index aa7267ed..1a60b6e6 100644 --- a/src/engine/engine_core_constraint.c +++ b/src/engine/engine_core_constraint.c @@ -2039,7 +2039,7 @@ void mj_projectConstraint(const mjModel* m, mjData* d) { // inverse square root of D from inertia LDL decomposition mjtNum* sqrtInvD = mjSTACKALLOC(d, nv, mjtNum); for (int i=0; i < nv; i++) { - int diag = d->C_rowadr[i] + d->C_rownnz[i] - 1; + int diag = d->M_rowadr[i] + d->M_rownnz[i] - 1; sqrtInvD[i] = 1 / mju_sqrt(d->qLD[diag]); } @@ -2076,10 +2076,10 @@ void mj_projectConstraint(const mjModel* m, mjData* d) { } // traverse row j of C, marking new unique nonzeros - int nnzC = d->C_rownnz[j]; - int adrC = d->C_rowadr[j]; + int nnzC = d->M_rownnz[j]; + int adrC = d->M_rowadr[j]; for (int k=0; k < nnzC; k++) { - int c = d->C_colind[adrC + k]; + int c = d->M_colind[adrC + k]; if (marker[c] != r) { marker[c] = r; nnz++; @@ -2159,10 +2159,10 @@ void mj_projectConstraint(const mjModel* m, mjData* d) { continue; } int j = B_colind[i]; - int adrC = d->C_rowadr[j]; + int adrC = d->M_rowadr[j]; mju_addToSclSparseInc(B + adrB, d->qLD + adrC, nnzB, B_colind + adrB, - d->C_rownnz[j]-1, d->C_colind + adrC, -b); + d->M_rownnz[j]-1, d->M_colind + adrC, -b); } // B(r,:) <- sqrt(inv(D)) * B(r,:) diff --git a/src/engine/engine_core_smooth.c b/src/engine/engine_core_smooth.c index 6e5b03f7..0f104122 100644 --- a/src/engine/engine_core_smooth.c +++ b/src/engine/engine_core_smooth.c @@ -1579,7 +1579,7 @@ void mj_makeM(const mjModel* m, mjData* d) { TM_START; mj_crb(m, d); mj_tendonArmature(m, d); - mju_gather(d->M, d->qM, d->mapM2C, m->nC); + mju_gather(d->M, d->qM, d->mapM2M, m->nC); TM_END(mjTIMER_POS_INERTIA); } @@ -1653,7 +1653,7 @@ void mj_factorI_legacy(const mjModel* m, mjData* d, const mjtNum* M, mjtNum* qLD void mj_factorM(const mjModel* m, mjData* d) { TM_START; mju_copy(d->qLD, d->M, m->nC); - mj_factorI(d->qLD, d->qLDiagInv, m->nv, d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); + mj_factorI(d->qLD, d->qLDiagInv, m->nv, d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); TM_ADD(mjTIMER_POS_INERTIA); } @@ -1895,7 +1895,7 @@ void mj_solveM(const mjModel* m, mjData* d, mjtNum* x, const mjtNum* y, int n) { mju_copy(x, y, n*m->nv); } mj_solveLD(x, d->qLD, d->qLDiagInv, m->nv, n, - d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); + d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); } @@ -1906,9 +1906,9 @@ void mj_solveM2(const mjModel* m, mjData* d, mjtNum* x, const mjtNum* y, int nv = m->nv; // local copies of key variables - const int* rownnz = d->C_rownnz; - const int* rowadr = d->C_rowadr; - const int* colind = d->C_colind; + const int* rownnz = d->M_rownnz; + const int* rowadr = d->M_rowadr; + const int* colind = d->M_colind; const int* diagnum = m->dof_simplenum; const mjtNum* qLD = d->qLD; diff --git a/src/engine/engine_forward.c b/src/engine/engine_forward.c index 65dad884..df22386f 100644 --- a/src/engine/engine_forward.c +++ b/src/engine/engine_forward.c @@ -869,18 +869,18 @@ void mj_EulerSkip(const mjModel* m, mjData* d, int skipfactor) { // qH = M + h*diag(B) mju_copy(d->qH, d->M, nC); for (int i=0; i < nv; i++) { - d->qH[d->C_rowadr[i] + d->C_rownnz[i] - 1] += m->opt.timestep * m->dof_damping[i]; + d->qH[d->M_rowadr[i] + d->M_rownnz[i] - 1] += m->opt.timestep * m->dof_damping[i]; } // factorize in-place - mj_factorI(d->qH, d->qHDiagInv, nv, d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); + mj_factorI(d->qH, d->qHDiagInv, nv, d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); } // solve mju_add(qfrc, d->qfrc_smooth, d->qfrc_constraint, nv); mju_copy(qacc, qfrc, m->nv); mj_solveLD(qacc, d->qH, d->qHDiagInv, nv, 1, - d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); + d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); } // advance state and time @@ -1053,16 +1053,16 @@ void mj_implicitSkip(const mjModel* m, mjData* d, int skipfactor) { mju_addScl(MhB, d->qM, MhB, -m->opt.timestep, nM); // gather qH <- MhB (legacy to CSR) - mju_gather(d->qH, MhB, d->mapM2C, nC); + mju_gather(d->qH, MhB, d->mapM2M, nC); // factorize in-place - mj_factorI(d->qH, d->qHDiagInv, nv, d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); + mj_factorI(d->qH, d->qHDiagInv, nv, d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); } // solve for qacc: (qM - dt*qDeriv) * qacc = qfrc mju_copy(qacc, qfrc, nv); mj_solveLD(qacc, d->qH, d->qHDiagInv, nv, 1, - d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); + d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); } else { mjERROR("integrator must be implicit or implicitfast"); diff --git a/src/engine/engine_io.c b/src/engine/engine_io.c index 56e7d43f..d090f229 100644 --- a/src/engine/engine_io.c +++ b/src/engine/engine_io.c @@ -1139,8 +1139,8 @@ static void copyM2Sparse(const mjModel* m, mjData* d, int* dst, const int* src, const int* rownnz; const int* rowadr; if (reduced && !upper) { - rownnz = d->C_rownnz; - rowadr = d->C_rowadr; + rownnz = d->M_rownnz; + rowadr = d->M_rowadr; } else if (!reduced && upper) { rownnz = d->D_rownnz; rowadr = d->D_rowadr; @@ -1248,12 +1248,12 @@ static void makeDofDofmaps(const mjModel* m, mjData* d) { } // make mapM2C - for (int i=0; i < nC; i++) d->mapM2C[i] = -1; - copyM2Sparse(m, d, d->mapM2C, M, /*reduced=*/1, /*upper=*/0); + for (int i=0; i < nC; i++) d->mapM2M[i] = -1; + copyM2Sparse(m, d, d->mapM2M, M, /*reduced=*/1, /*upper=*/0); // check that all indices are filled in for (int i=0; i < nC; i++) { - if (d->mapM2C[i] < 0) { + if (d->mapM2M[i] < 0) { mjERROR("unassigned index in mapM2C"); } } @@ -1976,7 +1976,7 @@ static void _resetData(const mjModel* m, mjData* d, unsigned char debug_value) { checkDBSparse(m, d); // make C - makeDofDofSparse(m, d, d->C_rownnz, d->C_rowadr, NULL, d->C_colind, /*reduced=*/1, /*upper=*/0); + makeDofDofSparse(m, d, d->M_rownnz, d->M_rowadr, NULL, d->M_colind, /*reduced=*/1, /*upper=*/0); // make index mappings: mapM2D, mapD2M, mapM2C, mapM2M makeDofDofmaps(m, d); diff --git a/src/engine/engine_island.c b/src/engine/engine_island.c index 4d162972..d38943d4 100644 --- a/src/engine/engine_island.c +++ b/src/engine/engine_island.c @@ -537,7 +537,7 @@ void mj_island(const mjModel* m, mjData* d) { // inertia: block-diagonalize both iLD <- qLD and iM <- qM mju_blockDiagSparse(d->iLD, d->iM_rownnz, d->iM_rowadr, d->iM_colind, - d->qLD, d->C_rownnz, d->C_rowadr, d->C_colind, + d->qLD, d->M_rownnz, d->M_rowadr, d->M_colind, nidof, nisland, d->map_idof2dof, d->map_dof2idof, d->island_idofadr, d->island_idofadr, diff --git a/src/engine/engine_print.c b/src/engine/engine_print.c index ef02d1ed..d6e5204e 100644 --- a/src/engine/engine_print.c +++ b/src/engine/engine_print.c @@ -1124,14 +1124,14 @@ void mj_printFormattedData(const mjModel* m, const mjData* d, const char* filena d->moment_rowadr, d->moment_colind, fp, float_format); printArray("CRB", m->nbody, 10, d->crb, fp, float_format); printInertia("QM", d->qM, m, fp, float_format); - printSparse("M", d->M, m->nv, d->C_rownnz, - d->C_rowadr, d->C_colind, fp, float_format); - printSparse("QLD", d->qLD, m->nv, d->C_rownnz, - d->C_rowadr, d->C_colind, fp, float_format); + printSparse("M", d->M, m->nv, d->M_rownnz, + d->M_rowadr, d->M_colind, fp, float_format); + printSparse("QLD", d->qLD, m->nv, d->M_rownnz, + d->M_rowadr, d->M_colind, fp, float_format); printArray("QLDIAGINV", m->nv, 1, d->qLDiagInv, fp, float_format); if (!mju_isZero(d->qHDiagInv, m->nv)) { - printSparse("QH", d->qH, m->nv, d->C_rownnz, d->C_rowadr, d->C_colind, fp, float_format); + printSparse("QH", d->qH, m->nv, d->M_rownnz, d->M_rowadr, d->M_colind, fp, float_format); printArray("QHDIAGINV", m->nv, 1, d->qHDiagInv, fp, float_format); } @@ -1160,34 +1160,34 @@ void mj_printFormattedData(const mjModel* m, const mjData* d, const char* filena } fprintf(fp, "\n\n"); - // C sparse structure - mj_printSparsity("C: reduced dof-dof matrix", m->nv, m->nv, d->C_rowadr, NULL, d->C_rownnz, - NULL, d->C_colind, fp); + // M sparse structure + mj_printSparsity("M: reduced inertia matrix", m->nv, m->nv, d->M_rowadr, NULL, d->M_rownnz, + NULL, d->M_colind, fp); - fprintf(fp, NAME_FORMAT, "C_rownnz"); + fprintf(fp, NAME_FORMAT, "M_rownnz"); for (int i = 0; i < m->nv; i++) { - fprintf(fp, " %d", d->C_rownnz[i]); + fprintf(fp, " %d", d->M_rownnz[i]); } fprintf(fp, "\n\n"); // C_rowadr - fprintf(fp, NAME_FORMAT, "C_rowadr"); + fprintf(fp, NAME_FORMAT, "M_rowadr"); for (int i = 0; i < m->nv; i++) { - fprintf(fp, " %d", d->C_rowadr[i]); + fprintf(fp, " %d", d->M_rowadr[i]); } fprintf(fp, "\n\n"); // C_colind - fprintf(fp, NAME_FORMAT, "C_colind"); + fprintf(fp, NAME_FORMAT, "M_colind"); for (int i = 0; i < m->nC; i++) { - fprintf(fp, " %d", d->C_colind[i]); + fprintf(fp, " %d", d->M_colind[i]); } fprintf(fp, "\n\n"); - // mapM2C - fprintf(fp, NAME_FORMAT, "mapM2C"); + // mapM2M + fprintf(fp, NAME_FORMAT, "mapM2M"); for (int i = 0; i < m->nC; i++) { - fprintf(fp, " %d", d->mapM2C[i]); + fprintf(fp, " %d", d->mapM2M[i]); } fprintf(fp, "\n\n"); diff --git a/src/engine/engine_solver.c b/src/engine/engine_solver.c index be4c5c3b..2b111e9e 100644 --- a/src/engine/engine_solver.c +++ b/src/engine/engine_solver.c @@ -886,10 +886,10 @@ static void CGpointers(const mjModel* m, const mjData* d, mjCGContext* ctx, int ctx->qacc = d->qacc; // inertia - ctx->M_rownnz = d->C_rownnz; - ctx->M_rowadr = d->C_rowadr; + ctx->M_rownnz = d->M_rownnz; + ctx->M_rowadr = d->M_rowadr; ctx->M_diagnum = m->dof_simplenum; - ctx->M_colind = d->C_colind; + ctx->M_colind = d->M_colind; ctx->M = d->M; ctx->qLD = d->qLD; ctx->qLDiagInv = d->qLDiagInv; diff --git a/src/engine/engine_support.c b/src/engine/engine_support.c index 362292da..9836ced9 100644 --- a/src/engine/engine_support.c +++ b/src/engine/engine_support.c @@ -1047,14 +1047,14 @@ void mj_mulM2(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec) // non-simple: add off-diagonals if (!m->dof_simplenum[i]) { - int adr = d->C_rowadr[i]; - res[i] += mju_dotSparse(qLD+adr, vec, d->C_rownnz[i] - 1, d->C_colind+adr); + int adr = d->M_rowadr[i]; + res[i] += mju_dotSparse(qLD+adr, vec, d->M_rownnz[i] - 1, d->M_colind+adr); } } // res *= sqrt(D) for (int i=0; i < nv; i++) { - int diag = d->C_rowadr[i] + d->C_rownnz[i] - 1; + int diag = d->M_rowadr[i] + d->M_rownnz[i] - 1; res[i] *= mju_sqrt(qLD[diag]); } } @@ -1072,10 +1072,10 @@ void mj_addM(const mjModel* m, mjData* d, mjtNum* dst, // gather C <- qM (legacy to CSR) mjtNum* C = mjSTACKALLOC(d, nC, mjtNum); - mju_gather(C, d->qM, d->mapM2C, nC); + mju_gather(C, d->qM, d->mapM2M, nC); // add to dst - mj_addMSparse(m, d, dst, rownnz, rowadr, colind, C, d->C_rownnz, d->C_rowadr, d->C_colind); + mj_addMSparse(m, d, dst, rownnz, rowadr, colind, C, d->M_rownnz, d->M_rowadr, d->M_colind); mj_freeStack(d); } diff --git a/test/benchmark/factorI_benchmark_test.cc b/test/benchmark/factorI_benchmark_test.cc index 998af271..1508edd7 100644 --- a/test/benchmark/factorI_benchmark_test.cc +++ b/test/benchmark/factorI_benchmark_test.cc @@ -46,7 +46,7 @@ static void BM_factorI(benchmark::State& state, bool legacy, bool coil) { // M: mass matrix in CSR format mjtNum* M = mj_stackAllocNum(d, m->nC); - mju_gather(M, d->qM, d->mapM2C, m->nC); + mju_gather(M, d->qM, d->mapM2M, m->nC); // LDlegacy: legacy LD matrix (size nM) mjtNum* LDlegacy = mj_stackAllocNum(d, m->nM); @@ -59,7 +59,7 @@ static void BM_factorI(benchmark::State& state, bool legacy, bool coil) { } else { mju_copy(d->qLD, M, m->nC); mj_factorI(d->qLD, d->qLDiagInv, m->nv, - d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); + d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); } } } diff --git a/test/benchmark/inertia_benchmark_test.cc b/test/benchmark/inertia_benchmark_test.cc index c8460cda..ba30190c 100644 --- a/test/benchmark/inertia_benchmark_test.cc +++ b/test/benchmark/inertia_benchmark_test.cc @@ -48,7 +48,7 @@ static void BM_solve(benchmark::State& state, SolveType type) { // M: mass matrix in CSR format mjtNum* M = mj_stackAllocNum(d, m->nC); - mju_gather(M, d->qM, d->mapM2C, m->nC); + mju_gather(M, d->qM, d->mapM2M, m->nC); // LDlegacy: legacy LD matrix (size nM) mjtNum* LDlegacy = mj_stackAllocNum(d, m->nM); @@ -73,9 +73,9 @@ static void BM_solve(benchmark::State& state, SolveType type) { case SolveType::kCsr: mju_copy(d->qLD, M, m->nC); mj_factorI(d->qLD, d->qLDiagInv, m->nv, - d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); + d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); mj_solveLD(res, d->qLD, d->qLDiagInv, m->nv, 1, - d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); + d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); } } } diff --git a/test/benchmark/solveLD_benchmark_test.cc b/test/benchmark/solveLD_benchmark_test.cc index 1ff28a4b..29fd7901 100644 --- a/test/benchmark/solveLD_benchmark_test.cc +++ b/test/benchmark/solveLD_benchmark_test.cc @@ -54,7 +54,7 @@ static void BM_solveLD(benchmark::State& state, bool featherstone, bool coil) { // scatter into legacy matrix mjtNum* LDlegacy = mj_stackAllocNum(d, m->nM); mju_zero(LDlegacy, m->nM); - mju_scatter(LDlegacy, d->qLD, d->mapM2C, m->nC); + mju_scatter(LDlegacy, d->qLD, d->mapM2M, m->nC); // benchmark while (state.KeepRunningBatch(kNumBenchmarkSteps)) { @@ -64,7 +64,7 @@ static void BM_solveLD(benchmark::State& state, bool featherstone, bool coil) { mj_solveLD_legacy(m, res, 1, LDlegacy, d->qLDiagInv); } else { mj_solveLD(res, d->qLD, d->qLDiagInv, m->nv, 1, - d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); + d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); } } } diff --git a/test/engine/engine_core_smooth_test.cc b/test/engine/engine_core_smooth_test.cc index 6adb8764..25843744 100644 --- a/test/engine/engine_core_smooth_test.cc +++ b/test/engine/engine_core_smooth_test.cc @@ -649,7 +649,7 @@ TEST_F(CoreSmoothTest, FactorI) { int nv = model->nv; vector Ldense(nv*nv, 0); mju_sparse2dense(Ldense.data(), data->qLD, nv, nv, - data->C_rownnz, data->C_rowadr, data->C_colind); + data->M_rownnz, data->M_rowadr, data->M_colind); for (int i=0; i < nv; i++) { // set diagonal to 1 Ldense[i*nv+i] = 1; @@ -658,7 +658,7 @@ TEST_F(CoreSmoothTest, FactorI) { // dense D matrix vector Ddense(nv*nv); mju_sparse2dense(Ddense.data(), data->qLD, nv, nv, - data->C_rownnz, data->C_rowadr, data->C_colind); + data->M_rownnz, data->M_rowadr, data->M_colind); for (int i=0; i < nv; i++) { for (int j=0; j < nv; j++) { // zero everything except the diagonal @@ -698,12 +698,12 @@ TEST_F(CoreSmoothTest, SolveLDs) { // copy M into LD: Legacy format vector LDlegacy(nM, 0); - mju_scatter(LDlegacy.data(), d->qLD, d->mapM2C, nC); + mju_scatter(LDlegacy.data(), d->qLD, d->mapM2M, nC); // compare LD and LDs densified matrices vector LDdense(nv*nv); mju_sparse2dense(LDdense.data(), d->qLD, nv, nv, - d->C_rownnz, d->C_rowadr, d->C_colind); + d->M_rownnz, d->M_rowadr, d->M_colind); vector LDdense2(nv*nv); mj_fullM(m, LDdense2.data(), LDlegacy.data()); @@ -722,7 +722,7 @@ TEST_F(CoreSmoothTest, SolveLDs) { mj_solveLD_legacy(m, vec.data(), 1, LDlegacy.data(), d->qLDiagInv); mj_solveLD(vec2.data(), d->qLD, d->qLDiagInv, nv, 1, - d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); + d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); // expect vectors to match up to floating point precision for (int i=0; i < nv; i++) { @@ -746,7 +746,7 @@ TEST_F(CoreSmoothTest, SolveLDmultipleVectors) { // copy LD into LDlegacy: Legacy format vector LDlegacy(m->nM, 0); - mju_scatter(LDlegacy.data(), d->qLD, d->mapM2C, m->nC); + mju_scatter(LDlegacy.data(), d->qLD, d->mapM2M, m->nC); // compare n LD and LDs vector solve int n = 3; @@ -757,7 +757,7 @@ TEST_F(CoreSmoothTest, SolveLDmultipleVectors) { mj_solveLD_legacy(m, vec.data(), n, LDlegacy.data(), d->qLDiagInv); mj_solveLD(vec2.data(), d->qLD, d->qLDiagInv, nv, n, - d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); + d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); // expect vectors to match up to floating point precision for (int i=0; i < nv*n; i++) { @@ -781,7 +781,7 @@ TEST_F(CoreSmoothTest, SolveM2) { int nv = m->nv; vector sqrtInvD(nv); for (int i=0; i < nv; i++) { - int diag = d->C_rowadr[i] + d->C_rownnz[i] - 1; + int diag = d->M_rowadr[i] + d->M_rownnz[i] - 1; sqrtInvD[i] = 1 / mju_sqrt(d->qLD[diag]); } @@ -795,7 +795,7 @@ TEST_F(CoreSmoothTest, SolveM2) { mj_solveM2(m, d, res.data(), vec.data(), sqrtInvD.data(), n); mj_solveLD(vec2.data(), d->qLD, d->qLDiagInv, nv, n, - d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); + d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); // expect equality of dot(v, M^-1 * v) and dot(M^-1/2 * v, M^-1/2 * v) for (int i=0; i < n; i++) { @@ -824,17 +824,17 @@ TEST_F(CoreSmoothTest, FactorIs) { // copy qLDlegacy into qLDexpected: CSR format vector qLDexpected(nC); - mju_gather(qLDexpected.data(), qLDlegacy.data(), d->mapM2C, nC); + mju_gather(qLDexpected.data(), qLDlegacy.data(), d->mapM2M, nC); // copy qM into qLD: CSR format vector qLD(nC); - mju_gather(qLD.data(), d->qM, d->mapM2C, nC); + mju_gather(qLD.data(), d->qM, d->mapM2M, nC); vector qLDiagInvExpected(d->qLDiagInv, d->qLDiagInv + nv); vector qLDiagInv(nv, 0); mj_factorI(qLD.data(), qLDiagInv.data(), nv, - d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); + d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); // expect outputs to match to floating point precision EXPECT_THAT(qLD, Pointwise(DoubleNear(1e-12), qLDexpected)); diff --git a/test/engine/engine_derivative_test.cc b/test/engine/engine_derivative_test.cc index 12b9ca88..027578e7 100644 --- a/test/engine/engine_derivative_test.cc +++ b/test/engine/engine_derivative_test.cc @@ -436,7 +436,7 @@ static void LinearSystem(const mjModel* m, mjData* d, mjtNum* A, mjtNum* B) { Ac[nv*nv + i*nv + i] = -m->dof_damping[i]; } mj_solveLD(Ac, d->qH, d->qHDiagInv, nv, 2*nv, - d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); + d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); // A = [dt*Ac; Ac] mju_transpose(A, Ac, 2*nv, nv); @@ -464,7 +464,7 @@ static void LinearSystem(const mjModel* m, mjData* d, mjtNum* A, mjtNum* B) { mju_sparse2dense(Bc, d->actuator_moment, nu, nv, d->moment_rownnz, d->moment_rowadr, d->moment_colind); mj_solveLD(Bc, d->qH, d->qHDiagInv, nv, nu, - d->C_rownnz, d->C_rowadr, m->dof_simplenum, d->C_colind); + d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); mju_transpose(BcT, Bc, nu, nv); mju_scl(B, BcT, dt*dt, nu*nv); mju_scl(B+nu*nv, BcT, dt, nu*nv); diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index a77daf2e..34355b04 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -4952,10 +4952,10 @@ public unsafe struct mjData_ { public int* B_rownnz; public int* B_rowadr; public int* B_colind; - public int* C_rownnz; - public int* C_rowadr; - public int* C_colind; - public int* mapM2C; + public int* M_rownnz; + public int* M_rowadr; + public int* M_colind; + public int* mapM2M; public int* D_rownnz; public int* D_rowadr; public int* D_diag; From b0c36c6b331713e4cd5759fd14439698e6e395be Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Wed, 14 May 2025 06:54:36 -0700 Subject: [PATCH 131/191] Remove dead functions. PiperOrigin-RevId: 758663369 Change-Id: I2de0d9802eb73c3541ef17e46182608b08a3793e --- src/engine/engine_support.c | 61 +++++-------------------------------- src/engine/engine_support.h | 8 ----- 2 files changed, 7 insertions(+), 62 deletions(-) diff --git a/src/engine/engine_support.c b/src/engine/engine_support.c index 9836ced9..2173879a 100644 --- a/src/engine/engine_support.c +++ b/src/engine/engine_support.c @@ -1065,70 +1065,23 @@ void mj_mulM2(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec) // destination can be sparse or dense when all int* are NULL void mj_addM(const mjModel* m, mjData* d, mjtNum* dst, int* rownnz, int* rowadr, int* colind) { + int nv = m->nv; // sparse if (rownnz && rowadr && colind) { - int nC = m->nC; mj_markStack(d); + mjtNum* buf_val = mjSTACKALLOC(d, nv, mjtNum); + int* buf_ind = mjSTACKALLOC(d, nv, int); - // gather C <- qM (legacy to CSR) - mjtNum* C = mjSTACKALLOC(d, nC, mjtNum); - mju_gather(C, d->qM, d->mapM2M, nC); + mju_addToMatSparse(dst, rownnz, rowadr, colind, nv, + d->M, d->M_rownnz, d->M_rowadr, d->M_colind, + buf_val, buf_ind); - // add to dst - mj_addMSparse(m, d, dst, rownnz, rowadr, colind, C, d->M_rownnz, d->M_rowadr, d->M_colind); mj_freeStack(d); } // dense else { - mj_addMDense(m, d, dst); - } -} - - - -// add inertia matrix to sparse destination matrix -void mj_addMSparse(const mjModel* m, mjData* d, mjtNum* dst, - int* rownnz, int* rowadr, int* colind, const mjtNum* M, - const int* M_rownnz, const int* M_rowadr, const int* M_colind) { - int nv = m->nv; - - mj_markStack(d); - mjtNum* buf_val = mjSTACKALLOC(d, nv, mjtNum); - int* buf_ind = mjSTACKALLOC(d, nv, int); - - mju_addToMatSparse(dst, rownnz, rowadr, colind, nv, - M, M_rownnz, M_rowadr, M_colind, - buf_val, buf_ind); - - mj_freeStack(d); -} - - - -// add inertia matrix to dense destination matrix -void mj_addMDense(const mjModel* m, mjData* d, mjtNum* dst) { - int nv = m->nv; - - for (int i = 0; i < nv; i++) { - int adr = m->dof_Madr[i]; - int j = i; - while (j >= 0) { - // add - dst[i*nv+j] += d->qM[adr]; - if (j < i) { - dst[j*nv+i] += d->qM[adr]; - } - - // only diagonal if simplenum - if (m->dof_simplenum[i]) { - break; - } - - // advance - j = m->dof_parentid[j]; - adr++; - } + mju_addToSymSparse(dst, d->M, nv, d->M_rownnz, d->M_rowadr, d->M_colind, /*flg_upper=*/ 1); } } diff --git a/src/engine/engine_support.h b/src/engine/engine_support.h index 5933f6a7..3d5f14c3 100644 --- a/src/engine/engine_support.h +++ b/src/engine/engine_support.h @@ -135,14 +135,6 @@ MJAPI void mj_mulM2(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum MJAPI void mj_addM(const mjModel* m, mjData* d, mjtNum* dst, int* rownnz, int* rowadr, int* colind); -// add inertia matrix to sparse destination matrix -MJAPI void mj_addMSparse(const mjModel* m, mjData* d, mjtNum* dst, - int* rownnz, int* rowadr, int* colind, const mjtNum* M, - const int* M_rownnz, const int* M_rowadr, const int* M_colind); - -// add inertia matrix to dense destination matrix -MJAPI void mj_addMDense(const mjModel* m, mjData* d, mjtNum* dst); - //-------------------------- perturbations --------------------------------------------------------- From d8bebdc675020cbf962543b2fa61779d6b91aa75 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Wed, 14 May 2025 07:12:19 -0700 Subject: [PATCH 132/191] Remove unnecessary diagnum argument in `mj_solveLD` and `mj_factorI` PiperOrigin-RevId: 758669364 Change-Id: Icfefc4a7a1e7d28da373725358ccf9d5f589f689 --- src/engine/engine_core_smooth.c | 37 ++++++++++-------------- src/engine/engine_core_smooth.h | 4 +-- src/engine/engine_forward.c | 8 ++--- src/engine/engine_solver.c | 2 +- test/benchmark/factorI_benchmark_test.cc | 2 +- test/benchmark/inertia_benchmark_test.cc | 4 +-- test/benchmark/solveLD_benchmark_test.cc | 2 +- test/engine/engine_core_smooth_test.cc | 8 ++--- test/engine/engine_derivative_test.cc | 4 +-- 9 files changed, 33 insertions(+), 38 deletions(-) diff --git a/src/engine/engine_core_smooth.c b/src/engine/engine_core_smooth.c index 0f104122..7f30294a 100644 --- a/src/engine/engine_core_smooth.c +++ b/src/engine/engine_core_smooth.c @@ -1653,7 +1653,7 @@ void mj_factorI_legacy(const mjModel* m, mjData* d, const mjtNum* M, mjtNum* qLD void mj_factorM(const mjModel* m, mjData* d) { TM_START; mju_copy(d->qLD, d->M, m->nC); - mj_factorI(d->qLD, d->qLDiagInv, m->nv, d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + mj_factorI(d->qLD, d->qLDiagInv, m->nv, d->M_rownnz, d->M_rowadr, d->M_colind); TM_ADD(mjTIMER_POS_INERTIA); } @@ -1661,32 +1661,28 @@ void mj_factorM(const mjModel* m, mjData* d) { // sparse L'*D*L factorizaton of inertia-like matrix M, assumed spd void mj_factorI(mjtNum* mat, mjtNum* diaginv, int nv, - const int* rownnz, const int* rowadr, const int* diagnum, const int* colind) { + const int* rownnz, const int* rowadr, const int* colind) { // backward loop over rows for (int k=nv-1; k >= 0; k--) { // get row k's address, diagonal index, inverse diagonal value - int rowadr_k = rowadr[k]; - int diag_k = rowadr_k + rownnz[k] - 1; - mjtNum invD = 1 / mat[diag_k]; + int start = rowadr[k]; + int diag = rownnz[k] - 1; + int end = start + diag; + mjtNum invD = 1 / mat[end]; if (diaginv) diaginv[k] = invD; - // skip if simple - if (diagnum[k]) { - continue; - } - - // update triangle above row k, inclusive - for (int adr=diag_k - 1; adr >= rowadr_k; adr--) { + // update triangle above row k + for (int adr=end - 1; adr >= start; adr--) { // tmp = L(k, i) / L(k, k) mjtNum tmp = mat[adr] * invD; // update row i < k: L(i, 0..i) -= L(i, 0..i) * L(k, i) / L(k, k) int i = colind[adr]; - mju_addToScl(mat + rowadr[i], mat + rowadr_k, -tmp, rownnz[i]); - - // update ith element of row k: L(k, i) /= L(k, k) - mat[adr] = tmp; + mju_addToScl(mat + rowadr[i], mat + start, -tmp, rownnz[i]); } + + // update row k: L(k, :) /= L(k, k) + mju_scl(mat + start, mat + start, invD, diag); } } @@ -1807,11 +1803,11 @@ void mj_solveLD_legacy(const mjModel* m, mjtNum* restrict x, int n, // in-place sparse backsubstitution: x = inv(L'*D*L)*x void mj_solveLD(mjtNum* restrict x, const mjtNum* qLD, const mjtNum* qLDiagInv, int nv, int n, - const int* rownnz, const int* rowadr, const int* diagnum, const int* colind) { + const int* rownnz, const int* rowadr, const int* colind) { // x <- L^-T x for (int i=nv-1; i > 0; i--) { // skip diagonal rows - if (diagnum[i]) { + if (rownnz[i] == 1) { continue; } @@ -1862,8 +1858,7 @@ void mj_solveLD(mjtNum* restrict x, const mjtNum* qLD, const mjtNum* qLDiagInv, // x <- L^-1 x for (int i=1; i < nv; i++) { // skip diagonal rows - if (diagnum[i]) { - i += diagnum[i] - 1; // iterating forward: skip ahead, adjust i + if (rownnz[i] == 1) { continue; } @@ -1895,7 +1890,7 @@ void mj_solveM(const mjModel* m, mjData* d, mjtNum* x, const mjtNum* y, int n) { mju_copy(x, y, n*m->nv); } mj_solveLD(x, d->qLD, d->qLDiagInv, m->nv, n, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->M_rownnz, d->M_rowadr, d->M_colind); } diff --git a/src/engine/engine_core_smooth.h b/src/engine/engine_core_smooth.h index a2db11cf..a8cd1a2b 100644 --- a/src/engine/engine_core_smooth.h +++ b/src/engine/engine_core_smooth.h @@ -63,7 +63,7 @@ MJAPI void mj_factorI_legacy(const mjModel* m, mjData* d, const mjtNum* M, // sparse L'*D*L factorizaton of inertia-like matrix MJAPI void mj_factorI(mjtNum* mat, mjtNum* diaginv, int nv, - const int* rownnz, const int* rowadr, const int* diagnum, const int* colind); + const int* rownnz, const int* rowadr, const int* colind); // sparse L'*D*L factorizaton of the inertia matrix M, assumed spd MJAPI void mj_factorM(const mjModel* m, mjData* d); @@ -75,7 +75,7 @@ MJAPI void mj_solveLD_legacy(const mjModel* m, mjtNum* x, int n, // in-place sparse backsubstitution: x = inv(L'*D*L)*x // handle n vectors at once MJAPI void mj_solveLD(mjtNum* x, const mjtNum* qLD, const mjtNum* qLDiagInv, int nv, int n, - const int* rownnz, const int* rowadr, const int* diagnum, const int* colind); + const int* rownnz, const int* rowadr, const int* colind); // sparse backsubstitution: x = inv(L'*D*L)*y, use factorization in d MJAPI void mj_solveM(const mjModel* m, mjData* d, mjtNum* x, const mjtNum* y, int n); diff --git a/src/engine/engine_forward.c b/src/engine/engine_forward.c index df22386f..6115e77f 100644 --- a/src/engine/engine_forward.c +++ b/src/engine/engine_forward.c @@ -873,14 +873,14 @@ void mj_EulerSkip(const mjModel* m, mjData* d, int skipfactor) { } // factorize in-place - mj_factorI(d->qH, d->qHDiagInv, nv, d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + mj_factorI(d->qH, d->qHDiagInv, nv, d->M_rownnz, d->M_rowadr, d->M_colind); } // solve mju_add(qfrc, d->qfrc_smooth, d->qfrc_constraint, nv); mju_copy(qacc, qfrc, m->nv); mj_solveLD(qacc, d->qH, d->qHDiagInv, nv, 1, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->M_rownnz, d->M_rowadr, d->M_colind); } // advance state and time @@ -1056,13 +1056,13 @@ void mj_implicitSkip(const mjModel* m, mjData* d, int skipfactor) { mju_gather(d->qH, MhB, d->mapM2M, nC); // factorize in-place - mj_factorI(d->qH, d->qHDiagInv, nv, d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + mj_factorI(d->qH, d->qHDiagInv, nv, d->M_rownnz, d->M_rowadr, d->M_colind); } // solve for qacc: (qM - dt*qDeriv) * qacc = qfrc mju_copy(qacc, qfrc, nv); mj_solveLD(qacc, d->qH, d->qHDiagInv, nv, 1, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->M_rownnz, d->M_rowadr, d->M_colind); } else { mjERROR("integrator must be implicit or implicitfast"); diff --git a/src/engine/engine_solver.c b/src/engine/engine_solver.c index 2b111e9e..899d935c 100644 --- a/src/engine/engine_solver.c +++ b/src/engine/engine_solver.c @@ -1071,7 +1071,7 @@ static void CGupdateGradient(mjCGContext* ctx, int flg_Newton) { else { mju_copy(ctx->Mgrad, ctx->grad, nv); mj_solveLD(ctx->Mgrad, ctx->qLD, ctx->qLDiagInv, nv, 1, - ctx->M_rownnz, ctx->M_rowadr, ctx->M_diagnum, ctx->M_colind); + ctx->M_rownnz, ctx->M_rowadr, ctx->M_colind); } } diff --git a/test/benchmark/factorI_benchmark_test.cc b/test/benchmark/factorI_benchmark_test.cc index 1508edd7..62a42c6c 100644 --- a/test/benchmark/factorI_benchmark_test.cc +++ b/test/benchmark/factorI_benchmark_test.cc @@ -59,7 +59,7 @@ static void BM_factorI(benchmark::State& state, bool legacy, bool coil) { } else { mju_copy(d->qLD, M, m->nC); mj_factorI(d->qLD, d->qLDiagInv, m->nv, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->M_rownnz, d->M_rowadr, d->M_colind); } } } diff --git a/test/benchmark/inertia_benchmark_test.cc b/test/benchmark/inertia_benchmark_test.cc index ba30190c..457aa32a 100644 --- a/test/benchmark/inertia_benchmark_test.cc +++ b/test/benchmark/inertia_benchmark_test.cc @@ -73,9 +73,9 @@ static void BM_solve(benchmark::State& state, SolveType type) { case SolveType::kCsr: mju_copy(d->qLD, M, m->nC); mj_factorI(d->qLD, d->qLDiagInv, m->nv, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->M_rownnz, d->M_rowadr, d->M_colind); mj_solveLD(res, d->qLD, d->qLDiagInv, m->nv, 1, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->M_rownnz, d->M_rowadr, d->M_colind); } } } diff --git a/test/benchmark/solveLD_benchmark_test.cc b/test/benchmark/solveLD_benchmark_test.cc index 29fd7901..65cf700c 100644 --- a/test/benchmark/solveLD_benchmark_test.cc +++ b/test/benchmark/solveLD_benchmark_test.cc @@ -64,7 +64,7 @@ static void BM_solveLD(benchmark::State& state, bool featherstone, bool coil) { mj_solveLD_legacy(m, res, 1, LDlegacy, d->qLDiagInv); } else { mj_solveLD(res, d->qLD, d->qLDiagInv, m->nv, 1, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->M_rownnz, d->M_rowadr, d->M_colind); } } } diff --git a/test/engine/engine_core_smooth_test.cc b/test/engine/engine_core_smooth_test.cc index 25843744..880f3e7e 100644 --- a/test/engine/engine_core_smooth_test.cc +++ b/test/engine/engine_core_smooth_test.cc @@ -722,7 +722,7 @@ TEST_F(CoreSmoothTest, SolveLDs) { mj_solveLD_legacy(m, vec.data(), 1, LDlegacy.data(), d->qLDiagInv); mj_solveLD(vec2.data(), d->qLD, d->qLDiagInv, nv, 1, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->M_rownnz, d->M_rowadr, d->M_colind); // expect vectors to match up to floating point precision for (int i=0; i < nv; i++) { @@ -757,7 +757,7 @@ TEST_F(CoreSmoothTest, SolveLDmultipleVectors) { mj_solveLD_legacy(m, vec.data(), n, LDlegacy.data(), d->qLDiagInv); mj_solveLD(vec2.data(), d->qLD, d->qLDiagInv, nv, n, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->M_rownnz, d->M_rowadr, d->M_colind); // expect vectors to match up to floating point precision for (int i=0; i < nv*n; i++) { @@ -795,7 +795,7 @@ TEST_F(CoreSmoothTest, SolveM2) { mj_solveM2(m, d, res.data(), vec.data(), sqrtInvD.data(), n); mj_solveLD(vec2.data(), d->qLD, d->qLDiagInv, nv, n, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->M_rownnz, d->M_rowadr, d->M_colind); // expect equality of dot(v, M^-1 * v) and dot(M^-1/2 * v, M^-1/2 * v) for (int i=0; i < n; i++) { @@ -834,7 +834,7 @@ TEST_F(CoreSmoothTest, FactorIs) { vector qLDiagInv(nv, 0); mj_factorI(qLD.data(), qLDiagInv.data(), nv, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->M_rownnz, d->M_rowadr, d->M_colind); // expect outputs to match to floating point precision EXPECT_THAT(qLD, Pointwise(DoubleNear(1e-12), qLDexpected)); diff --git a/test/engine/engine_derivative_test.cc b/test/engine/engine_derivative_test.cc index 027578e7..3b0a8d92 100644 --- a/test/engine/engine_derivative_test.cc +++ b/test/engine/engine_derivative_test.cc @@ -436,7 +436,7 @@ static void LinearSystem(const mjModel* m, mjData* d, mjtNum* A, mjtNum* B) { Ac[nv*nv + i*nv + i] = -m->dof_damping[i]; } mj_solveLD(Ac, d->qH, d->qHDiagInv, nv, 2*nv, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->M_rownnz, d->M_rowadr, d->M_colind); // A = [dt*Ac; Ac] mju_transpose(A, Ac, 2*nv, nv); @@ -464,7 +464,7 @@ static void LinearSystem(const mjModel* m, mjData* d, mjtNum* A, mjtNum* B) { mju_sparse2dense(Bc, d->actuator_moment, nu, nv, d->moment_rownnz, d->moment_rowadr, d->moment_colind); mj_solveLD(Bc, d->qH, d->qHDiagInv, nv, nu, - d->M_rownnz, d->M_rowadr, m->dof_simplenum, d->M_colind); + d->M_rownnz, d->M_rowadr, d->M_colind); mju_transpose(BcT, Bc, nu, nv); mju_scl(B, BcT, dt*dt, nu*nv); mju_scl(B+nu*nv, BcT, dt, nu*nv); From 71bdd915f7d3d3b55cda38120817ac7e98bdc9d4 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Wed, 14 May 2025 07:38:25 -0700 Subject: [PATCH 133/191] Move bending stiffness from plugin to the compiler. PiperOrigin-RevId: 758677644 Change-Id: I7d5c5140ba6948002bfa4ae9d6b4bf0f6de0e39b --- doc/XMLreference.rst | 8 +- doc/XMLschema.rst | 4 + doc/includes/references.h | 2 + include/mujoco/mjmodel.h | 1 + include/mujoco/mjspec.h | 1 + include/mujoco/mjxmacro.h | 1 + model/flex/trampoline.xml | 2 +- model/plugin/elasticity/flag_flex.xml | 8 +- model/plugin/elasticity/pancake_flex.xml | 7 +- model/plugin/elasticity/plate_flex.xml | 8 +- model/plugin/elasticity/poncho_flex.xml | 8 +- .../plugin/elasticity/poncho_vertcollide.xml | 8 +- plugin/elasticity/shell.cc | 82 ++----------------- plugin/elasticity/shell.h | 6 +- python/mujoco/introspect/structs.py | 13 +++ src/user/user_init.c | 1 + src/user/user_mesh.cc | 76 +++++++++++++++-- src/user/user_model.cc | 5 ++ src/user/user_objects.h | 1 + src/xml/xml_native_reader.cc | 8 +- src/xml/xml_native_writer.cc | 1 + test/plugin/elasticity/elasticity_test.cc | 36 +------- unity/Runtime/Bindings/MjBindings.cs | 1 + 23 files changed, 134 insertions(+), 154 deletions(-) diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index ef01da9d..1f479369 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -3627,9 +3627,10 @@ element is used to adjust the properties of all edges in the flex. .. _flexcomp-elasticity-poisson: .. _flexcomp-elasticity-damping: .. _flexcomp-elasticity-thickness: +.. _flexcomp-elasticity-elastic2d: .. |body/flexcomp/elasticity attrib list| replace:: - :at:`young`, :at:`poisson`, :at:`damping`, :at:`thickness` + :at:`young`, :at:`poisson`, :at:`damping`, :at:`thickness`, :at:`elastic2d` |body/flexcomp/elasticity attrib list| Same as in :ref:`flex/elasticity`. @@ -4070,6 +4071,11 @@ stress-strain relationship.. See also :ref:`deformable ` objects. This thickness can be set equal to 2 times the :ref:`radius ` in order to match the geometry, but is exposed separately since the radius might be constrained by considerations related to collision detection. +.. _flex-elasticity-elastic2d: + +:at:`elastic2d`: :at-val:`int, "1"` + Elastic contribution to passive forces of 2D flexes. 0: none, 1: bending only, 2: stretching only, 3: bending and + stretching .. _flex-contact: diff --git a/doc/XMLschema.rst b/doc/XMLschema.rst index 4935a883..1306115e 100644 --- a/doc/XMLschema.rst +++ b/doc/XMLschema.rst @@ -443,6 +443,8 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | | | | | :ref:`young` | :ref:`poisson` | :ref:`damping` | :ref:`thickness` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +| | | | :ref:`elastic2d` | | | | | +| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_2| flexcomp |br| |_2| |L| | | .. table:: | | :ref:`contact | ? | :class: mjcf-attributes | @@ -520,6 +522,8 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | | | | | :ref:`young` | :ref:`poisson` | :ref:`damping` | :ref:`thickness` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +| | | | :ref:`elastic2d` | | | | | +| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| deformable |br| |_| |L| | | .. table:: | | :ref:`skin | \* | :class: mjcf-attributes | diff --git a/doc/includes/references.h b/doc/includes/references.h index 0622ff60..9726123d 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -1246,6 +1246,7 @@ struct mjModel_ { mjtNum* flexedge_invweight0; // edge inv. weight in qpos0 (nflexedge x 1) mjtNum* flex_radius; // radius around primitive element (nflex x 1) mjtNum* flex_stiffness; // finite element stiffness matrix (nflexelem x 21) + mjtNum* flex_bending; // bending stiffness (nflexedge x 16) mjtNum* flex_damping; // Rayleigh's damping coefficient (nflex x 1) mjtNum* flex_edgestiffness; // edge stiffness (nflex x 1) mjtNum* flex_edgedamping; // edge damping (nflex x 1) @@ -2078,6 +2079,7 @@ typedef struct mjsFlex_ { // flex specification double poisson; // Poisson's ratio double damping; // Rayleigh's damping double thickness; // thickness (2D only) + int elastic2d; // 2D passive forces; 0: none, 1: bending, 2: stretching, 3: both // mesh properties mjStringVec* nodebody; // node body names diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index 8c00e062..0c531664 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -913,6 +913,7 @@ struct mjModel_ { mjtNum* flexedge_invweight0; // edge inv. weight in qpos0 (nflexedge x 1) mjtNum* flex_radius; // radius around primitive element (nflex x 1) mjtNum* flex_stiffness; // finite element stiffness matrix (nflexelem x 21) + mjtNum* flex_bending; // bending stiffness (nflexedge x 16) mjtNum* flex_damping; // Rayleigh's damping coefficient (nflex x 1) mjtNum* flex_edgestiffness; // edge stiffness (nflex x 1) mjtNum* flex_edgedamping; // edge damping (nflex x 1) diff --git a/include/mujoco/mjspec.h b/include/mujoco/mjspec.h index 512e7c29..d1b89d8e 100644 --- a/include/mujoco/mjspec.h +++ b/include/mujoco/mjspec.h @@ -444,6 +444,7 @@ typedef struct mjsFlex_ { // flex specification double poisson; // Poisson's ratio double damping; // Rayleigh's damping double thickness; // thickness (2D only) + int elastic2d; // 2D passive forces; 0: none, 1: bending, 2: stretching, 3: both // mesh properties mjStringVec* nodebody; // node body names diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index c7ce4861..b381d5d2 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -366,6 +366,7 @@ X ( mjtNum, flexedge_invweight0, nflexedge, 1 ) \ XMJV( mjtNum, flex_radius, nflex, 1 ) \ X ( mjtNum, flex_stiffness, nflexelem, 21 ) \ + X ( mjtNum, flex_bending, nflexedge, 16 ) \ X ( mjtNum, flex_damping, nflex, 1 ) \ X ( mjtNum, flex_edgestiffness, nflex, 1 ) \ X ( mjtNum, flex_edgedamping, nflex, 1 ) \ diff --git a/model/flex/trampoline.xml b/model/flex/trampoline.xml index 8d436e5d..16775a82 100644 --- a/model/flex/trampoline.xml +++ b/model/flex/trampoline.xml @@ -39,7 +39,7 @@ radius=".001" mass="10" name="plate" dim="2"> - + diff --git a/model/plugin/elasticity/flag_flex.xml b/model/plugin/elasticity/flag_flex.xml index 667a8c36..9e37edab 100644 --- a/model/plugin/elasticity/flag_flex.xml +++ b/model/plugin/elasticity/flag_flex.xml @@ -37,12 +37,8 @@ - - - - - - + + diff --git a/model/plugin/elasticity/pancake_flex.xml b/model/plugin/elasticity/pancake_flex.xml index 38e839f9..fb390e34 100644 --- a/model/plugin/elasticity/pancake_flex.xml +++ b/model/plugin/elasticity/pancake_flex.xml @@ -39,12 +39,7 @@ radius=".01" mass=".5" name="plate" dim="2"> - - - - - - + diff --git a/model/plugin/elasticity/plate_flex.xml b/model/plugin/elasticity/plate_flex.xml index b490b3ae..e39ec427 100644 --- a/model/plugin/elasticity/plate_flex.xml +++ b/model/plugin/elasticity/plate_flex.xml @@ -42,12 +42,8 @@ radius=".001" mass="100" name="plate"> - - - - - - + + diff --git a/model/plugin/elasticity/poncho_flex.xml b/model/plugin/elasticity/poncho_flex.xml index 72783d7e..9400d106 100644 --- a/model/plugin/elasticity/poncho_flex.xml +++ b/model/plugin/elasticity/poncho_flex.xml @@ -1418,13 +1418,9 @@ 398 399 418 398 376 378"> + - - - - - - + diff --git a/model/plugin/elasticity/poncho_vertcollide.xml b/model/plugin/elasticity/poncho_vertcollide.xml index 895df038..16e572bb 100644 --- a/model/plugin/elasticity/poncho_vertcollide.xml +++ b/model/plugin/elasticity/poncho_vertcollide.xml @@ -1418,13 +1418,9 @@ 398 399 418 398 376 378"> + - - - - - - + diff --git a/plugin/elasticity/shell.cc b/plugin/elasticity/shell.cc index 772b06eb..699040c6 100644 --- a/plugin/elasticity/shell.cc +++ b/plugin/elasticity/shell.cc @@ -33,54 +33,18 @@ namespace { // local tetrahedron numbering constexpr int kNumVerts = Stencil2D::kNumVerts; -// cotangent between two edges -mjtNum cot(mjtNum* x, int v0, int v1, int v2) { - mjtNum normal[3]; - mjtNum edge1[3]; - mjtNum edge2[3]; - mju_sub3(edge1, x+3*v1, x+3*v0); - mju_sub3(edge2, x+3*v2, x+3*v0); - mju_cross(normal, edge1, edge2); - - return mju_dot3(edge1, edge2) / mju_norm3(normal); -} - -// area of a triangle -mjtNum ComputeVolume(const mjtNum* x, const int v[kNumVerts]) { - mjtNum normal[3]; - mjtNum edge1[3]; - mjtNum edge2[3]; - - mju_sub3(edge1, x+3*v[1], x+3*v[0]); - mju_sub3(edge2, x+3*v[2], x+3*v[0]); - mju_cross(normal, edge1, edge2); - - return mju_norm3(normal) / 2; -} } // namespace // factory function std::optional Shell::Create(const mjModel* m, mjData* d, int instance) { - if (CheckAttr("poisson", m, instance) && - CheckAttr("young", m, instance) && - CheckAttr("thickness", m, instance)) { - mjtNum nu = strtod(mj_getPluginConfig(m, instance, "poisson"), nullptr); - mjtNum E = strtod(mj_getPluginConfig(m, instance, "young"), nullptr); - mjtNum thick = - strtod(mj_getPluginConfig(m, instance, "thickness"), nullptr); - return Shell(m, d, instance, nu, E, thick); - } else { - mju_warning("Invalid parameter specification in shell plugin"); - return std::nullopt; - } + return Shell(m, d, instance); } // plugin constructor -Shell::Shell(const mjModel* m, mjData* d, int instance, mjtNum nu, mjtNum E, - mjtNum thick) - : f0(-1), thickness(thick) { +Shell::Shell(const mjModel* m, mjData* d, int instance) + : f0(-1) { // count plugin bodies nv = 0; for (int i = 1; i < m->nbody; i++) { @@ -104,9 +68,6 @@ Shell::Shell(const mjModel* m, mjData* d, int instance, mjtNum nu, mjtNum E, } } - // material parameters - mjtNum mu = E / (2*(1+nu)); - // loop over all triangles for (int t = 0; t < m->flex_elemnum[f0]; t++) { int* v = m->flex_elem + 3*(t+m->flex_elemadr[f0]); @@ -119,41 +80,9 @@ Shell::Shell(const mjModel* m, mjData* d, int instance, mjtNum nu, mjtNum E, // allocate array position.assign(nv*3, 0); - bending.assign(m->flex_edgenum[f0]*16, 0); // store previous positions mju_copy(position.data(), m->body_pos+3*i0, 3*nv); - - // assemble bending Hessian - for (int e = 0; e < m->flex_edgenum[f0]; e++) { - int* edge = m->flex_edge + 2*(e+m->flex_edgeadr[f0]); - int* flap = m->flex_edgeflap + 2*(e+m->flex_edgeadr[f0]); - int v[4] = {edge[0], edge[1], flap[0], flap[1]}; - int vadj[3] = {v[1], v[0], v[3]}; - - if (v[3]== -1) { - // skip boundary edges - continue; - } - - // cotangent operator from Wardetzky at al., "Discrete Quadratic Curvature - // Energies", https://cims.nyu.edu/gcl/papers/wardetzky2007dqb.pdf - - mjtNum a01 = cot(m->body_pos+3*i0, v[0], v[1], v[2]); - mjtNum a02 = cot(m->body_pos+3*i0, v[0], v[3], v[1]); - mjtNum a03 = cot(m->body_pos+3*i0, v[1], v[2], v[0]); - mjtNum a04 = cot(m->body_pos+3*i0, v[1], v[0], v[3]); - mjtNum c[4] = {a03 + a04, a01 + a02, -(a01 + a03), -(a02 + a04)}; - mjtNum volume = ComputeVolume(m->body_pos+3*i0, v) + - ComputeVolume(m->body_pos+3*i0, vadj); - - for (int v1 = 0; v1 < StencilFlap::kNumVerts; v1++) { - for (int v2 = 0; v2 < StencilFlap::kNumVerts; v2++) { - bending[16 * e + 4 * v1 + v2] += - 1.5 * c[v1] * c[v2] / volume * mu * pow(thickness, 3) / 12; - } - } - } } void Shell::Compute(const mjModel* m, mjData* d, int instance) { @@ -166,10 +95,11 @@ void Shell::Compute(const mjModel* m, mjData* d, int instance) { // skip boundary edges continue; } + mjtNum* k = m->flex_bending + 16*m->flex_edgeadr[f0]; for (int i = 0; i < StencilFlap::kNumVerts; i++) { for (int j = 0; j < StencilFlap::kNumVerts; j++) { for (int x = 0; x < 3; x++) { - force[3*i+x] += bending[16*e+4*i+j] * d->xpos[3*(i0+v[j])+x]; + force[3*i+x] += k[16*e+4*i+j] * d->xpos[3*(i0+v[j])+x]; } } } @@ -195,7 +125,7 @@ void Shell::RegisterPlugin() { plugin.name = "mujoco.elasticity.shell"; plugin.capabilityflags |= mjPLUGIN_PASSIVE; - const char* attributes[] = {"young", "poisson", "thickness", "damping"}; + const char* attributes[] = {"damping"}; plugin.nattribute = sizeof(attributes) / sizeof(attributes[0]); plugin.attributes = attributes; plugin.nstate = +[](const mjModel* m, int instance) { return 0; }; diff --git a/plugin/elasticity/shell.h b/plugin/elasticity/shell.h index e8496e70..aa1e0191 100644 --- a/plugin/elasticity/shell.h +++ b/plugin/elasticity/shell.h @@ -51,13 +51,9 @@ class Shell { // precomputed quantities std::vector position; // previous-step positions (nv x 3) - std::vector bending; // bending Hessian (ne x 16) - - mjtNum thickness; private: - Shell(const mjModel* m, mjData* d, int instance, mjtNum nu, mjtNum E, - mjtNum thick); + Shell(const mjModel* m, mjData* d, int instance); }; } // namespace mujoco::plugin::elasticity diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index 4d45dc9e..d9f04366 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -2755,6 +2755,14 @@ STRUCTS: Mapping[str, StructDecl] = dict([ doc='finite element stiffness matrix', array_extent=('nflexelem', 21), ), + StructFieldDecl( + name='flex_bending', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='bending stiffness', + array_extent=('nflexedge', 16), + ), StructFieldDecl( name='flex_damping', type=PointerType( @@ -10867,6 +10875,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='double'), doc='thickness (2D only)', ), + StructFieldDecl( + name='elastic2d', + type=ValueType(name='int'), + doc='2D passive forces; 0: none, 1: bending, 2: stretching, 3: both', # pylint: disable=line-too-long + ), StructFieldDecl( name='nodebody', type=PointerType( diff --git a/src/user/user_init.c b/src/user/user_init.c index 01f4a8b9..3a55af25 100644 --- a/src/user/user_init.c +++ b/src/user/user_init.c @@ -238,6 +238,7 @@ void mjs_defaultFlex(mjsFlex* flex) { flex->rgba[0] = flex->rgba[1] = flex->rgba[2] = 0.5f; flex->rgba[3] = 1.0f; flex->thickness = -1; + flex->elastic2d = 1; } diff --git a/src/user/user_mesh.cc b/src/user/user_mesh.cc index b44ecff5..49230914 100644 --- a/src/user/user_mesh.cc +++ b/src/user/user_mesh.cc @@ -3075,6 +3075,56 @@ static void CreateFlapStencil(std::vector& flaps, } } +// cotangent between two edges +double inline cot(double* x, int v0, int v1, int v2) { + double normal[3]; + double edge1[3] = {x[3*v1]-x[3*v0], x[3*v1+1]-x[3*v0+1], x[3*v1+2]-x[3*v0+2]}; + double edge2[3] = {x[3*v2]-x[3*v0], x[3*v2+1]-x[3*v0+1], x[3*v2+2]-x[3*v0+2]}; + + mjuu_crossvec(normal, edge1, edge2); + return mjuu_dot3(edge1, edge2) / sqrt(mjuu_dot3(normal, normal)); +} + +// area of a triangle +double inline ComputeVolume(const double* x, const int v[Stencil2D::kNumVerts]) { + double normal[3]; + double edge1[3] = {x[3*v[1]]-x[3*v[0]], x[3*v[1]+1]-x[3*v[0]+1], x[3*v[1]+2]-x[3*v[0]+2]}; + double edge2[3] = {x[3*v[2]]-x[3*v[0]], x[3*v[2]+1]-x[3*v[0]+1], x[3*v[2]+2]-x[3*v[0]+2]}; + + mjuu_crossvec(normal, edge1, edge2); + return sqrt(mjuu_dot3(normal, normal)) / 2; +} + +// compute bending stiffness for a single edge +template +void inline ComputeBending(double* bending, double* pos, const int v[4], double mu, + double thickness) { + int vadj[3] = {v[1], v[0], v[3]}; + + if (v[3]== -1) { + // skip boundary edges + return; + } + + // cotangent operator from Wardetzky at al., "Discrete Quadratic Curvature + // Energies", https://cims.nyu.edu/gcl/papers/wardetzky2007dqb.pdf + + mjtNum a01 = cot(pos, v[0], v[1], v[2]); + mjtNum a02 = cot(pos, v[0], v[3], v[1]); + mjtNum a03 = cot(pos, v[1], v[2], v[0]); + mjtNum a04 = cot(pos, v[1], v[0], v[3]); + mjtNum c[4] = {a03 + a04, a01 + a02, -(a01 + a03), -(a02 + a04)}; + mjtNum volume = ComputeVolume(pos, v) + + ComputeVolume(pos, vadj); + + for (int v1 = 0; v1 < T::kNumVerts; v1++) { + for (int v2 = 0; v2 < T::kNumVerts; v2++) { + bending[4 * v1 + v2] += + 1.5 * c[v1] * c[v2] / volume * mu * pow(thickness, 3) / 12; + } + } +} + //----------------------------- linear elasticity -------------------------------------------------- // Gauss Legendre quadrature points in 1 dimension on the interval [a, b] @@ -3570,11 +3620,18 @@ void mjCFlex::Compile(const mjVFS* vfs) { // set size nedge = (int)edge.size(); + // create flap stencil + if (dim == 2) { + CreateFlapStencil(flaps, elem_, edgeidx_); + } + // compute elasticity if (young > 0) { if (poisson < 0 || poisson >= 0.5) { throw mjCError(this, "Poisson ratio must be in [0, 0.5)"); } + + // linear elasticity stiffness.assign(21*nelem, 0); if (interpolated) { int min_size = ceil(nodexpos.size()*nodexpos.size() / 21); @@ -3583,11 +3640,13 @@ void mjCFlex::Compile(const mjVFS* vfs) { } ComputeLinearStiffness(stiffness, nodexpos.data(), young, poisson); } + + // geometrically nonlinear elasticity for (unsigned int t = 0; t < nelem; t++) { if (interpolated) { continue; } - if (dim == 2) { + if (dim == 2 && elastic2d >= 2 && thickness > 0) { ComputeStiffness(stiffness, vertxpos, elem_.data() + (dim + 1) * t, t, young, poisson, thickness); @@ -3597,6 +3656,16 @@ void mjCFlex::Compile(const mjVFS* vfs) { poisson); } } + + // bending stiffness (2D only) + if (dim == 2 && (elastic2d == 1 || elastic2d == 3) && thickness > 0) { + bending.assign(nedge*16, 0); + + for (unsigned int e = 0; e < nedge; e++) { + ComputeBending(bending.data() + 16 * e, vertxpos.data(), flaps[e].vertices, + young / (2 * (1 + poisson)), thickness); + } + } } // add plugins @@ -3613,11 +3682,6 @@ void mjCFlex::Compile(const mjVFS* vfs) { } } - // create flap stencil - if (dim == 2) { - CreateFlapStencil(flaps, elem_, edgeidx_); - } - // create shell fragments and element-vertex collision pairs CreateShellPair(); diff --git a/src/user/user_model.cc b/src/user/user_model.cc index f705551c..9e516380 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -3043,6 +3043,11 @@ void mjCModel::CopyObjects(mjModel* m) { } else { mjuu_zerovec(m->flex_stiffness + 21 * elem_adr, 21 * pfl->nelem); } + if (!pfl->bending.empty()) { + mjuu_copyvec(m->flex_bending + 16 * edge_adr, pfl->bending.data(), pfl->bending.size()); + } else { + mjuu_zerovec(m->flex_bending + 16 * edge_adr, 16 * pfl->nedge); + } m->flex_damping[i] = (mjtNum)pfl->damping; // set fields: mesh-like diff --git a/src/user/user_objects.h b/src/user/user_objects.h index 585d6abb..46df52e6 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -857,6 +857,7 @@ class mjCFlex_ : public mjCBase { std::vector elemaabb_; // element bounding volume std::vector edgeidx_; // element edge ids std::vector stiffness; // elasticity stiffness matrix + std::vector bending; // bending stiffness matrix // variable-size data std::vector vertbody_; // vertex body names diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 85723a6e..8e2874aa 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -311,7 +311,7 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { "flatskin", "pos", "quat", "axisangle", "xyaxes", "zaxis", "euler", "origin"}, {"<"}, {"edge", "?", "5", "equality", "solref", "solimp", "stiffness", "damping"}, - {"elasticity", "?", "4", "young", "poisson", "damping", "thickness"}, + {"elasticity", "?", "5", "young", "poisson", "damping", "thickness", "elastic2d"}, {"contact", "?", "14", "contype", "conaffinity", "condim", "priority", "friction", "solmix", "solref", "solimp", "margin", "gap", "internal", "selfcollide", "activelayers", "vertcollide"}, @@ -332,7 +332,7 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { "friction", "solmix", "solref", "solimp", "margin", "gap", "internal", "selfcollide", "activelayers", "vertcollide"}, {"edge", "?", "2", "stiffness", "damping"}, - {"elasticity", "?", "4", "young", "poisson", "damping", "thickness"}, + {"elasticity", "?", "5", "young", "poisson", "damping", "thickness", "elastic2d"}, {">"}, {"skin", "*", "9", "name", "file", "material", "rgba", "inflate", "vertex", "texcoord", "face", "group"}, @@ -1401,6 +1401,7 @@ void mjXReader::OneFlex(XMLElement* elem, mjsFlex* flex) { ReadAttr(elasticity, "poisson", 1, &flex->poisson, text); ReadAttr(elasticity, "thickness", 1, &flex->thickness, text); ReadAttr(elasticity, "damping", 1, &flex->damping, text); + ReadAttr(elasticity, "elastic2d", 1, &flex->elastic2d, text); } // write error info @@ -2664,10 +2665,11 @@ void mjXReader::OneFlexcomp(XMLElement* elem, mjsBody* body, const mjVFS* vfs) { ReadAttr(elasticity, "poisson", 1, &dflex.poisson, text); ReadAttr(elasticity, "damping", 1, &dflex.damping, text); ReadAttr(elasticity, "thickness", 1, &dflex.thickness, text); + ReadAttr(elasticity, "elastic2d", 1, &dflex.elastic2d, text); } // check errors - if (elasticity && fcomp.equality) { + if (dflex.elastic2d >= 2 && fcomp.equality) { throw mjXError(elem, "elasticity and edge constraints cannot both be present"); } diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index a7f27871..14905144 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -194,6 +194,7 @@ void mjXWriter::OneFlex(XMLElement* elem, const mjCFlex* flex) { WriteAttr(elastic, "poisson", 1, &flex->poisson, &defflex.poisson); WriteAttr(elastic, "thickness", 1, &flex->thickness, &defflex.thickness); WriteAttr(elastic, "damping", 1, &flex->damping, &defflex.damping); + WriteAttr(elastic, "elastic2d", 1, &flex->elastic2d, &defflex.elastic2d); // edge subelement XMLElement* edge = InsertEnd(elem, "edge"); diff --git a/test/plugin/elasticity/elasticity_test.cc b/test/plugin/elasticity/elasticity_test.cc index 16273900..754fa46e 100644 --- a/test/plugin/elasticity/elasticity_test.cc +++ b/test/plugin/elasticity/elasticity_test.cc @@ -65,11 +65,8 @@ TEST_F(ElasticityTest, ElasticEnergyShell) { - - - - - + + @@ -97,7 +94,7 @@ TEST_F(ElasticityTest, ElasticEnergyShell) { for (int x = 0; x < 3; x++) { mjtNum elongation1 = scale * shell->position[3*v[i]+x]; mjtNum elongation2 = scale * shell->position[3*v[j]+x]; - energy += shell->bending[16*e+4*i+j] * elongation1 * elongation2; + energy += m->flex_bending[16*e+4*i+j] * elongation1 * elongation2; } } } @@ -117,7 +114,7 @@ TEST_F(PluginTest, ElasticEnergyMembrane) { - + @@ -161,31 +158,6 @@ TEST_F(PluginTest, ElasticEnergyMembrane) { mj_deleteModel(m); } -TEST_F(ElasticityTest, InvalidThickness) { - static constexpr char xml[] = R"( - - - - - - - - - - - - - - - )"; - - char error[1024] = {0}; - mjModel* m = LoadModelFromString(xml, error, sizeof(error)); - ASSERT_THAT(m, testing::IsNull()); - EXPECT_THAT(error, ::testing::HasSubstr("Invalid parameter")); -} - // -------------------------------- solid ----------------------------------- TEST_F(ElasticityTest, ElasticEnergySolid) { static constexpr char cantilever_xml[] = R"( diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index 34355b04..4f0d209b 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -5497,6 +5497,7 @@ public unsafe struct mjModel_ { public double* flexedge_invweight0; public double* flex_radius; public double* flex_stiffness; + public double* flex_bending; public double* flex_damping; public double* flex_edgestiffness; public double* flex_edgedamping; From e1baa15776ca7e899c178fb581ba33202da8bd30 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Wed, 14 May 2025 08:11:10 -0700 Subject: [PATCH 134/191] Remove unnecessary argument in `mju_mulSymVecSparse` PiperOrigin-RevId: 758688536 Change-Id: Ife1099b948215363ddaa6d0b0edcf3492edcddcc --- src/engine/engine_solver.c | 4 ++-- src/engine/engine_util_sparse.c | 9 +-------- src/engine/engine_util_sparse.h | 3 +-- test/engine/engine_util_sparse_test.cc | 5 ++--- 4 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/engine/engine_solver.c b/src/engine/engine_solver.c index 899d935c..7568b0f6 100644 --- a/src/engine/engine_solver.c +++ b/src/engine/engine_solver.c @@ -1357,7 +1357,7 @@ static mjtNum CGsearch(mjCGContext* ctx, mjtNum tolerance, mjtNum ls_iterations) // compute Mv = M * v mju_mulSymVecSparse(ctx->Mv, ctx->M, ctx->search, nv, - ctx->M_rownnz, ctx->M_rowadr, ctx->M_diagnum, ctx->M_colind); + ctx->M_rownnz, ctx->M_rowadr, ctx->M_colind); // compute Jv = J * search (dense or sparse) if (!ctx->is_sparse) { @@ -1887,7 +1887,7 @@ static void mj_solCGNewton(const mjModel* m, mjData* d, int island, int maxiter, // compute Ma = M * qacc mju_mulSymVecSparse(ctx.Ma, ctx.M, ctx.qacc, nv, - ctx.M_rownnz, ctx.M_rowadr, ctx.M_diagnum, ctx.M_colind); + ctx.M_rownnz, ctx.M_rowadr, ctx.M_colind); // compute Jaref = J * qacc - aref (dense or sparse) diff --git a/src/engine/engine_util_sparse.c b/src/engine/engine_util_sparse.c index 8d364c5e..17e960a3 100644 --- a/src/engine/engine_util_sparse.c +++ b/src/engine/engine_util_sparse.c @@ -227,7 +227,7 @@ void mju_addToSymSparse(mjtNum* res, const mjtNum* mat, int n, void mju_mulSymVecSparse(mjtNum* restrict res, const mjtNum* restrict mat, const mjtNum* restrict vec, int n, const int* restrict rownnz, const int* restrict rowadr, - const int* restrict diagnum, const int* restrict colind) { + const int* restrict colind) { // clear res mju_zero(res, n); @@ -240,13 +240,6 @@ void mju_mulSymVecSparse(mjtNum* restrict res, const mjtNum* restrict mat, // diagonal res[i] = row[diag] * vec[i]; - // TODO: consider using SIMD if diagnum[i] >= 4 - - // shortcut for diagonal row/column - if (diagnum[i]) { - continue; - } - // off-diagonals const int* ind = colind + adr; for (int k=diag-1; k >= 0; k--) { diff --git a/src/engine/engine_util_sparse.h b/src/engine/engine_util_sparse.h index 616b4d21..ab7ea0f3 100644 --- a/src/engine/engine_util_sparse.h +++ b/src/engine/engine_util_sparse.h @@ -64,8 +64,7 @@ MJAPI void mju_addToSymSparse(mjtNum* res, const mjtNum* mat, int n, // multiply symmetric matrix (only lower triangle represented) by vector: // res = (mat + strict_upper(mat')) * vec MJAPI void mju_mulSymVecSparse(mjtNum* res, const mjtNum* mat, const mjtNum* vec, int n, - const int* rownnz, const int* rowadr, const int* diagnum, - const int* colind); + const int* rownnz, const int* rowadr, const int* colind); // compress sparse matrix, remove elements with abs(value) <= minval, return total non-zeros MJAPI int mju_compressSparse(mjtNum* mat, int nr, int nc, diff --git a/test/engine/engine_util_sparse_test.cc b/test/engine/engine_util_sparse_test.cc index 10875a67..7fee2127 100644 --- a/test/engine/engine_util_sparse_test.cc +++ b/test/engine/engine_util_sparse_test.cc @@ -1075,7 +1075,7 @@ TEST_F(EngineUtilSparseTest, MjuMulSymVecSparse) { constexpr int nnz = 9; mjtNum mat[n*n] = {1, 0, 0, 0, - -1, 2, 0, 0, // spurious (ignored) -1 at (1, 0) + 0, 2, 0, 0, 3, 0, 4, 0, 5, 6, 7, 8}; @@ -1090,12 +1090,11 @@ TEST_F(EngineUtilSparseTest, MjuMulSymVecSparse) { int rowadr[n]; int colind[nnz]; mju_dense2sparse(mat_sparse, mat, n, n, rownnz, rowadr, colind, nnz); - int diagnum[n] = {0, 1, 0, 0}; // multiply: res = (mat + strict_upper(mat')) * vec mjtNum vec[n] = {4, 3, 2, 1}; mjtNum res[n]; - mju_mulSymVecSparse(res, mat_sparse, vec, n, rownnz, rowadr, diagnum, colind); + mju_mulSymVecSparse(res, mat_sparse, vec, n, rownnz, rowadr, colind); // dense multiply mjtNum res2[n]; From 0a5da8db220336993dd1012ea887a14321c71e40 Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Wed, 14 May 2025 09:09:01 -0700 Subject: [PATCH 135/191] Rename MjcSiteAPI to MjcPhysicsSiteAPI for consistency. PiperOrigin-RevId: 758708231 Change-Id: I75914ba52159996515547aa9e2e93690cadbfc66 --- src/experimental/usd/mjcPhysics/generatedSchema.usda | 2 +- src/experimental/usd/mjcPhysics/plugInfo.json | 2 +- src/experimental/usd/mjcPhysics/schema.usda | 2 +- src/experimental/usd/mjcPhysics/siteAPI.h | 4 ++-- src/experimental/usd/mjcPhysics/tokens.cpp | 4 ++-- src/experimental/usd/mjcPhysics/tokens.h | 4 ++-- src/experimental/usd/mjcPhysics/wrapTokens.cpp | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/experimental/usd/mjcPhysics/generatedSchema.usda b/src/experimental/usd/mjcPhysics/generatedSchema.usda index 62050c6c..7510f8e3 100644 --- a/src/experimental/usd/mjcPhysics/generatedSchema.usda +++ b/src/experimental/usd/mjcPhysics/generatedSchema.usda @@ -220,7 +220,7 @@ class "MjcPhysicsSceneAPI" ( ) } -class "MjcSiteAPI" ( +class "MjcPhysicsSiteAPI" ( doc = "API describing a Mujoco site." ) { diff --git a/src/experimental/usd/mjcPhysics/plugInfo.json b/src/experimental/usd/mjcPhysics/plugInfo.json index fcf19910..9206a5d0 100644 --- a/src/experimental/usd/mjcPhysics/plugInfo.json +++ b/src/experimental/usd/mjcPhysics/plugInfo.json @@ -18,7 +18,7 @@ }, "MjcPhysicsSiteAPI": { "alias": { - "UsdSchemaBase": "MjcSiteAPI" + "UsdSchemaBase": "MjcPhysicsSiteAPI" }, "autoGenerated": true, "bases": [ diff --git a/src/experimental/usd/mjcPhysics/schema.usda b/src/experimental/usd/mjcPhysics/schema.usda index d6e2727d..7bd5b6e6 100644 --- a/src/experimental/usd/mjcPhysics/schema.usda +++ b/src/experimental/usd/mjcPhysics/schema.usda @@ -510,7 +510,7 @@ class "MjcPhysicsSceneAPI" ) } -class "MjcSiteAPI" +class "MjcPhysicsSiteAPI" ( customData = { string className = "SiteAPI" diff --git a/src/experimental/usd/mjcPhysics/siteAPI.h b/src/experimental/usd/mjcPhysics/siteAPI.h index e1981297..74712557 100644 --- a/src/experimental/usd/mjcPhysics/siteAPI.h +++ b/src/experimental/usd/mjcPhysics/siteAPI.h @@ -34,7 +34,7 @@ PXR_NAMESPACE_OPEN_SCOPE class SdfAssetPath; // -------------------------------------------------------------------------- // -// MJCSITEAPI // +// MJCPHYSICSSITEAPI // // -------------------------------------------------------------------------- // /// \class MjcPhysicsSiteAPI @@ -104,7 +104,7 @@ class MjcPhysicsSiteAPI : public UsdAPISchemaBase { static bool CanApply(const UsdPrim &prim, std::string *whyNot = nullptr); /// Applies this single-apply API schema to the given \p prim. - /// This information is stored by adding "MjcSiteAPI" to the + /// This information is stored by adding "MjcPhysicsSiteAPI" to the /// token-valued, listOp metadata \em apiSchemas on the prim. /// /// \return A valid MjcPhysicsSiteAPI object is returned upon success. diff --git a/src/experimental/usd/mjcPhysics/tokens.cpp b/src/experimental/usd/mjcPhysics/tokens.cpp index b6bfd4b8..4f0da276 100644 --- a/src/experimental/usd/mjcPhysics/tokens.cpp +++ b/src/experimental/usd/mjcPhysics/tokens.cpp @@ -86,7 +86,7 @@ MjcPhysicsTokensType::MjcPhysicsTokensType() rk4("rk4", TfToken::Immortal), sparse("sparse", TfToken::Immortal), MjcPhysicsSceneAPI("MjcPhysicsSceneAPI", TfToken::Immortal), - MjcSiteAPI("MjcSiteAPI", TfToken::Immortal), + MjcPhysicsSiteAPI("MjcPhysicsSiteAPI", TfToken::Immortal), allTokens({auto_, cg, dense, @@ -149,7 +149,7 @@ MjcPhysicsTokensType::MjcPhysicsTokensType() rk4, sparse, MjcPhysicsSceneAPI, - MjcSiteAPI}) {} + MjcPhysicsSiteAPI}) {} TfStaticData MjcPhysicsTokens; diff --git a/src/experimental/usd/mjcPhysics/tokens.h b/src/experimental/usd/mjcPhysics/tokens.h index 8d6b5f92..92ffc587 100644 --- a/src/experimental/usd/mjcPhysics/tokens.h +++ b/src/experimental/usd/mjcPhysics/tokens.h @@ -313,10 +313,10 @@ struct MjcPhysicsTokensType { /// /// Schema identifer and family for MjcPhysicsSceneAPI const TfToken MjcPhysicsSceneAPI; - /// \brief "MjcSiteAPI" + /// \brief "MjcPhysicsSiteAPI" /// /// Schema identifer and family for MjcPhysicsSiteAPI - const TfToken MjcSiteAPI; + const TfToken MjcPhysicsSiteAPI; /// A vector of all of the tokens listed above. const std::vector allTokens; }; diff --git a/src/experimental/usd/mjcPhysics/wrapTokens.cpp b/src/experimental/usd/mjcPhysics/wrapTokens.cpp index bd9d4b75..5b0672cc 100644 --- a/src/experimental/usd/mjcPhysics/wrapTokens.cpp +++ b/src/experimental/usd/mjcPhysics/wrapTokens.cpp @@ -146,5 +146,5 @@ void wrapMjcPhysicsTokens() { _AddToken(cls, "rk4", MjcPhysicsTokens->rk4); _AddToken(cls, "sparse", MjcPhysicsTokens->sparse); _AddToken(cls, "MjcPhysicsSceneAPI", MjcPhysicsTokens->MjcPhysicsSceneAPI); - _AddToken(cls, "MjcSiteAPI", MjcPhysicsTokens->MjcSiteAPI); + _AddToken(cls, "MjcPhysicsSiteAPI", MjcPhysicsTokens->MjcPhysicsSiteAPI); } From dc4968506b96ae1a73a02abb5fc9f5ccc86dd1e7 Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Wed, 14 May 2025 09:38:11 -0700 Subject: [PATCH 136/191] Remove MjcPhysics prefix from schema API types. PiperOrigin-RevId: 758718987 Change-Id: I8a3a5dc7a990d401a91f5982e62c590e8924472a --- src/experimental/usd/mjcPhysics/generatedSchema.usda | 4 ++-- src/experimental/usd/mjcPhysics/plugInfo.json | 4 ++-- src/experimental/usd/mjcPhysics/sceneAPI.h | 4 ++-- src/experimental/usd/mjcPhysics/schema.usda | 10 ++-------- src/experimental/usd/mjcPhysics/siteAPI.h | 4 ++-- src/experimental/usd/mjcPhysics/tokens.cpp | 8 ++++---- src/experimental/usd/mjcPhysics/tokens.h | 8 ++++---- src/experimental/usd/mjcPhysics/wrapTokens.cpp | 4 ++-- 8 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/experimental/usd/mjcPhysics/generatedSchema.usda b/src/experimental/usd/mjcPhysics/generatedSchema.usda index 7510f8e3..4cb4d177 100644 --- a/src/experimental/usd/mjcPhysics/generatedSchema.usda +++ b/src/experimental/usd/mjcPhysics/generatedSchema.usda @@ -3,7 +3,7 @@ "WARNING: THIS FILE IS GENERATED BY usdGenSchema. DO NOT EDIT." ) -class "MjcPhysicsSceneAPI" ( +class "SceneAPI" ( doc = "API providing global simulation options for Mujoco." ) { @@ -220,7 +220,7 @@ class "MjcPhysicsSceneAPI" ( ) } -class "MjcPhysicsSiteAPI" ( +class "SiteAPI" ( doc = "API describing a Mujoco site." ) { diff --git a/src/experimental/usd/mjcPhysics/plugInfo.json b/src/experimental/usd/mjcPhysics/plugInfo.json index 9206a5d0..56e11dd5 100644 --- a/src/experimental/usd/mjcPhysics/plugInfo.json +++ b/src/experimental/usd/mjcPhysics/plugInfo.json @@ -8,7 +8,7 @@ "Types": { "MjcPhysicsSceneAPI": { "alias": { - "UsdSchemaBase": "MjcPhysicsSceneAPI" + "UsdSchemaBase": "SceneAPI" }, "autoGenerated": true, "bases": [ @@ -18,7 +18,7 @@ }, "MjcPhysicsSiteAPI": { "alias": { - "UsdSchemaBase": "MjcPhysicsSiteAPI" + "UsdSchemaBase": "SiteAPI" }, "autoGenerated": true, "bases": [ diff --git a/src/experimental/usd/mjcPhysics/sceneAPI.h b/src/experimental/usd/mjcPhysics/sceneAPI.h index 0fd0d56e..4b77202a 100644 --- a/src/experimental/usd/mjcPhysics/sceneAPI.h +++ b/src/experimental/usd/mjcPhysics/sceneAPI.h @@ -35,7 +35,7 @@ PXR_NAMESPACE_OPEN_SCOPE class SdfAssetPath; // -------------------------------------------------------------------------- // -// MJCPHYSICSSCENEAPI // +// SCENEAPI // // -------------------------------------------------------------------------- // /// \class MjcPhysicsSceneAPI @@ -110,7 +110,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { static bool CanApply(const UsdPrim &prim, std::string *whyNot = nullptr); /// Applies this single-apply API schema to the given \p prim. - /// This information is stored by adding "MjcPhysicsSceneAPI" to the + /// This information is stored by adding "SceneAPI" to the /// token-valued, listOp metadata \em apiSchemas on the prim. /// /// \return A valid MjcPhysicsSceneAPI object is returned upon success. diff --git a/src/experimental/usd/mjcPhysics/schema.usda b/src/experimental/usd/mjcPhysics/schema.usda index 7bd5b6e6..3d416fc2 100644 --- a/src/experimental/usd/mjcPhysics/schema.usda +++ b/src/experimental/usd/mjcPhysics/schema.usda @@ -92,11 +92,8 @@ over "GLOBAL" ( } -class "MjcPhysicsSceneAPI" +class "SceneAPI" ( - customData = { - string className = "SceneAPI" - } doc = """API providing global simulation options for Mujoco.""" inherits = @@ -510,11 +507,8 @@ class "MjcPhysicsSceneAPI" ) } -class "MjcPhysicsSiteAPI" +class "SiteAPI" ( - customData = { - string className = "SiteAPI" - } doc = """API describing a Mujoco site.""" inherits = diff --git a/src/experimental/usd/mjcPhysics/siteAPI.h b/src/experimental/usd/mjcPhysics/siteAPI.h index 74712557..537345ce 100644 --- a/src/experimental/usd/mjcPhysics/siteAPI.h +++ b/src/experimental/usd/mjcPhysics/siteAPI.h @@ -34,7 +34,7 @@ PXR_NAMESPACE_OPEN_SCOPE class SdfAssetPath; // -------------------------------------------------------------------------- // -// MJCPHYSICSSITEAPI // +// SITEAPI // // -------------------------------------------------------------------------- // /// \class MjcPhysicsSiteAPI @@ -104,7 +104,7 @@ class MjcPhysicsSiteAPI : public UsdAPISchemaBase { static bool CanApply(const UsdPrim &prim, std::string *whyNot = nullptr); /// Applies this single-apply API schema to the given \p prim. - /// This information is stored by adding "MjcPhysicsSiteAPI" to the + /// This information is stored by adding "SiteAPI" to the /// token-valued, listOp metadata \em apiSchemas on the prim. /// /// \return A valid MjcPhysicsSiteAPI object is returned upon success. diff --git a/src/experimental/usd/mjcPhysics/tokens.cpp b/src/experimental/usd/mjcPhysics/tokens.cpp index 4f0da276..7b1868f8 100644 --- a/src/experimental/usd/mjcPhysics/tokens.cpp +++ b/src/experimental/usd/mjcPhysics/tokens.cpp @@ -85,8 +85,8 @@ MjcPhysicsTokensType::MjcPhysicsTokensType() pyramidal("pyramidal", TfToken::Immortal), rk4("rk4", TfToken::Immortal), sparse("sparse", TfToken::Immortal), - MjcPhysicsSceneAPI("MjcPhysicsSceneAPI", TfToken::Immortal), - MjcPhysicsSiteAPI("MjcPhysicsSiteAPI", TfToken::Immortal), + SceneAPI("SceneAPI", TfToken::Immortal), + SiteAPI("SiteAPI", TfToken::Immortal), allTokens({auto_, cg, dense, @@ -148,8 +148,8 @@ MjcPhysicsTokensType::MjcPhysicsTokensType() pyramidal, rk4, sparse, - MjcPhysicsSceneAPI, - MjcPhysicsSiteAPI}) {} + SceneAPI, + SiteAPI}) {} TfStaticData MjcPhysicsTokens; diff --git a/src/experimental/usd/mjcPhysics/tokens.h b/src/experimental/usd/mjcPhysics/tokens.h index 92ffc587..50239a66 100644 --- a/src/experimental/usd/mjcPhysics/tokens.h +++ b/src/experimental/usd/mjcPhysics/tokens.h @@ -309,14 +309,14 @@ struct MjcPhysicsTokensType { /// Possible value for MjcPhysicsSceneAPI::GetJacobianAttr(), This token /// represents the sparse constraint Jacobian and matrices computed from it. const TfToken sparse; - /// \brief "MjcPhysicsSceneAPI" + /// \brief "SceneAPI" /// /// Schema identifer and family for MjcPhysicsSceneAPI - const TfToken MjcPhysicsSceneAPI; - /// \brief "MjcPhysicsSiteAPI" + const TfToken SceneAPI; + /// \brief "SiteAPI" /// /// Schema identifer and family for MjcPhysicsSiteAPI - const TfToken MjcPhysicsSiteAPI; + const TfToken SiteAPI; /// A vector of all of the tokens listed above. const std::vector allTokens; }; diff --git a/src/experimental/usd/mjcPhysics/wrapTokens.cpp b/src/experimental/usd/mjcPhysics/wrapTokens.cpp index 5b0672cc..3a5aa603 100644 --- a/src/experimental/usd/mjcPhysics/wrapTokens.cpp +++ b/src/experimental/usd/mjcPhysics/wrapTokens.cpp @@ -145,6 +145,6 @@ void wrapMjcPhysicsTokens() { _AddToken(cls, "pyramidal", MjcPhysicsTokens->pyramidal); _AddToken(cls, "rk4", MjcPhysicsTokens->rk4); _AddToken(cls, "sparse", MjcPhysicsTokens->sparse); - _AddToken(cls, "MjcPhysicsSceneAPI", MjcPhysicsTokens->MjcPhysicsSceneAPI); - _AddToken(cls, "MjcPhysicsSiteAPI", MjcPhysicsTokens->MjcPhysicsSiteAPI); + _AddToken(cls, "SceneAPI", MjcPhysicsTokens->SceneAPI); + _AddToken(cls, "SiteAPI", MjcPhysicsTokens->SiteAPI); } From e8c566bee1348006d54dfdad791c1723c7ccdf4d Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Thu, 15 May 2025 00:38:33 -0700 Subject: [PATCH 137/191] Remove use of `diagnum` in solver PiperOrigin-RevId: 759021147 Change-Id: I9e5c85b6542380780fc487781398f406ddaad51b --- doc/includes/references.h | 1 - include/mujoco/mjdata.h | 1 - include/mujoco/mjxmacro.h | 1 - python/mujoco/introspect/structs.py | 8 -------- src/engine/engine_island.c | 19 ------------------- src/engine/engine_solver.c | 3 --- unity/Runtime/Bindings/MjBindings.cs | 1 - 7 files changed, 34 deletions(-) diff --git a/doc/includes/references.h b/doc/includes/references.h index 9726123d..59320911 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -392,7 +392,6 @@ struct mjData_ { mjtNum* iacc_smooth; // unconstrained acceleration (nidof x 1) int* iM_rownnz; // inertia: non-zeros in each row (nidof x 1) int* iM_rowadr; // inertia: address of each row in iM_colind (nidof x 1) - int* iM_diagnum; // inertia: num of consecutive diagonal elements (nidof x 1) int* iM_colind; // inertia: column indices of non-zeros (nC x 1) mjtNum* iM; // total inertia (sparse) (nC x 1) mjtNum* iLD; // L'*D*L factorization of M (sparse) (nC x 1) diff --git a/include/mujoco/mjdata.h b/include/mujoco/mjdata.h index ac780c82..c05db9ab 100644 --- a/include/mujoco/mjdata.h +++ b/include/mujoco/mjdata.h @@ -420,7 +420,6 @@ struct mjData_ { mjtNum* iacc_smooth; // unconstrained acceleration (nidof x 1) int* iM_rownnz; // inertia: non-zeros in each row (nidof x 1) int* iM_rowadr; // inertia: address of each row in iM_colind (nidof x 1) - int* iM_diagnum; // inertia: num of consecutive diagonal elements (nidof x 1) int* iM_colind; // inertia: column indices of non-zeros (nC x 1) mjtNum* iM; // total inertia (sparse) (nC x 1) mjtNum* iLD; // L'*D*L factorization of M (sparse) (nC x 1) diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index b381d5d2..ecd40310 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -758,7 +758,6 @@ X( mjtNum, iacc_smooth, MJ_D(nidof), 1 ) \ X( int, iM_rownnz, MJ_D(nidof), 1 ) \ X( int, iM_rowadr, MJ_D(nidof), 1 ) \ - X( int, iM_diagnum, MJ_D(nidof), 1 ) \ X( int, iM_colind, MJ_M(nC), 1 ) \ X( mjtNum, iM, MJ_M(nC), 1 ) \ X( mjtNum, iLD, MJ_M(nC), 1 ) \ diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index d9f04366..4aac02a9 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -6024,14 +6024,6 @@ STRUCTS: Mapping[str, StructDecl] = dict([ doc='inertia: address of each row in iM_colind', array_extent=('nidof',), ), - StructFieldDecl( - name='iM_diagnum', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='inertia: num of consecutive diagonal elements', - array_extent=('nidof',), - ), StructFieldDecl( name='iM_colind', type=PointerType( diff --git a/src/engine/engine_island.c b/src/engine/engine_island.c index d38943d4..05364373 100644 --- a/src/engine/engine_island.c +++ b/src/engine/engine_island.c @@ -544,25 +544,6 @@ void mj_island(const mjModel* m, mjData* d) { d->iM, d->M); mju_gather(d->iLDiagInv, d->qLDiagInv, d->map_idof2dof, nidof); - // compute iM_diagnum (dof_simplenum per island) - int count = 0; - int dof_next = d->map_idof2dof[nidof-1]; - for (int i=nidof-1; i >= 0; i--) { - // check if island boundary was crossed - int dof = d->map_idof2dof[i]; - int island_boundary = (d->dof_island[dof] != d->dof_island[dof_next]); - dof_next = dof; - - // accumulate and set simple dof (diagonal row) counter - if (m->dof_simplenum[dof] && !island_boundary) { - count++; // increment counter - } else { - count = 0; // reset - } - d->iM_diagnum[i] = count; - } - - // ------------------------------------- constraints --------------------------------------------- diff --git a/src/engine/engine_solver.c b/src/engine/engine_solver.c index 7568b0f6..37b83024 100644 --- a/src/engine/engine_solver.c +++ b/src/engine/engine_solver.c @@ -786,7 +786,6 @@ struct _mjCGContext { // inertia const int* M_rownnz; const int* M_rowadr; - const int* M_diagnum; const int* M_colind; const mjtNum* M; const mjtNum* qLD; @@ -888,7 +887,6 @@ static void CGpointers(const mjModel* m, const mjData* d, mjCGContext* ctx, int // inertia ctx->M_rownnz = d->M_rownnz; ctx->M_rowadr = d->M_rowadr; - ctx->M_diagnum = m->dof_simplenum; ctx->M_colind = d->M_colind; ctx->M = d->M; ctx->qLD = d->qLD; @@ -937,7 +935,6 @@ static void CGpointers(const mjModel* m, const mjData* d, mjCGContext* ctx, int // inertia ctx->M_rownnz = d->iM_rownnz + idofadr; ctx->M_rowadr = d->iM_rowadr + idofadr; - ctx->M_diagnum = d->iM_diagnum + idofadr; ctx->M_colind = d->iM_colind; ctx->M = d->iM; ctx->qLD = d->iLD; diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index 4f0d209b..d0dd61aa 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -5004,7 +5004,6 @@ public unsafe struct mjData_ { public double* iacc_smooth; public int* iM_rownnz; public int* iM_rowadr; - public int* iM_diagnum; public int* iM_colind; public double* iM; public double* iLD; From 7ba507323dff297e6c7b2ed9503f7db42a7972b9 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Thu, 15 May 2025 03:09:30 -0700 Subject: [PATCH 138/191] Handle mocap body in `bind`. PiperOrigin-RevId: 759065524 Change-Id: Ic4cc737596af883f267b6e2c470ac4591ea1eead --- mjx/mujoco/mjx/_src/support.py | 10 +++++++++- mjx/mujoco/mjx/_src/support_test.py | 14 +++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/mjx/mujoco/mjx/_src/support.py b/mjx/mujoco/mjx/_src/support.py index aea5bced..ab330067 100644 --- a/mjx/mujoco/mjx/_src/support.py +++ b/mjx/mujoco/mjx/_src/support.py @@ -473,6 +473,8 @@ class BindData(object): return self._slice(self.__getname(name), slice(adr, adr + num)) else: return self._slice(self.__getname(name), adr) + elif name in ('mocap_pos', 'mocap_quat'): + return self._slice(self.__getname(name), self.model.body_mocapid[self.id]) return self._slice(self.__getname(name), self.id) def set(self, name: str, value: jax.Array) -> Data: @@ -485,7 +487,7 @@ class BindData(object): iter(value) except TypeError: value = [value] - if name in ('qpos', 'qvel', 'qacc'): + if name in ('qpos', 'qvel', 'qacc', 'mocap_pos', 'mocap_quat'): adr = num = 0 if name == 'qpos': adr = self.model.jnt_qposadr[self.id] @@ -495,6 +497,12 @@ class BindData(object): adr = self.model.jnt_dofadr[self.id] typ = self.model.jnt_type[self.id] num = sum((typ == jt) * jt.dof_width() for jt in JointType) + elif name == 'mocap_pos': + adr = self.model.body_mocapid[self.id] * 3 + num = np.ones_like(self.id, dtype=int) * 3 + elif name == 'mocap_quat': + adr = self.model.body_mocapid[self.id] * 4 + num = np.ones_like(self.id, dtype=int) * 4 if not isinstance(self.id, list): adr = [adr] num = [num] diff --git a/mjx/mujoco/mjx/_src/support_test.py b/mjx/mujoco/mjx/_src/support_test.py index 38392d38..05c1e5bd 100644 --- a/mjx/mujoco/mjx/_src/support_test.py +++ b/mjx/mujoco/mjx/_src/support_test.py @@ -173,6 +173,8 @@ class SupportTest(parameterized.TestCase): + + @@ -318,6 +320,16 @@ class SupportTest(parameterized.TestCase): np.testing.assert_array_equal( dx10.bind(mx, s.bodies[1]).xfrc_applied, [2, 2, 2, 2, 2, 2] ) + dx11 = dx.bind(mx, s.bodies[-1]).set( + 'mocap_pos', + [1, 2, 3], + ) + np.testing.assert_array_equal( + dx11.bind(mx, s.bodies[-2]).mocap_pos, [100, 110, 120] + ) + np.testing.assert_array_equal( + dx11.bind(mx, s.bodies[-1]).mocap_pos, [1, 2, 3] + ) # test attribute and type mismatches with self.assertRaisesRegex( @@ -359,7 +371,7 @@ class SupportTest(parameterized.TestCase): self.assertEqual( str(e.exception), 'mjSpec signature does not match mjx.Model signature:' - ' 17856615236057737915 != 12517827274439268436', + ' 15297169659434471387 != 2785811613804955188', ) _CONTACTS = """ From 81442e06a0f10a08b54912fb2264e4844427cc0d Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Thu, 15 May 2025 06:37:10 -0700 Subject: [PATCH 139/191] Add MjcPhysicsSiteAPI support and testing. PiperOrigin-RevId: 759124593 Change-Id: Ic2fa2a00f8384f432ee2eef95c80f5c180aa4ca9 --- .../usd/plugins/mjcf/mujoco_to_usd.cc | 3 + .../usd/mjcPhysics/mjc_site_api_test.cc | 96 +++++++++++++++++++ .../usd/plugins/mjcf/mjcf_file_format_test.cc | 15 +++ 3 files changed, 114 insertions(+) create mode 100644 test/experimental/usd/mjcPhysics/mjc_site_api_test.cc diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc index ef5e9f71..45d59d66 100644 --- a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc @@ -21,6 +21,7 @@ #include #include +#include "third_party/mujoco/src/experimental/usd/mjcPhysics/tokens.h" #include "mjcf/utils.h" #include #include @@ -834,6 +835,8 @@ class ModelWriter { pxr::SdfPath site_path = WriteSiteGeom(site, body_path); SetPrimPurpose(data_, site_path, pxr::UsdGeomTokens->guide); + ApplyApiSchema(data_, site_path, pxr::MjcPhysicsTokens->SiteAPI); + int site_id = mjs_getId(site->element); auto transform = MujocoPosQuatToTransform(&model_->site_pos[3 * site_id], &model_->site_quat[4 * site_id]); diff --git a/test/experimental/usd/mjcPhysics/mjc_site_api_test.cc b/test/experimental/usd/mjcPhysics/mjc_site_api_test.cc new file mode 100644 index 00000000..a193dbb1 --- /dev/null +++ b/test/experimental/usd/mjcPhysics/mjc_site_api_test.cc @@ -0,0 +1,96 @@ +// Copyright 2025 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. + +#include +#include +#include +#include +#include "src/experimental/usd/mjcPhysics/siteAPI.h" +#include "test/fixture.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define EXPECT_SITE_TYPE(spec, site_path, expected_type) \ + { \ + mjsElement* site_element = \ + mjs_findElement(spec, mjOBJ_SITE, site_path.GetString().c_str()); \ + EXPECT_THAT(site_element, NotNull()); \ + mjsSite* site = mjs_asSite(site_element); \ + EXPECT_EQ(site->type, expected_type); \ + } + +namespace mujoco { +namespace { + +using pxr::MjcPhysicsSiteAPI; +using pxr::SdfPath; +using MjcSiteApiTest = MujocoTest; +using testing::NotNull; + +TEST_F(MjcSiteApiTest, TestApply) { + auto stage = pxr::UsdStage::CreateInMemory(); + + auto test_body_path = SdfPath("/World/TestBody"); + auto body = pxr::UsdGeomXform::Define(stage, test_body_path); + pxr::UsdPhysicsRigidBodyAPI::Apply(body.GetPrim()); + + auto test_collider_path = + test_body_path.AppendChild(pxr::TfToken("Collider")); + auto collider = pxr::UsdGeomSphere::Define(stage, test_collider_path); + pxr::UsdPhysicsCollisionAPI::Apply(collider.GetPrim()); + + auto test_sphere_site_path = + test_body_path.AppendChild(pxr::TfToken("SphereSite")); + auto test_cylinder_site_path = + test_body_path.AppendChild(pxr::TfToken("CylinderSite")); + auto test_capsule_site_path = + test_body_path.AppendChild(pxr::TfToken("CapsuleSite")); + auto test_box_site_path = test_body_path.AppendChild(pxr::TfToken("BoxSite")); + + auto sphere = pxr::UsdGeomSphere::Define(stage, test_sphere_site_path); + MjcPhysicsSiteAPI::Apply(sphere.GetPrim()); + + auto cylinder = pxr::UsdGeomCylinder::Define(stage, test_cylinder_site_path); + MjcPhysicsSiteAPI::Apply(cylinder.GetPrim()); + + auto capsule = pxr::UsdGeomCapsule::Define(stage, test_capsule_site_path); + MjcPhysicsSiteAPI::Apply(capsule.GetPrim()); + + auto box = pxr::UsdGeomCube::Define(stage, test_box_site_path); + MjcPhysicsSiteAPI::Apply(box.GetPrim()); + + mjSpec* spec = mj_parseUSDStage(stage); + mjModel* default_model = mj_compile(spec, nullptr); + EXPECT_THAT(default_model, NotNull()) << mjs_getError(spec); + + EXPECT_SITE_TYPE(spec, test_sphere_site_path, mjGEOM_SPHERE); + EXPECT_SITE_TYPE(spec, test_cylinder_site_path, mjGEOM_CYLINDER); + EXPECT_SITE_TYPE(spec, test_capsule_site_path, mjGEOM_CAPSULE); + EXPECT_SITE_TYPE(spec, test_box_site_path, mjGEOM_BOX); + + mj_deleteModel(default_model); + mj_deleteSpec(spec); +} + +} // namespace +} // namespace mujoco diff --git a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc index 22b8ac52..8186af02 100644 --- a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc +++ b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc @@ -17,6 +17,8 @@ #include #include +#include "src/experimental/usd/mjcPhysics/sceneAPI.h" +#include "src/experimental/usd/mjcPhysics/siteAPI.h" #include "test/experimental/usd/test_utils.h" #include "test/fixture.h" #include @@ -518,15 +520,28 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestSitePrimsAuthored) { auto stage = pxr::UsdStage::Open(layer); EXPECT_PRIM_VALID(stage, "/test/box_site"); EXPECT_PRIM_IS_A(stage, "/test/box_site", pxr::UsdGeomCube); + EXPECT_PRIM_API_APPLIED(stage, "/test/box_site", pxr::MjcPhysicsSiteAPI); + EXPECT_PRIM_VALID(stage, "/test/ball/ball/sphere_site"); EXPECT_PRIM_IS_A(stage, "/test/ball/ball/sphere_site", pxr::UsdGeomSphere); + EXPECT_PRIM_API_APPLIED(stage, "/test/ball/ball/sphere_site", + pxr::MjcPhysicsSiteAPI); + EXPECT_PRIM_VALID(stage, "/test/ball/ball/capsule_site"); EXPECT_PRIM_IS_A(stage, "/test/ball/ball/capsule_site", pxr::UsdGeomCapsule); + EXPECT_PRIM_API_APPLIED(stage, "/test/ball/ball/capsule_site", + pxr::MjcPhysicsSiteAPI); + EXPECT_PRIM_VALID(stage, "/test/ball/ball/cylinder_site"); EXPECT_PRIM_IS_A(stage, "/test/ball/ball/cylinder_site", pxr::UsdGeomCylinder); + EXPECT_PRIM_API_APPLIED(stage, "/test/ball/ball/cylinder_site", + pxr::MjcPhysicsSiteAPI); + EXPECT_PRIM_VALID(stage, "/test/ball/ball/ellipsoid_site"); EXPECT_PRIM_IS_A(stage, "/test/ball/ball/ellipsoid_site", pxr::UsdGeomSphere); + EXPECT_PRIM_API_APPLIED(stage, "/test/ball/ball/ellipsoid_site", + pxr::MjcPhysicsSiteAPI); } TEST_F(MjcfSdfFileFormatPluginTest, TestSitePrimsPurpose) { From af0cb2d365967210082f85b9e19c449269a41465 Mon Sep 17 00:00:00 2001 From: Hannes Braun Date: Thu, 15 May 2025 15:42:51 +0200 Subject: [PATCH 140/191] Use errno.h instead of sys/errno.h --- simulate/main.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simulate/main.cc b/simulate/main.cc index 3ebe873c..8d70e6e8 100644 --- a/simulate/main.cc +++ b/simulate/main.cc @@ -39,7 +39,7 @@ extern "C" { #if defined(__APPLE__) #include #endif - #include + #include #include #endif } From ee8abdf854b46179ecb912f723ca9f371555354a Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Fri, 16 May 2025 06:41:08 -0700 Subject: [PATCH 141/191] Speed up mju_sqrMatTDSparse PiperOrigin-RevId: 759593417 Change-Id: I0168e1f96333769d09d61e835560910d43aac608 --- src/engine/engine_util_sparse.c | 207 +++++++++++++++++- src/engine/engine_util_sparse.h | 10 + .../engine_util_sparse_benchmark_test.cc | 15 +- test/engine/engine_util_sparse_test.cc | 124 +++++++++-- 4 files changed, 335 insertions(+), 21 deletions(-) diff --git a/src/engine/engine_util_sparse.c b/src/engine/engine_util_sparse.c index 17e960a3..44dddb92 100644 --- a/src/engine/engine_util_sparse.c +++ b/src/engine/engine_util_sparse.c @@ -18,6 +18,7 @@ #include #include +#include #include // IWYU pragma: keep #include #include "engine/engine_io.h" @@ -711,8 +712,10 @@ void mju_sqrMatTDUncompressedInit(int* res_rowadr, int nc) { -// compute sparse M'*diag*M (diag=NULL: compute M'*M), res has uncompressed layout -// res_rowadr is required to be precomputed +// max number of supernodes handled +#define mjMAXSUPER 8 + +// compute sparse M'*diag*M (diag=NULL: compute M'*M), res_rowadr must be precomputed void mju_sqrMatTDSparse(mjtNum* res, const mjtNum* mat, const mjtNum* matT, const mjtNum* diag, int nr, int nc, int* res_rownnz, const int* res_rowadr, int* res_colind, @@ -721,6 +724,206 @@ void mju_sqrMatTDSparse(mjtNum* res, const mjtNum* mat, const mjtNum* matT, const int* rownnzT, const int* rowadrT, const int* colindT, const int* rowsuperT, mjData* d, int* diagind) { + mj_markStack(d); + + // reinterpret transposed matrices as compressed sparse column + const mjtNum* mat_csc = matT; + const int* colnnz = rownnzT; + const int* coladr = rowadrT; + const int* rowind = colindT; + const int* colsuper = rowsuperT; + const mjtNum* matT_csc = mat; + const int* colnnzT = rownnz; + const int* coladrT = rowadr; + const int* rowindT = colind; + // rowsuper is unused + + // marker[i] = 1 if row i is set in current column + int* marker = mjSTACKALLOC(d, nc, int); + mju_zeroInt(marker, nc); + + // dense buffer (considered column-major) containing up to mjMAXSUPER columns + mjtNum* buffer = mjSTACKALLOC(d, nc*mjMAXSUPER, mjtNum); + + // dense index vector of the current column (unsorted) + int* buffer_idx = mjSTACKALLOC(d, nc, int); + + // rowstart[i]: address of first row in column mat'[:, i] with index > current column + int* rowstart = mjSTACKALLOC(d, nr, int); + mju_zeroInt(rowstart, nr); + + // clear res_rownnz + mju_zeroInt(res_rownnz, nc); + + // construct res[lower+diagonal], by column + for (int c=0; c < nc; c++) { + int buffer_nnz = 0; + + // prepare column c of mat + int nnz = colnnz[c]; + int adr = coladr[c]; + const int* ind = rowind + adr; + + // val: array of ns > 0 column pointers with identical pattern to c + const mjtNum* val[mjMAXSUPER]; + + // first column is c + int ns = 1; + val[0] = mat_csc + adr; + + // add c's supernodes, if any + int cs; + if (colsuper && (cs = colsuper[c])) { + ns += mjMIN(cs, mjMAXSUPER - 1); + for (int s=1; s < ns; s++) { + val[s] = mat_csc + coladr[c + s]; + } + } + + // diagonal special-case: dense dot product of column c, with/out diag + mjtNum diag_c[mjMAXSUPER]; + if (diag) { + for (int s=0; s < ns; s++) { + mjtNum ds = 0; + for (int k=0; k < nnz; k++) { + ds += (val[s][k] * val[s][k]) * diag[ind[k]]; + } + diag_c[s] = ds; + } + } else { + for (int s=0; s < ns; s++) { + diag_c[s] = mju_dot(val[s], val[s], nnz); + } + } + + // in the strict lower triangle, compute + // res[:, c] = mat' * mat[:, c] = sum_r(diag[r] * mat'[:, r] * mat[:, c]) + for (int i=0; i < nnz; i++) { + // prepare column r of mat' + int r = ind[i]; + int adrT = coladrT[r]; + int nnzT = colnnzT[r]; + const int* indT = rowindT + adrT; + const mjtNum* valT = matT_csc + adrT; + + // get v[s] = diag[r] * mat[r, c + s] for s in [0, ns) + mjtNum v[mjMAXSUPER]; + if (diag) { + mjtNum diag_r = diag[r]; + for (int s=0; s < ns; s++) { + v[s] = diag_r * val[s][i]; + } + } else { + for (int s=0; s < ns; s++) { + v[s] = val[s][i]; + } + } + + // gather to dense buffer columns: buffer[:, s] += mat'[:, r] * v[s] + for (int k=rowstart[r]; k < nnzT; k++) { + int j = indT[k]; + + // if j is not in the strict lower triangle, increment rowstart and continue + if (j <= c) { + rowstart[r]++; + continue; + } + + // first nonzero in row j: mark and set value + if (!marker[j]) { + // mark j and save it + marker[j] = 1; + buffer_idx[buffer_nnz++] = j; + + // set value + mjtNum vk = valT[k]; + for (int s=0; s < ns; s++) { + buffer[s*nc + j] = vk * v[s]; + } + } + + // otherwise existing nonzero in row j: add to value + else { + mjtNum vk = valT[k]; + for (int s=0; s < ns; s++) { + buffer[s*nc + j] += vk * v[s]; + } + } + } + } + + // scatter to res from dense buffer: res[:, c + s] = buffer[:, s] for s in [0, ns) + + // write values under diagonal + for (int i=0; i < buffer_nnz; i++) { + int j = buffer_idx[i]; + marker[j] = 0; + int adr_j = res_rowadr[j] + res_rownnz[j]; + + // truncate row to strict lower triangle + int lower = j - c; + int nm = mjMIN(ns, lower); + + // increment nonzeros + res_rownnz[j] += nm; + + // write value + for (int s=0; s < nm; s++) { + res[adr_j + s] = buffer[s*nc + j]; + } + + // write index + for (int s=0; s < nm; s++) { + res_colind[adr_j + s] = c + s; + } + } + + // write diagonal value + for (int s=0; s < ns; s++) { + int adr_s = res_rowadr[c + s] + res_rownnz[c + s]++; + res_colind[adr_s] = c + s; + res[adr_s] = diag_c[s]; + } + + // supernode: skip ahead if ns > 1 + c += ns - 1; + } + + // upper triangle requested: save diagonal indices and fill + if (diagind) { + // save diagonal indices + for (int i=0; i < nc; i++) { + diagind[i] = res_rowadr[i] + res_rownnz[i] - 1; + } + + // fill upper triangle + for (int i=0; i < nc; i++) { + int start = res_rowadr[i]; + int end = start + res_rownnz[i] - 1; + for (int j=start; j < end; j++) { + int adr = res_rowadr[res_colind[j]] + res_rownnz[res_colind[j]]++; + res[adr] = res[j]; + res_colind[adr] = i; + } + } + } + + mj_freeStack(d); +} + +#undef mjMAXSUPER + + + +// legacy row-based implementation (reference) +void mju_sqrMatTDSparse_row(mjtNum* res, const mjtNum* mat, const mjtNum* matT, + const mjtNum* diag, int nr, int nc, + int* res_rownnz, const int* res_rowadr, int* res_colind, + const int* rownnz, const int* rowadr, + const int* colind, const int* rowsuper, + const int* rownnzT, const int* rowadrT, + const int* colindT, const int* rowsuperT, + mjData* d, int* diagind) { // allocate space for accumulation buffer and matT mj_markStack(d); diff --git a/src/engine/engine_util_sparse.h b/src/engine/engine_util_sparse.h index ab7ea0f3..33f65513 100644 --- a/src/engine/engine_util_sparse.h +++ b/src/engine/engine_util_sparse.h @@ -110,6 +110,16 @@ MJAPI void mju_sqrMatTDSparse(mjtNum* res, const mjtNum* mat, const mjtNum* matT const int* colindT, const int* rowsuperT, mjData* d, int* diagind); +// LEGACY: row-based implementation +MJAPI void mju_sqrMatTDSparse_row(mjtNum* res, const mjtNum* mat, const mjtNum* matT, + const mjtNum* diag, int nr, int nc, + int* res_rownnz, const int* res_rowadr, int* res_colind, + const int* rownnz, const int* rowadr, + const int* colind, const int* rowsuper, + const int* rownnzT, const int* rowadrT, + const int* colindT, const int* rowsuperT, + mjData* d, int* diagind); + // precount res_rownnz and precompute res_rowadr for mju_sqrMatTDSparse MJAPI void mju_sqrMatTDSparseCount(int* res_rownnz, int* res_rowadr, int nr, const int* rownnz, const int* rowadr, const int* colind, diff --git a/test/benchmark/engine_util_sparse_benchmark_test.cc b/test/benchmark/engine_util_sparse_benchmark_test.cc index 6be10ded..6a1b3835 100644 --- a/test/benchmark/engine_util_sparse_benchmark_test.cc +++ b/test/benchmark/engine_util_sparse_benchmark_test.cc @@ -613,18 +613,25 @@ static void BM_sqrMatTDSparse(benchmark::State& state, SqrMatTDFuncPtr func) { } void ABSL_ATTRIBUTE_NO_TAIL_CALL -BM_sqrMatTDSparse_new(benchmark::State& state) { +BM_sqrMatTDSparse_col(benchmark::State& state) { MujocoErrorTestGuard guard; BM_sqrMatTDSparse(state, &mju_sqrMatTDSparse); } -BENCHMARK(BM_sqrMatTDSparse_new); +BENCHMARK(BM_sqrMatTDSparse_col); void ABSL_ATTRIBUTE_NO_TAIL_CALL -BM_sqrMatTDSparse_old(benchmark::State& state) { +BM_sqrMatTDSparse_row(benchmark::State& state) { + MujocoErrorTestGuard guard; + BM_sqrMatTDSparse(state, &mju_sqrMatTDSparse_row); +} +BENCHMARK(BM_sqrMatTDSparse_row); + +void ABSL_ATTRIBUTE_NO_TAIL_CALL +BM_sqrMatTDSparse_uncompressed(benchmark::State& state) { MujocoErrorTestGuard guard; BM_sqrMatTDSparse(state, nullptr); } -BENCHMARK(BM_sqrMatTDSparse_old); +BENCHMARK(BM_sqrMatTDSparse_uncompressed); } // namespace } // namespace mujoco diff --git a/test/engine/engine_util_sparse_test.cc b/test/engine/engine_util_sparse_test.cc index 7fee2127..edbd2558 100644 --- a/test/engine/engine_util_sparse_test.cc +++ b/test/engine/engine_util_sparse_test.cc @@ -377,9 +377,53 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse1) { mj_deleteModel(model); } +TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparseLower) { + // 2 -1 1 + // M = 2 -1 2 + // 2 2 3 + + mjModel* model = LoadModelFromString(""); + mjData* data = mj_makeData(model); + + mjtNum mat[] = {2, -1, 1, 2, -1, 2, 2, 2, 3}; + int colind[] = {0, 1, 2, 0, 1, 2, 0, 1, 2}; + int rownnz[] = {3, 3, 3}; + int rowadr[] = {0, 3, 6}; + + mjtNum matT[] = {2, 2, 2, -1, -1, 2, 1, 2, 3}; + int colindT[] = {0, 1, 2, 0, 1, 2, 0, 1, 2}; + int rownnzT[] = {3, 3, 3}; + int rowadrT[] = {0, 3, 6}; + + mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; + int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; + int rownnzH[] = {0, 0, 0}; + int rowadrH[] = {0, 0, 0}; + + // test precount + mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind, + rownnzT, rowadrT, colindT, nullptr, data, 0); + EXPECT_THAT(rownnzH, ElementsAre(1, 2, 3)); + EXPECT_THAT(rowadrH, ElementsAre(0, 1, 3)); + + // test computation + mju_sqrMatTDUncompressedInit(rowadrH, 3); + mju_sqrMatTDSparse(matH, mat, matT, nullptr, 3, 3, rownnzH, rowadrH, colindH, + rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT, + nullptr, data, nullptr); + + EXPECT_THAT(matH, ElementsAre(12, 0, 0, 0, 6, 0, 12, 3, 14)); + EXPECT_THAT(colindH, ElementsAre(0, 0, 0, 0, 1, 0, 0, 1, 2)); + EXPECT_THAT(rownnzH, ElementsAre(1, 2, 3)); + EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6)); + + mj_deleteData(data); + mj_deleteModel(model); +} + TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse2) { // 2 -1 1 - // M = 1 2 -1 + // M = 2 -1 2 // 2 2 3 mjModel* model = LoadModelFromString(""); @@ -419,6 +463,7 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse2) { EXPECT_THAT(colindH, ElementsAre(0, 1, 2, 0, 1, 2, 0, 1, 2)); EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3)); EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6)); + EXPECT_THAT(diagindH, ElementsAre(0, 4, 8)); mj_deleteData(data); mj_deleteModel(model); @@ -464,14 +509,63 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse3) { nullptr, data, diagindH); EXPECT_THAT(matH, ElementsAre(66, 4, 0, 4, 35, 0, 0, 0, 0)); - EXPECT_THAT(colindH, ElementsAre(0, 1, 0, 0, 1, 0, 0, 0, 0)); - EXPECT_THAT(rownnzH, ElementsAre(2, 2, 0)); + EXPECT_THAT(colindH, ElementsAre(0, 1, 0, 0, 1, 0, 2, 0, 0)); + EXPECT_THAT(rownnzH, ElementsAre(2, 2, 1)); EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6)); mj_deleteData(data); mj_deleteModel(model); } +TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse3b) { + // 1 2 0 + // M = 0 3 4 + // 5 0 0 + + mjModel* model = LoadModelFromString(""); + mjData* data = mj_makeData(model); + + mjtNum mat[] = {1, 2, 3, 4, 5}; + int colind[] = {0, 1, 1, 2, 0}; + int rownnz[] = {2, 2, 1}; + int rowadr[] = {0, 2, 4}; + + mjtNum matT[] = {1, 5, 2, 3, 4}; + int colindT[] = {0, 2, 0, 1, 1}; + int rownnzT[] = {2, 2, 1}; + int rowadrT[] = {0, 2, 4}; + + mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; + int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; + int rownnzH[] = {0, 0, 0}; + int rowadrH[] = {0, 0, 0}; + int diagindH[] = {0, 0, 0}; + + mjtNum diag[] = {1, 1, 1}; + + // test precount + mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind, + rownnzT, rowadrT, colindT, nullptr, data, 1); + + EXPECT_THAT(rownnzH, ElementsAre(2, 3, 2)); + EXPECT_THAT(rowadrH, ElementsAre(0, 2, 5)); + + // test computation + mju_sqrMatTDUncompressedInit(rowadrH, 3); + mju_sqrMatTDSparse(matH, mat, matT, diag, 3, 3, rownnzH, rowadrH, colindH, + rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT, + nullptr, data, diagindH); + + EXPECT_THAT(matH, ElementsAre(26, 2, 0, 2, 13, 12, 12, 16, 0)); + EXPECT_THAT(colindH, ElementsAre(0, 1, 0, 0, 1, 2, 1, 2, 0)); + EXPECT_THAT(rownnzH, ElementsAre(2, 3, 2)); + EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6)); + EXPECT_THAT(diagindH, ElementsAre(0, 4, 7)); + + mj_deleteData(data); + mj_deleteModel(model); +} + TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse4) { // 1 0 2 // M = 0 0 3 @@ -513,8 +607,8 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse4) { nullptr, data, diagindH); EXPECT_THAT(matH, ElementsAre(66, 4, 0, 0, 0, 0, 4, 35, 0)); - EXPECT_THAT(colindH, ElementsAre(0, 2, 0, 0, 0, 0, 0, 2, 0)); - EXPECT_THAT(rownnzH, ElementsAre(2, 0, 2)); + EXPECT_THAT(colindH, ElementsAre(0, 2, 0, 1, 0, 0, 0, 2, 0)); + EXPECT_THAT(rownnzH, ElementsAre(2, 1, 2)); EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6)); mj_deleteData(data); @@ -759,19 +853,19 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse9) { } TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse10) { - // 1 1 1 - // M = 2 2 2 - // 3 3 3 + // 1 2 3 + // M = 2 3 2 + // 3 1 1 mjModel* model = LoadModelFromString(""); mjData* data = mj_makeData(model); - mjtNum mat[] = {1, 1, 1, 2, 2, 2, 3, 3, 3}; + mjtNum mat[] = {1, 2, 3, 2, 3, 2, 3, 1, 1}; int colind[] = {0, 1, 2, 0, 1, 2, 0, 1, 2}; int rownnz[] = {3, 3, 3}; int rowadr[] = {0, 3, 6}; - mjtNum matT[] = {1, 2, 3, 1, 2, 3, 1, 2, 3}; + mjtNum matT[] = {1, 2, 3, 2, 3, 1, 3, 2, 1}; int colindT[] = {0, 1, 2, 0, 1, 2, 0, 1, 2}; int rownnzT[] = {3, 3, 3}; int rowadrT[] = {0, 3, 6}; @@ -783,7 +877,7 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse10) { int rowadrH[] = {0, 0, 0}; int diagindH[] = {0, 0, 0}; - mjtNum diag[] = {1, 1, 1}; + mjtNum diag[] = {1, 2, 1}; // test precount mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind, @@ -798,7 +892,7 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse10) { rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT, rowsuperT, data, diagindH); - EXPECT_THAT(matH, ElementsAre(14, 14, 14, 14, 14, 14, 14, 14, 14)); + EXPECT_THAT(matH, ElementsAre(18, 17, 14, 17, 23, 19, 14, 19, 18)); EXPECT_THAT(colindH, ElementsAre(0, 1, 2, 0, 1, 2, 0, 1, 2)); EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3)); EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6)); @@ -951,9 +1045,9 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse13) { EXPECT_THAT(matH, ElementsAre(3, 3, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)); - EXPECT_THAT(colindH, ElementsAre(0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)); - EXPECT_THAT(rownnzH, ElementsAre(2, 2, 0, 0, 0)); + EXPECT_THAT(colindH, ElementsAre(0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, + 3, 0, 0, 0, 0, 4, 0, 0, 0, 0)); + EXPECT_THAT(rownnzH, ElementsAre(2, 2, 1, 1, 1)); EXPECT_THAT(rowadrH, ElementsAre(0, 5, 10, 15, 20)); mj_deleteData(data); From df09980258d51c73906684e6a0c57ac1b4840f6c Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Fri, 16 May 2025 07:12:00 -0700 Subject: [PATCH 142/191] Create python module for MjcPhysics. PiperOrigin-RevId: 759602056 Change-Id: I06e604a5f6b3ca3b1a4bb949589d1b7ab72dd6ad --- .../usd/mjcPhysics/wrapSceneAPI.cpp | 653 ------------------ .../usd/mjcPhysics/wrapSiteAPI.cpp | 120 ---- .../usd/mjcPhysics/wrapTokens.cpp | 150 ---- 3 files changed, 923 deletions(-) delete mode 100644 src/experimental/usd/mjcPhysics/wrapSceneAPI.cpp delete mode 100644 src/experimental/usd/mjcPhysics/wrapSiteAPI.cpp delete mode 100644 src/experimental/usd/mjcPhysics/wrapTokens.cpp diff --git a/src/experimental/usd/mjcPhysics/wrapSceneAPI.cpp b/src/experimental/usd/mjcPhysics/wrapSceneAPI.cpp deleted file mode 100644 index f8936938..00000000 --- a/src/experimental/usd/mjcPhysics/wrapSceneAPI.cpp +++ /dev/null @@ -1,653 +0,0 @@ -// Copyright 2025 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. - -#include -#include - -#include "./sceneAPI.h" -#include "pxr/base/tf/pyAnnotatedBoolResult.h" -#include "pxr/base/tf/pyContainerConversions.h" -#include "pxr/base/tf/pyResultConversions.h" -#include "pxr/base/tf/pyUtils.h" -#include "pxr/base/tf/wrapTypeHelpers.h" -#include "pxr/usd/sdf/primSpec.h" -#include "pxr/usd/usd/pyConversions.h" -#include "pxr/usd/usd/schemaBase.h" - -using namespace boost::python; - -PXR_NAMESPACE_USING_DIRECTIVE - -namespace { - -#define WRAP_CUSTOM \ - template \ - static void _CustomWrapCode(Cls &_class) - -// fwd decl. -WRAP_CUSTOM; - -static UsdAttribute _CreateTimestepAttr(MjcPhysicsSceneAPI &self, - object defaultVal, bool writeSparsely) { - return self.CreateTimestepAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double), writeSparsely); -} - -static UsdAttribute _CreateApiRateAttr(MjcPhysicsSceneAPI &self, - object defaultVal, bool writeSparsely) { - return self.CreateApiRateAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double), writeSparsely); -} - -static UsdAttribute _CreateImpRatioAttr(MjcPhysicsSceneAPI &self, - object defaultVal, bool writeSparsely) { - return self.CreateImpRatioAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double), writeSparsely); -} - -static UsdAttribute _CreateWindAttr(MjcPhysicsSceneAPI &self, object defaultVal, - bool writeSparsely) { - return self.CreateWindAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double3), - writeSparsely); -} - -static UsdAttribute _CreateMagneticAttr(MjcPhysicsSceneAPI &self, - object defaultVal, bool writeSparsely) { - return self.CreateMagneticAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double3), - writeSparsely); -} - -static UsdAttribute _CreateDensityAttr(MjcPhysicsSceneAPI &self, - object defaultVal, bool writeSparsely) { - return self.CreateDensityAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double), writeSparsely); -} - -static UsdAttribute _CreateViscosityAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateViscosityAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double), writeSparsely); -} - -static UsdAttribute _CreateOMarginAttr(MjcPhysicsSceneAPI &self, - object defaultVal, bool writeSparsely) { - return self.CreateOMarginAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double), writeSparsely); -} - -static UsdAttribute _CreateOSolRefAttr(MjcPhysicsSceneAPI &self, - object defaultVal, bool writeSparsely) { - return self.CreateOSolRefAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->DoubleArray), - writeSparsely); -} - -static UsdAttribute _CreateOSolImpAttr(MjcPhysicsSceneAPI &self, - object defaultVal, bool writeSparsely) { - return self.CreateOSolImpAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->DoubleArray), - writeSparsely); -} - -static UsdAttribute _CreateOFrictionAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateOFrictionAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->DoubleArray), - writeSparsely); -} - -static UsdAttribute _CreateIntegratorAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateIntegratorAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Token), writeSparsely); -} - -static UsdAttribute _CreateConeAttr(MjcPhysicsSceneAPI &self, object defaultVal, - bool writeSparsely) { - return self.CreateConeAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Token), writeSparsely); -} - -static UsdAttribute _CreateJacobianAttr(MjcPhysicsSceneAPI &self, - object defaultVal, bool writeSparsely) { - return self.CreateJacobianAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Token), writeSparsely); -} - -static UsdAttribute _CreateSolverAttr(MjcPhysicsSceneAPI &self, - object defaultVal, bool writeSparsely) { - return self.CreateSolverAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Token), writeSparsely); -} - -static UsdAttribute _CreateIterationsAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateIterationsAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Int), writeSparsely); -} - -static UsdAttribute _CreateToleranceAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateToleranceAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double), writeSparsely); -} - -static UsdAttribute _CreateLSIterationsAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateLSIterationsAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Int), writeSparsely); -} - -static UsdAttribute _CreateLSToleranceAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateLSToleranceAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double), writeSparsely); -} - -static UsdAttribute _CreateNoslipIterationsAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateNoslipIterationsAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Int), writeSparsely); -} - -static UsdAttribute _CreateNoslipToleranceAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateNoslipToleranceAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double), writeSparsely); -} - -static UsdAttribute _CreateCCDIterationsAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateCCDIterationsAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Int), writeSparsely); -} - -static UsdAttribute _CreateCCDToleranceAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateCCDToleranceAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Double), writeSparsely); -} - -static UsdAttribute _CreateSDFIterationsAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateSDFIterationsAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Int), writeSparsely); -} - -static UsdAttribute _CreateSDFInitPointsAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateSDFInitPointsAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Int), writeSparsely); -} - -static UsdAttribute _CreateActuatorGroupDisableAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateActuatorGroupDisableAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->IntArray), - writeSparsely); -} - -static UsdAttribute _CreateConstraintFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateConstraintFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateEqualityFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateEqualityFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateFrictionLossFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateFrictionLossFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateLimitFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateLimitFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateContactFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateContactFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreatePassiveFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreatePassiveFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateGravityFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateGravityFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateClampCtrlFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateClampCtrlFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateWarmStartFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateWarmStartFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateFilterParentFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateFilterParentFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateActuationFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateActuationFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateRefSafeFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateRefSafeFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateSensorFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateSensorFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateMidPhaseFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateMidPhaseFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateNativeCCDFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateNativeCCDFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateEulerDampFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateEulerDampFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateAutoResetFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateAutoResetFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateOverrideFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateOverrideFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateEnergyFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateEnergyFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateFwdinvFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateFwdinvFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateInvDiscreteFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateInvDiscreteFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateMultiCCDFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateMultiCCDFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static UsdAttribute _CreateIslandFlagAttr(MjcPhysicsSceneAPI &self, - object defaultVal, - bool writeSparsely) { - return self.CreateIslandFlagAttr( - UsdPythonToSdfType(defaultVal, SdfValueTypeNames->Bool), writeSparsely); -} - -static std::string _Repr(const MjcPhysicsSceneAPI &self) { - std::string primRepr = TfPyRepr(self.GetPrim()); - return TfStringPrintf("MjcPhysics.SceneAPI(%s)", primRepr.c_str()); -} - -struct MjcPhysicsSceneAPI_CanApplyResult - : public TfPyAnnotatedBoolResult { - MjcPhysicsSceneAPI_CanApplyResult(bool val, std::string const &msg) - : TfPyAnnotatedBoolResult(val, msg) {} -}; - -static MjcPhysicsSceneAPI_CanApplyResult _WrapCanApply(const UsdPrim &prim) { - std::string whyNot; - bool result = MjcPhysicsSceneAPI::CanApply(prim, &whyNot); - return MjcPhysicsSceneAPI_CanApplyResult(result, whyNot); -} - -} // anonymous namespace - -void wrapMjcPhysicsSceneAPI() { - typedef MjcPhysicsSceneAPI This; - - MjcPhysicsSceneAPI_CanApplyResult::Wrap( - "_CanApplyResult", "whyNot"); - - class_ > cls("SceneAPI"); - - cls.def(init(arg("prim"))) - .def(init(arg("schemaObj"))) - .def(TfTypePythonClass()) - - .def("Get", &This::Get, (arg("stage"), arg("path"))) - .staticmethod("Get") - - .def("CanApply", &_WrapCanApply, (arg("prim"))) - .staticmethod("CanApply") - - .def("Apply", &This::Apply, (arg("prim"))) - .staticmethod("Apply") - - .def("GetSchemaAttributeNames", &This::GetSchemaAttributeNames, - arg("includeInherited") = true, - return_value_policy()) - .staticmethod("GetSchemaAttributeNames") - - .def("_GetStaticTfType", (TfType const &(*)())TfType::Find, - return_value_policy()) - .staticmethod("_GetStaticTfType") - - .def(!self) - - .def("GetTimestepAttr", &This::GetTimestepAttr) - .def("CreateTimestepAttr", &_CreateTimestepAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetApiRateAttr", &This::GetApiRateAttr) - .def("CreateApiRateAttr", &_CreateApiRateAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetImpRatioAttr", &This::GetImpRatioAttr) - .def("CreateImpRatioAttr", &_CreateImpRatioAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetWindAttr", &This::GetWindAttr) - .def("CreateWindAttr", &_CreateWindAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetMagneticAttr", &This::GetMagneticAttr) - .def("CreateMagneticAttr", &_CreateMagneticAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetDensityAttr", &This::GetDensityAttr) - .def("CreateDensityAttr", &_CreateDensityAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetViscosityAttr", &This::GetViscosityAttr) - .def("CreateViscosityAttr", &_CreateViscosityAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetOMarginAttr", &This::GetOMarginAttr) - .def("CreateOMarginAttr", &_CreateOMarginAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetOSolRefAttr", &This::GetOSolRefAttr) - .def("CreateOSolRefAttr", &_CreateOSolRefAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetOSolImpAttr", &This::GetOSolImpAttr) - .def("CreateOSolImpAttr", &_CreateOSolImpAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetOFrictionAttr", &This::GetOFrictionAttr) - .def("CreateOFrictionAttr", &_CreateOFrictionAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetIntegratorAttr", &This::GetIntegratorAttr) - .def("CreateIntegratorAttr", &_CreateIntegratorAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetConeAttr", &This::GetConeAttr) - .def("CreateConeAttr", &_CreateConeAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetJacobianAttr", &This::GetJacobianAttr) - .def("CreateJacobianAttr", &_CreateJacobianAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetSolverAttr", &This::GetSolverAttr) - .def("CreateSolverAttr", &_CreateSolverAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetIterationsAttr", &This::GetIterationsAttr) - .def("CreateIterationsAttr", &_CreateIterationsAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetToleranceAttr", &This::GetToleranceAttr) - .def("CreateToleranceAttr", &_CreateToleranceAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetLSIterationsAttr", &This::GetLSIterationsAttr) - .def("CreateLSIterationsAttr", &_CreateLSIterationsAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetLSToleranceAttr", &This::GetLSToleranceAttr) - .def("CreateLSToleranceAttr", &_CreateLSToleranceAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetNoslipIterationsAttr", &This::GetNoslipIterationsAttr) - .def("CreateNoslipIterationsAttr", &_CreateNoslipIterationsAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetNoslipToleranceAttr", &This::GetNoslipToleranceAttr) - .def("CreateNoslipToleranceAttr", &_CreateNoslipToleranceAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetCCDIterationsAttr", &This::GetCCDIterationsAttr) - .def("CreateCCDIterationsAttr", &_CreateCCDIterationsAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetCCDToleranceAttr", &This::GetCCDToleranceAttr) - .def("CreateCCDToleranceAttr", &_CreateCCDToleranceAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetSDFIterationsAttr", &This::GetSDFIterationsAttr) - .def("CreateSDFIterationsAttr", &_CreateSDFIterationsAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetSDFInitPointsAttr", &This::GetSDFInitPointsAttr) - .def("CreateSDFInitPointsAttr", &_CreateSDFInitPointsAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetActuatorGroupDisableAttr", &This::GetActuatorGroupDisableAttr) - .def("CreateActuatorGroupDisableAttr", &_CreateActuatorGroupDisableAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetConstraintFlagAttr", &This::GetConstraintFlagAttr) - .def("CreateConstraintFlagAttr", &_CreateConstraintFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetEqualityFlagAttr", &This::GetEqualityFlagAttr) - .def("CreateEqualityFlagAttr", &_CreateEqualityFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetFrictionLossFlagAttr", &This::GetFrictionLossFlagAttr) - .def("CreateFrictionLossFlagAttr", &_CreateFrictionLossFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetLimitFlagAttr", &This::GetLimitFlagAttr) - .def("CreateLimitFlagAttr", &_CreateLimitFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetContactFlagAttr", &This::GetContactFlagAttr) - .def("CreateContactFlagAttr", &_CreateContactFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetPassiveFlagAttr", &This::GetPassiveFlagAttr) - .def("CreatePassiveFlagAttr", &_CreatePassiveFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetGravityFlagAttr", &This::GetGravityFlagAttr) - .def("CreateGravityFlagAttr", &_CreateGravityFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetClampCtrlFlagAttr", &This::GetClampCtrlFlagAttr) - .def("CreateClampCtrlFlagAttr", &_CreateClampCtrlFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetWarmStartFlagAttr", &This::GetWarmStartFlagAttr) - .def("CreateWarmStartFlagAttr", &_CreateWarmStartFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetFilterParentFlagAttr", &This::GetFilterParentFlagAttr) - .def("CreateFilterParentFlagAttr", &_CreateFilterParentFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetActuationFlagAttr", &This::GetActuationFlagAttr) - .def("CreateActuationFlagAttr", &_CreateActuationFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetRefSafeFlagAttr", &This::GetRefSafeFlagAttr) - .def("CreateRefSafeFlagAttr", &_CreateRefSafeFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetSensorFlagAttr", &This::GetSensorFlagAttr) - .def("CreateSensorFlagAttr", &_CreateSensorFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetMidPhaseFlagAttr", &This::GetMidPhaseFlagAttr) - .def("CreateMidPhaseFlagAttr", &_CreateMidPhaseFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetNativeCCDFlagAttr", &This::GetNativeCCDFlagAttr) - .def("CreateNativeCCDFlagAttr", &_CreateNativeCCDFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetEulerDampFlagAttr", &This::GetEulerDampFlagAttr) - .def("CreateEulerDampFlagAttr", &_CreateEulerDampFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetAutoResetFlagAttr", &This::GetAutoResetFlagAttr) - .def("CreateAutoResetFlagAttr", &_CreateAutoResetFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetOverrideFlagAttr", &This::GetOverrideFlagAttr) - .def("CreateOverrideFlagAttr", &_CreateOverrideFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetEnergyFlagAttr", &This::GetEnergyFlagAttr) - .def("CreateEnergyFlagAttr", &_CreateEnergyFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetFwdinvFlagAttr", &This::GetFwdinvFlagAttr) - .def("CreateFwdinvFlagAttr", &_CreateFwdinvFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetInvDiscreteFlagAttr", &This::GetInvDiscreteFlagAttr) - .def("CreateInvDiscreteFlagAttr", &_CreateInvDiscreteFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetMultiCCDFlagAttr", &This::GetMultiCCDFlagAttr) - .def("CreateMultiCCDFlagAttr", &_CreateMultiCCDFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("GetIslandFlagAttr", &This::GetIslandFlagAttr) - .def("CreateIslandFlagAttr", &_CreateIslandFlagAttr, - (arg("defaultValue") = object(), arg("writeSparsely") = false)) - - .def("__repr__", ::_Repr); - - _CustomWrapCode(cls); -} - -// ===================================================================== // -// Feel free to add custom code below this line, it will be preserved by -// the code generator. The entry point for your custom code should look -// minimally like the following: -// -// WRAP_CUSTOM { -// _class -// .def("MyCustomMethod", ...) -// ; -// } -// -// Of course any other ancillary or support code may be provided. -// -// Just remember to wrap code in the appropriate delimiters: -// 'namespace {', '}'. -// -// ===================================================================== // -// --(BEGIN CUSTOM CODE)-- - -namespace { - -WRAP_CUSTOM {} - -} // namespace diff --git a/src/experimental/usd/mjcPhysics/wrapSiteAPI.cpp b/src/experimental/usd/mjcPhysics/wrapSiteAPI.cpp deleted file mode 100644 index 267978d7..00000000 --- a/src/experimental/usd/mjcPhysics/wrapSiteAPI.cpp +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2025 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. - -#include -#include - -#include "./siteAPI.h" -#include "pxr/base/tf/pyAnnotatedBoolResult.h" -#include "pxr/base/tf/pyContainerConversions.h" -#include "pxr/base/tf/pyResultConversions.h" -#include "pxr/base/tf/pyUtils.h" -#include "pxr/base/tf/wrapTypeHelpers.h" -#include "pxr/usd/sdf/primSpec.h" -#include "pxr/usd/usd/pyConversions.h" -#include "pxr/usd/usd/schemaBase.h" - -using namespace boost::python; - -PXR_NAMESPACE_USING_DIRECTIVE - -namespace { - -#define WRAP_CUSTOM \ - template \ - static void _CustomWrapCode(Cls &_class) - -// fwd decl. -WRAP_CUSTOM; - -static std::string _Repr(const MjcPhysicsSiteAPI &self) { - std::string primRepr = TfPyRepr(self.GetPrim()); - return TfStringPrintf("MjcPhysics.SiteAPI(%s)", primRepr.c_str()); -} - -struct MjcPhysicsSiteAPI_CanApplyResult - : public TfPyAnnotatedBoolResult { - MjcPhysicsSiteAPI_CanApplyResult(bool val, std::string const &msg) - : TfPyAnnotatedBoolResult(val, msg) {} -}; - -static MjcPhysicsSiteAPI_CanApplyResult _WrapCanApply(const UsdPrim &prim) { - std::string whyNot; - bool result = MjcPhysicsSiteAPI::CanApply(prim, &whyNot); - return MjcPhysicsSiteAPI_CanApplyResult(result, whyNot); -} - -} // anonymous namespace - -void wrapMjcPhysicsSiteAPI() { - typedef MjcPhysicsSiteAPI This; - - MjcPhysicsSiteAPI_CanApplyResult::Wrap( - "_CanApplyResult", "whyNot"); - - class_ > cls("SiteAPI"); - - cls.def(init(arg("prim"))) - .def(init(arg("schemaObj"))) - .def(TfTypePythonClass()) - - .def("Get", &This::Get, (arg("stage"), arg("path"))) - .staticmethod("Get") - - .def("CanApply", &_WrapCanApply, (arg("prim"))) - .staticmethod("CanApply") - - .def("Apply", &This::Apply, (arg("prim"))) - .staticmethod("Apply") - - .def("GetSchemaAttributeNames", &This::GetSchemaAttributeNames, - arg("includeInherited") = true, - return_value_policy()) - .staticmethod("GetSchemaAttributeNames") - - .def("_GetStaticTfType", (TfType const &(*)())TfType::Find, - return_value_policy()) - .staticmethod("_GetStaticTfType") - - .def(!self) - - .def("__repr__", ::_Repr); - - _CustomWrapCode(cls); -} - -// ===================================================================== // -// Feel free to add custom code below this line, it will be preserved by -// the code generator. The entry point for your custom code should look -// minimally like the following: -// -// WRAP_CUSTOM { -// _class -// .def("MyCustomMethod", ...) -// ; -// } -// -// Of course any other ancillary or support code may be provided. -// -// Just remember to wrap code in the appropriate delimiters: -// 'namespace {', '}'. -// -// ===================================================================== // -// --(BEGIN CUSTOM CODE)-- - -namespace { - -WRAP_CUSTOM {} - -} // namespace diff --git a/src/experimental/usd/mjcPhysics/wrapTokens.cpp b/src/experimental/usd/mjcPhysics/wrapTokens.cpp deleted file mode 100644 index 3a5aa603..00000000 --- a/src/experimental/usd/mjcPhysics/wrapTokens.cpp +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright 2025 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. - -// GENERATED FILE. DO NOT EDIT. -#include - -#include "./tokens.h" - -PXR_NAMESPACE_USING_DIRECTIVE - -namespace { - -// Helper to return a static token as a string. We wrap tokens as Python -// strings and for some reason simply wrapping the token using def_readonly -// bypasses to-Python conversion, leading to the error that there's no -// Python type for the C++ TfToken type. So we wrap this functor instead. -class _WrapStaticToken { - public: - _WrapStaticToken(const TfToken* token) : _token(token) {} - - std::string operator()() const { return _token->GetString(); } - - private: - const TfToken* _token; -}; - -template -void _AddToken(T& cls, const char* name, const TfToken& token) { - cls.add_static_property( - name, - boost::python::make_function( - _WrapStaticToken(&token), - boost::python::return_value_policy(), - boost::mpl::vector1())); -} - -} // namespace - -void wrapMjcPhysicsTokens() { - boost::python::class_ cls( - "Tokens", boost::python::no_init); - _AddToken(cls, "auto_", MjcPhysicsTokens->auto_); - _AddToken(cls, "cg", MjcPhysicsTokens->cg); - _AddToken(cls, "dense", MjcPhysicsTokens->dense); - _AddToken(cls, "elliptic", MjcPhysicsTokens->elliptic); - _AddToken(cls, "euler", MjcPhysicsTokens->euler); - _AddToken(cls, "implicit", MjcPhysicsTokens->implicit); - _AddToken(cls, "implicitfast", MjcPhysicsTokens->implicitfast); - _AddToken(cls, "mjcPhysicsActuatorgroupdisable", - MjcPhysicsTokens->mjcPhysicsActuatorgroupdisable); - _AddToken(cls, "mjcPhysicsApirate", MjcPhysicsTokens->mjcPhysicsApirate); - _AddToken(cls, "mjcPhysicsCcd_iterations", - MjcPhysicsTokens->mjcPhysicsCcd_iterations); - _AddToken(cls, "mjcPhysicsCcd_tolerance", - MjcPhysicsTokens->mjcPhysicsCcd_tolerance); - _AddToken(cls, "mjcPhysicsCone", MjcPhysicsTokens->mjcPhysicsCone); - _AddToken(cls, "mjcPhysicsDensity", MjcPhysicsTokens->mjcPhysicsDensity); - _AddToken(cls, "mjcPhysicsFlagActuation", - MjcPhysicsTokens->mjcPhysicsFlagActuation); - _AddToken(cls, "mjcPhysicsFlagAutoreset", - MjcPhysicsTokens->mjcPhysicsFlagAutoreset); - _AddToken(cls, "mjcPhysicsFlagClampctrl", - MjcPhysicsTokens->mjcPhysicsFlagClampctrl); - _AddToken(cls, "mjcPhysicsFlagConstraint", - MjcPhysicsTokens->mjcPhysicsFlagConstraint); - _AddToken(cls, "mjcPhysicsFlagContact", - MjcPhysicsTokens->mjcPhysicsFlagContact); - _AddToken(cls, "mjcPhysicsFlagEnergy", - MjcPhysicsTokens->mjcPhysicsFlagEnergy); - _AddToken(cls, "mjcPhysicsFlagEquality", - MjcPhysicsTokens->mjcPhysicsFlagEquality); - _AddToken(cls, "mjcPhysicsFlagEulerdamp", - MjcPhysicsTokens->mjcPhysicsFlagEulerdamp); - _AddToken(cls, "mjcPhysicsFlagFilterparent", - MjcPhysicsTokens->mjcPhysicsFlagFilterparent); - _AddToken(cls, "mjcPhysicsFlagFrictionloss", - MjcPhysicsTokens->mjcPhysicsFlagFrictionloss); - _AddToken(cls, "mjcPhysicsFlagFwdinv", - MjcPhysicsTokens->mjcPhysicsFlagFwdinv); - _AddToken(cls, "mjcPhysicsFlagGravity", - MjcPhysicsTokens->mjcPhysicsFlagGravity); - _AddToken(cls, "mjcPhysicsFlagInvdiscrete", - MjcPhysicsTokens->mjcPhysicsFlagInvdiscrete); - _AddToken(cls, "mjcPhysicsFlagIsland", - MjcPhysicsTokens->mjcPhysicsFlagIsland); - _AddToken(cls, "mjcPhysicsFlagLimit", MjcPhysicsTokens->mjcPhysicsFlagLimit); - _AddToken(cls, "mjcPhysicsFlagMidphase", - MjcPhysicsTokens->mjcPhysicsFlagMidphase); - _AddToken(cls, "mjcPhysicsFlagMulticcd", - MjcPhysicsTokens->mjcPhysicsFlagMulticcd); - _AddToken(cls, "mjcPhysicsFlagNativeccd", - MjcPhysicsTokens->mjcPhysicsFlagNativeccd); - _AddToken(cls, "mjcPhysicsFlagOverride", - MjcPhysicsTokens->mjcPhysicsFlagOverride); - _AddToken(cls, "mjcPhysicsFlagPassive", - MjcPhysicsTokens->mjcPhysicsFlagPassive); - _AddToken(cls, "mjcPhysicsFlagRefsafe", - MjcPhysicsTokens->mjcPhysicsFlagRefsafe); - _AddToken(cls, "mjcPhysicsFlagSensor", - MjcPhysicsTokens->mjcPhysicsFlagSensor); - _AddToken(cls, "mjcPhysicsFlagWarmstart", - MjcPhysicsTokens->mjcPhysicsFlagWarmstart); - _AddToken(cls, "mjcPhysicsImpratio", MjcPhysicsTokens->mjcPhysicsImpratio); - _AddToken(cls, "mjcPhysicsIntegrator", - MjcPhysicsTokens->mjcPhysicsIntegrator); - _AddToken(cls, "mjcPhysicsIterations", - MjcPhysicsTokens->mjcPhysicsIterations); - _AddToken(cls, "mjcPhysicsJacobian", MjcPhysicsTokens->mjcPhysicsJacobian); - _AddToken(cls, "mjcPhysicsLs_iterations", - MjcPhysicsTokens->mjcPhysicsLs_iterations); - _AddToken(cls, "mjcPhysicsLs_tolerance", - MjcPhysicsTokens->mjcPhysicsLs_tolerance); - _AddToken(cls, "mjcPhysicsMagnetic", MjcPhysicsTokens->mjcPhysicsMagnetic); - _AddToken(cls, "mjcPhysicsNoslip_iterations", - MjcPhysicsTokens->mjcPhysicsNoslip_iterations); - _AddToken(cls, "mjcPhysicsNoslip_tolerance", - MjcPhysicsTokens->mjcPhysicsNoslip_tolerance); - _AddToken(cls, "mjcPhysicsO_friction", - MjcPhysicsTokens->mjcPhysicsO_friction); - _AddToken(cls, "mjcPhysicsO_margin", MjcPhysicsTokens->mjcPhysicsO_margin); - _AddToken(cls, "mjcPhysicsO_solimp", MjcPhysicsTokens->mjcPhysicsO_solimp); - _AddToken(cls, "mjcPhysicsO_solref", MjcPhysicsTokens->mjcPhysicsO_solref); - _AddToken(cls, "mjcPhysicsSdf_initpoints", - MjcPhysicsTokens->mjcPhysicsSdf_initpoints); - _AddToken(cls, "mjcPhysicsSdf_iterations", - MjcPhysicsTokens->mjcPhysicsSdf_iterations); - _AddToken(cls, "mjcPhysicsSolver", MjcPhysicsTokens->mjcPhysicsSolver); - _AddToken(cls, "mjcPhysicsTimestep", MjcPhysicsTokens->mjcPhysicsTimestep); - _AddToken(cls, "mjcPhysicsTolerance", MjcPhysicsTokens->mjcPhysicsTolerance); - _AddToken(cls, "mjcPhysicsViscosity", MjcPhysicsTokens->mjcPhysicsViscosity); - _AddToken(cls, "mjcPhysicsWind", MjcPhysicsTokens->mjcPhysicsWind); - _AddToken(cls, "newton", MjcPhysicsTokens->newton); - _AddToken(cls, "pgs", MjcPhysicsTokens->pgs); - _AddToken(cls, "pyramidal", MjcPhysicsTokens->pyramidal); - _AddToken(cls, "rk4", MjcPhysicsTokens->rk4); - _AddToken(cls, "sparse", MjcPhysicsTokens->sparse); - _AddToken(cls, "SceneAPI", MjcPhysicsTokens->SceneAPI); - _AddToken(cls, "SiteAPI", MjcPhysicsTokens->SiteAPI); -} From 178fb49c2b2ff48f59653515ab09b9cafca31b7a Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Fri, 16 May 2025 08:53:16 -0700 Subject: [PATCH 143/191] Fix namespacing in USD schema. PiperOrigin-RevId: 759632714 Change-Id: I666ca0e5b7f264f63eae0bbb39f0403654fb1c56 --- .../usd/mjcPhysics/generatedSchema.usda | 236 +++++++------- src/experimental/usd/mjcPhysics/sceneAPI.cpp | 294 +++++++++--------- src/experimental/usd/mjcPhysics/sceneAPI.h | 104 +++---- src/experimental/usd/mjcPhysics/schema.usda | 98 +++--- src/experimental/usd/mjcPhysics/tokens.cpp | 202 ++++++------ src/experimental/usd/mjcPhysics/tokens.h | 196 ++++++------ 6 files changed, 563 insertions(+), 567 deletions(-) diff --git a/src/experimental/usd/mjcPhysics/generatedSchema.usda b/src/experimental/usd/mjcPhysics/generatedSchema.usda index 4cb4d177..ee17510b 100644 --- a/src/experimental/usd/mjcPhysics/generatedSchema.usda +++ b/src/experimental/usd/mjcPhysics/generatedSchema.usda @@ -7,214 +7,214 @@ class "SceneAPI" ( doc = "API providing global simulation options for Mujoco." ) { - uniform int[] mjc:physics:actuatorgroupdisable ( + uniform bool mjc:flag:actuation = 1 ( + displayName = "Actuation Forces Toggle" + doc = "Enables all standard computations related to actuator forces, including actuator dynamics." + ) + uniform bool mjc:flag:autoreset = 1 ( + displayName = "Automatic Simulation Reset Toggle" + doc = "Enables the automatic resetting of the simulation state when numerical issues are detected." + ) + uniform bool mjc:flag:clampctrl = 1 ( + displayName = "Control Input Clamping Toggle" + doc = "Enables the clamping of control inputs to all actuators, according to actuator-specific attributes." + ) + uniform bool mjc:flag:constraint = 1 ( + displayName = "Constraint Solver Toggle" + doc = "Enables constraint solver." + ) + uniform bool mjc:flag:contact = 1 ( + displayName = "Contact Constraints and Collision Detection Toggle" + doc = "Enables collision detection and all standard computations related to contact constraints." + ) + uniform bool mjc:flag:energy = 0 ( + displayName = "Energy Computation Toggle" + doc = "Enables the computation of potential and kinetic energy (mjData.energy[0,1])." + ) + uniform bool mjc:flag:equality = 1 ( + displayName = "Equality Constraints Toggle" + doc = "Enables all standard computations related to equality constraints." + ) + uniform bool mjc:flag:eulerdamp = 1 ( + displayName = "Euler Integrator Damping Toggle" + doc = "Enables implicit integration with respect to joint damping in the Euler integrator." + ) + uniform bool mjc:flag:filterparent = 1 ( + displayName = "Parent-Child Contact Filtering Toggle" + doc = "Enables the filtering of contact pairs where the two geoms belong to a parent and child body." + ) + uniform bool mjc:flag:frictionloss = 1 ( + displayName = "Friction Loss Constraints Toggle" + doc = "Enables all standard computations related to friction loss constraints." + ) + uniform bool mjc:flag:fwdinv = 0 ( + displayName = "Forward/Inverse Dynamics Comparison Toggle" + doc = "Enables the automatic comparison of forward and inverse dynamics." + ) + uniform bool mjc:flag:gravity = 1 ( + displayName = "Gravity Toggle" + doc = "Enables the application of gravitational acceleration as defined in mjOption." + ) + uniform bool mjc:flag:invdiscrete = 0 ( + displayName = "Discrete-Time Inverse Dynamics Toggle" + doc = "Enables discrete-time inverse dynamics with mj_inverse for integrators other than RK4." + ) + uniform bool mjc:flag:island = 0 ( + displayName = "Constraint Island Discovery Toggle" + doc = "Enables the discovery of constraint islands." + ) + uniform bool mjc:flag:limit = 1 ( + displayName = "Joint and Tendon Limit Constraints Toggle" + doc = "Enables all standard computations related to joint and tendon limit constraints." + ) + uniform bool mjc:flag:midphase = 1 ( + displayName = "Mid-Phase Collision Filtering Toggle" + doc = "Enables mid-phase collision filtering using a static AABB bounding volume hierarchy (BVH)." + ) + uniform bool mjc:flag:multiccd = 0 ( + displayName = "Multiple Contact Collision Detection (CCD) Toggle" + doc = "Enables multiple-contact collision detection for geom pairs using a general-purpose convex-convex collider." + ) + uniform bool mjc:flag:nativeccd = 1 ( + displayName = "Native Convex Collision Detection Toggle" + doc = "Enables the native convex collision detection pipeline instead of using the libccd library." + ) + uniform bool mjc:flag:override = 0 ( + displayName = "Contact Override Mechanism Toggle" + doc = "Enables the contact override mechanism." + ) + uniform bool mjc:flag:passive = 1 ( + displayName = "Passive Forces Toggle" + doc = "Enables the simulation of joint and tendon spring-dampers, fluid dynamics forces, and custom passive forces." + ) + uniform bool mjc:flag:refsafe = 1 ( + displayName = "Solver Reference Safety Mechanism Toggle" + doc = "Enables a safety mechanism that prevents instabilities due to solref[0] being too small compared to the simulation timestep." + ) + uniform bool mjc:flag:sensor = 1 ( + displayName = "Sensor Computations Toggle" + doc = "Enables all computations related to sensors." + ) + uniform bool mjc:flag:warmstart = 1 ( + displayName = "Solver Warm-Starting Toggle" + doc = "Enables warm-starting of the constraint solver, using the solution from the previous time step to initialize the iterative optimization." + ) + uniform int[] mjc:option:actuatorgroupdisable ( displayName = "Actuator Group Disable" doc = "List of actuator groups to disable." ) - uniform double mjc:physics:apirate = 100 ( + uniform double mjc:option:apirate = 100 ( displayName = "ApiRate" doc = """Determines the rate (in Hz) at which an external API allows the update function to be executed.""" ) - uniform int mjc:physics:ccd_iterations = 50 ( + uniform int mjc:option:ccd_iterations = 50 ( displayName = "CCD Iterations" doc = "Maximum number of iterations of the algorithm used for convex collisions." ) - uniform double mjc:physics:ccd_tolerance = 0.000001 ( + uniform double mjc:option:ccd_tolerance = 0.000001 ( displayName = "CCD Tolerance" doc = """Tolerance threshold used for early termination of the convex collision algorithm.""" ) - uniform token mjc:physics:cone = "pyramidal" ( + uniform token mjc:option:cone = "pyramidal" ( allowedTokens = ["pyramidal", "elliptic"] displayName = "Friction Cone Type" doc = "The type of contact friction cone." ) - uniform double mjc:physics:density = 0 ( + uniform double mjc:option:density = 0 ( displayName = "Density" doc = "Density of medium." ) - uniform bool mjc:physics:flag:actuation = 1 ( - displayName = "Actuation Forces Toggle" - doc = "Enables all standard computations related to actuator forces, including actuator dynamics." - ) - uniform bool mjc:physics:flag:autoreset = 1 ( - displayName = "Automatic Simulation Reset Toggle" - doc = "Enables the automatic resetting of the simulation state when numerical issues are detected." - ) - uniform bool mjc:physics:flag:clampctrl = 1 ( - displayName = "Control Input Clamping Toggle" - doc = "Enables the clamping of control inputs to all actuators, according to actuator-specific attributes." - ) - uniform bool mjc:physics:flag:constraint = 1 ( - displayName = "Constraint Solver Toggle" - doc = "Enables constraint solver." - ) - uniform bool mjc:physics:flag:contact = 1 ( - displayName = "Contact Constraints and Collision Detection Toggle" - doc = "Enables collision detection and all standard computations related to contact constraints." - ) - uniform bool mjc:physics:flag:energy = 0 ( - displayName = "Energy Computation Toggle" - doc = "Enables the computation of potential and kinetic energy (mjData.energy[0,1])." - ) - uniform bool mjc:physics:flag:equality = 1 ( - displayName = "Equality Constraints Toggle" - doc = "Enables all standard computations related to equality constraints." - ) - uniform bool mjc:physics:flag:eulerdamp = 1 ( - displayName = "Euler Integrator Damping Toggle" - doc = "Enables implicit integration with respect to joint damping in the Euler integrator." - ) - uniform bool mjc:physics:flag:filterparent = 1 ( - displayName = "Parent-Child Contact Filtering Toggle" - doc = "Enables the filtering of contact pairs where the two geoms belong to a parent and child body." - ) - uniform bool mjc:physics:flag:frictionloss = 1 ( - displayName = "Friction Loss Constraints Toggle" - doc = "Enables all standard computations related to friction loss constraints." - ) - uniform bool mjc:physics:flag:fwdinv = 0 ( - displayName = "Forward/Inverse Dynamics Comparison Toggle" - doc = "Enables the automatic comparison of forward and inverse dynamics." - ) - uniform bool mjc:physics:flag:gravity = 1 ( - displayName = "Gravity Toggle" - doc = "Enables the application of gravitational acceleration as defined in mjOption." - ) - uniform bool mjc:physics:flag:invdiscrete = 0 ( - displayName = "Discrete-Time Inverse Dynamics Toggle" - doc = "Enables discrete-time inverse dynamics with mj_inverse for integrators other than RK4." - ) - uniform bool mjc:physics:flag:island = 0 ( - displayName = "Constraint Island Discovery Toggle" - doc = "Enables the discovery of constraint islands." - ) - uniform bool mjc:physics:flag:limit = 1 ( - displayName = "Joint and Tendon Limit Constraints Toggle" - doc = "Enables all standard computations related to joint and tendon limit constraints." - ) - uniform bool mjc:physics:flag:midphase = 1 ( - displayName = "Mid-Phase Collision Filtering Toggle" - doc = "Enables mid-phase collision filtering using a static AABB bounding volume hierarchy (BVH)." - ) - uniform bool mjc:physics:flag:multiccd = 0 ( - displayName = "Multiple Contact Collision Detection (CCD) Toggle" - doc = "Enables multiple-contact collision detection for geom pairs using a general-purpose convex-convex collider." - ) - uniform bool mjc:physics:flag:nativeccd = 1 ( - displayName = "Native Convex Collision Detection Toggle" - doc = "Enables the native convex collision detection pipeline instead of using the libccd library." - ) - uniform bool mjc:physics:flag:override = 0 ( - displayName = "Contact Override Mechanism Toggle" - doc = "Enables the contact override mechanism." - ) - uniform bool mjc:physics:flag:passive = 1 ( - displayName = "Passive Forces Toggle" - doc = "Enables the simulation of joint and tendon spring-dampers, fluid dynamics forces, and custom passive forces." - ) - uniform bool mjc:physics:flag:refsafe = 1 ( - displayName = "Solver Reference Safety Mechanism Toggle" - doc = "Enables a safety mechanism that prevents instabilities due to solref[0] being too small compared to the simulation timestep." - ) - uniform bool mjc:physics:flag:sensor = 1 ( - displayName = "Sensor Computations Toggle" - doc = "Enables all computations related to sensors." - ) - uniform bool mjc:physics:flag:warmstart = 1 ( - displayName = "Solver Warm-Starting Toggle" - doc = "Enables warm-starting of the constraint solver, using the solution from the previous time step to initialize the iterative optimization." - ) - uniform double mjc:physics:impratio = 1 ( + uniform double mjc:option:impratio = 1 ( displayName = "Impedance Ratio" doc = """Ratio of frictional-to-normal constraint impedance for elliptic friction cones.""" ) - uniform token mjc:physics:integrator = "euler" ( + uniform token mjc:option:integrator = "euler" ( allowedTokens = ["euler", "rk4", "implicit", "implicitfast"] displayName = "Integrator" doc = "Numerical integrator to be used." ) - uniform int mjc:physics:iterations = 100 ( + uniform int mjc:option:iterations = 100 ( displayName = "Solver Iterations" doc = "Maximum number of iterations of the constraint solver." ) - uniform token mjc:physics:jacobian = "auto" ( + uniform token mjc:option:jacobian = "auto" ( allowedTokens = ["auto", "dense", "sparse"] displayName = "Jacobian Type" doc = "The type of constraint Jacobian and matrices computed from it." ) - uniform int mjc:physics:ls_iterations = 50 ( + uniform int mjc:option:ls_iterations = 50 ( displayName = "Linesearch Iterations" doc = """Maximum number of linesearch iterations performed by CG/Newton constraint solvers.""" ) - uniform double mjc:physics:ls_tolerance = 0.01 ( + uniform double mjc:option:ls_tolerance = 0.01 ( displayName = "Linesearch Tolerance" doc = "Tolerance threshold used for early termination of the linesearch algorithm." ) - uniform double3 mjc:physics:magnetic = (0, -0.5, 0) ( + uniform double3 mjc:option:magnetic = (0, -0.5, 0) ( displayName = "Magnetic Flux" doc = "Global magnetic flux." ) - uniform int mjc:physics:noslip_iterations = 0 ( + uniform int mjc:option:noslip_iterations = 0 ( displayName = "Noslip Iterations" doc = "Maximum number of iterations of the Noslip solver." ) - uniform double mjc:physics:noslip_tolerance = 0.000001 ( + uniform double mjc:option:noslip_tolerance = 0.000001 ( displayName = "Noslip Tolerance" doc = "Tolerance threshold used for early termination of the Noslip solver." ) - uniform double[] mjc:physics:o_friction = [1, 1, 0.005, 0.0001, 0.0001] ( + uniform double[] mjc:option:o_friction = [1, 1, 0.005, 0.0001, 0.0001] ( displayName = "Contact Override Friction" doc = """Replaces the friction parameter of all active contact pairs when Contact override is enabled.""" ) - uniform double mjc:physics:o_margin = 0 ( + uniform double mjc:option:o_margin = 0 ( displayName = "Contact Override Margin" doc = """Replaces the margin parameter of all active contact pairs when Contact override is enabled.""" ) - uniform double[] mjc:physics:o_solimp = [0.9, 0.95, 0.001, 0.5, 2] ( + uniform double[] mjc:option:o_solimp = [0.9, 0.95, 0.001, 0.5, 2] ( displayName = "Contact Override SolImp" doc = """Replaces the solimp parameter of all active contact pairs when Contact override is enabled.""" ) - uniform double[] mjc:physics:o_solref = [0.02, 1] ( + uniform double[] mjc:option:o_solref = [0.02, 1] ( displayName = "Contact Override SolRef" doc = """Replaces the solref parameter of all active contact pairs when Contact override is enabled.""" ) - uniform int mjc:physics:sdf_initpoints = 40 ( + uniform int mjc:option:sdf_initpoints = 40 ( displayName = "SDF Initial Points" doc = """Number of starting points used for finding contacts with Signed Distance Field collisions.""" ) - uniform int mjc:physics:sdf_iterations = 10 ( + uniform int mjc:option:sdf_iterations = 10 ( displayName = "SDF Iterations" doc = """Number of iterations used for Signed Distance Field collisions (per initial point).""" ) - uniform token mjc:physics:solver = "newton" ( + uniform token mjc:option:solver = "newton" ( allowedTokens = ["pgs", "cg", "newton"] displayName = "Solver" doc = "Constraint solver algorithm to be used." ) - uniform double mjc:physics:timestep = 0.002 ( + uniform double mjc:option:timestep = 0.002 ( displayName = "Timestep" doc = "Controls the timestep in seconds used by MuJoCo." ) - uniform double mjc:physics:tolerance = 1e-8 ( + uniform double mjc:option:tolerance = 1e-8 ( displayName = "Solver Tolerance" doc = """Tolerance threshold used for early termination of the iterative solver.""" ) - uniform double mjc:physics:viscosity = 0 ( + uniform double mjc:option:viscosity = 0 ( displayName = "Viscosity" doc = "Viscosity of medium." ) - uniform double3 mjc:physics:wind = (0, 0, 0) ( + uniform double3 mjc:option:wind = (0, 0, 0) ( displayName = "Wind Velocity" doc = "Velocity vector of medium (i.e. wind)." ) diff --git a/src/experimental/usd/mjcPhysics/sceneAPI.cpp b/src/experimental/usd/mjcPhysics/sceneAPI.cpp index 0c01a533..7bce5c2d 100644 --- a/src/experimental/usd/mjcPhysics/sceneAPI.cpp +++ b/src/experimental/usd/mjcPhysics/sceneAPI.cpp @@ -75,543 +75,543 @@ const TfType &MjcPhysicsSceneAPI::_GetTfType() const { } UsdAttribute MjcPhysicsSceneAPI::GetTimestepAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsTimestep); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionTimestep); } UsdAttribute MjcPhysicsSceneAPI::CreateTimestepAttr(VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsTimestep, SdfValueTypeNames->Double, + MjcPhysicsTokens->mjcOptionTimestep, SdfValueTypeNames->Double, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetApiRateAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsApirate); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionApirate); } UsdAttribute MjcPhysicsSceneAPI::CreateApiRateAttr(VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsApirate, SdfValueTypeNames->Double, + MjcPhysicsTokens->mjcOptionApirate, SdfValueTypeNames->Double, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetImpRatioAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsImpratio); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionImpratio); } UsdAttribute MjcPhysicsSceneAPI::CreateImpRatioAttr(VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsImpratio, SdfValueTypeNames->Double, + MjcPhysicsTokens->mjcOptionImpratio, SdfValueTypeNames->Double, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetWindAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsWind); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionWind); } UsdAttribute MjcPhysicsSceneAPI::CreateWindAttr(VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsWind, SdfValueTypeNames->Double3, + MjcPhysicsTokens->mjcOptionWind, SdfValueTypeNames->Double3, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetMagneticAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsMagnetic); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionMagnetic); } UsdAttribute MjcPhysicsSceneAPI::CreateMagneticAttr(VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsMagnetic, SdfValueTypeNames->Double3, + MjcPhysicsTokens->mjcOptionMagnetic, SdfValueTypeNames->Double3, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetDensityAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsDensity); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionDensity); } UsdAttribute MjcPhysicsSceneAPI::CreateDensityAttr(VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsDensity, SdfValueTypeNames->Double, + MjcPhysicsTokens->mjcOptionDensity, SdfValueTypeNames->Double, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetViscosityAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsViscosity); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionViscosity); } UsdAttribute MjcPhysicsSceneAPI::CreateViscosityAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsViscosity, SdfValueTypeNames->Double, + MjcPhysicsTokens->mjcOptionViscosity, SdfValueTypeNames->Double, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetOMarginAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsO_margin); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionO_margin); } UsdAttribute MjcPhysicsSceneAPI::CreateOMarginAttr(VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsO_margin, SdfValueTypeNames->Double, + MjcPhysicsTokens->mjcOptionO_margin, SdfValueTypeNames->Double, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetOSolRefAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsO_solref); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionO_solref); } UsdAttribute MjcPhysicsSceneAPI::CreateOSolRefAttr(VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsO_solref, SdfValueTypeNames->DoubleArray, + MjcPhysicsTokens->mjcOptionO_solref, SdfValueTypeNames->DoubleArray, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetOSolImpAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsO_solimp); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionO_solimp); } UsdAttribute MjcPhysicsSceneAPI::CreateOSolImpAttr(VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsO_solimp, SdfValueTypeNames->DoubleArray, + MjcPhysicsTokens->mjcOptionO_solimp, SdfValueTypeNames->DoubleArray, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetOFrictionAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsO_friction); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionO_friction); } UsdAttribute MjcPhysicsSceneAPI::CreateOFrictionAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsO_friction, SdfValueTypeNames->DoubleArray, + MjcPhysicsTokens->mjcOptionO_friction, SdfValueTypeNames->DoubleArray, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetIntegratorAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsIntegrator); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionIntegrator); } UsdAttribute MjcPhysicsSceneAPI::CreateIntegratorAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsIntegrator, SdfValueTypeNames->Token, + MjcPhysicsTokens->mjcOptionIntegrator, SdfValueTypeNames->Token, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetConeAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsCone); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionCone); } UsdAttribute MjcPhysicsSceneAPI::CreateConeAttr(VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsCone, SdfValueTypeNames->Token, + MjcPhysicsTokens->mjcOptionCone, SdfValueTypeNames->Token, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetJacobianAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsJacobian); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionJacobian); } UsdAttribute MjcPhysicsSceneAPI::CreateJacobianAttr(VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsJacobian, SdfValueTypeNames->Token, + MjcPhysicsTokens->mjcOptionJacobian, SdfValueTypeNames->Token, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetSolverAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsSolver); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionSolver); } UsdAttribute MjcPhysicsSceneAPI::CreateSolverAttr(VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsSolver, SdfValueTypeNames->Token, + MjcPhysicsTokens->mjcOptionSolver, SdfValueTypeNames->Token, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetIterationsAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsIterations); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionIterations); } UsdAttribute MjcPhysicsSceneAPI::CreateIterationsAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsIterations, SdfValueTypeNames->Int, + MjcPhysicsTokens->mjcOptionIterations, SdfValueTypeNames->Int, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetToleranceAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsTolerance); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionTolerance); } UsdAttribute MjcPhysicsSceneAPI::CreateToleranceAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsTolerance, SdfValueTypeNames->Double, + MjcPhysicsTokens->mjcOptionTolerance, SdfValueTypeNames->Double, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetLSIterationsAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsLs_iterations); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionLs_iterations); } UsdAttribute MjcPhysicsSceneAPI::CreateLSIterationsAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsLs_iterations, SdfValueTypeNames->Int, + MjcPhysicsTokens->mjcOptionLs_iterations, SdfValueTypeNames->Int, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetLSToleranceAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsLs_tolerance); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionLs_tolerance); } UsdAttribute MjcPhysicsSceneAPI::CreateLSToleranceAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsLs_tolerance, SdfValueTypeNames->Double, + MjcPhysicsTokens->mjcOptionLs_tolerance, SdfValueTypeNames->Double, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetNoslipIterationsAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsNoslip_iterations); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionNoslip_iterations); } UsdAttribute MjcPhysicsSceneAPI::CreateNoslipIterationsAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsNoslip_iterations, SdfValueTypeNames->Int, + MjcPhysicsTokens->mjcOptionNoslip_iterations, SdfValueTypeNames->Int, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetNoslipToleranceAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsNoslip_tolerance); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionNoslip_tolerance); } UsdAttribute MjcPhysicsSceneAPI::CreateNoslipToleranceAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsNoslip_tolerance, SdfValueTypeNames->Double, + MjcPhysicsTokens->mjcOptionNoslip_tolerance, SdfValueTypeNames->Double, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetCCDIterationsAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsCcd_iterations); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionCcd_iterations); } UsdAttribute MjcPhysicsSceneAPI::CreateCCDIterationsAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsCcd_iterations, SdfValueTypeNames->Int, + MjcPhysicsTokens->mjcOptionCcd_iterations, SdfValueTypeNames->Int, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetCCDToleranceAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsCcd_tolerance); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionCcd_tolerance); } UsdAttribute MjcPhysicsSceneAPI::CreateCCDToleranceAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsCcd_tolerance, SdfValueTypeNames->Double, + MjcPhysicsTokens->mjcOptionCcd_tolerance, SdfValueTypeNames->Double, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetSDFIterationsAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsSdf_iterations); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionSdf_iterations); } UsdAttribute MjcPhysicsSceneAPI::CreateSDFIterationsAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsSdf_iterations, SdfValueTypeNames->Int, + MjcPhysicsTokens->mjcOptionSdf_iterations, SdfValueTypeNames->Int, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetSDFInitPointsAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsSdf_initpoints); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcOptionSdf_initpoints); } UsdAttribute MjcPhysicsSceneAPI::CreateSDFInitPointsAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsSdf_initpoints, SdfValueTypeNames->Int, + MjcPhysicsTokens->mjcOptionSdf_initpoints, SdfValueTypeNames->Int, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetActuatorGroupDisableAttr() const { return GetPrim().GetAttribute( - MjcPhysicsTokens->mjcPhysicsActuatorgroupdisable); + MjcPhysicsTokens->mjcOptionActuatorgroupdisable); } UsdAttribute MjcPhysicsSceneAPI::CreateActuatorGroupDisableAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsActuatorgroupdisable, + MjcPhysicsTokens->mjcOptionActuatorgroupdisable, SdfValueTypeNames->IntArray, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetConstraintFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagConstraint); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagConstraint); } UsdAttribute MjcPhysicsSceneAPI::CreateConstraintFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagConstraint, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagConstraint, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetEqualityFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagEquality); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagEquality); } UsdAttribute MjcPhysicsSceneAPI::CreateEqualityFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagEquality, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagEquality, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetFrictionLossFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagFrictionloss); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagFrictionloss); } UsdAttribute MjcPhysicsSceneAPI::CreateFrictionLossFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagFrictionloss, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagFrictionloss, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetLimitFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagLimit); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagLimit); } UsdAttribute MjcPhysicsSceneAPI::CreateLimitFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagLimit, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagLimit, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetContactFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagContact); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagContact); } UsdAttribute MjcPhysicsSceneAPI::CreateContactFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagContact, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagContact, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetPassiveFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagPassive); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagPassive); } UsdAttribute MjcPhysicsSceneAPI::CreatePassiveFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagPassive, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagPassive, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetGravityFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagGravity); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagGravity); } UsdAttribute MjcPhysicsSceneAPI::CreateGravityFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagGravity, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagGravity, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetClampCtrlFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagClampctrl); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagClampctrl); } UsdAttribute MjcPhysicsSceneAPI::CreateClampCtrlFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagClampctrl, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagClampctrl, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetWarmStartFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagWarmstart); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagWarmstart); } UsdAttribute MjcPhysicsSceneAPI::CreateWarmStartFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagWarmstart, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagWarmstart, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetFilterParentFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagFilterparent); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagFilterparent); } UsdAttribute MjcPhysicsSceneAPI::CreateFilterParentFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagFilterparent, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagFilterparent, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetActuationFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagActuation); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagActuation); } UsdAttribute MjcPhysicsSceneAPI::CreateActuationFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagActuation, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagActuation, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetRefSafeFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagRefsafe); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagRefsafe); } UsdAttribute MjcPhysicsSceneAPI::CreateRefSafeFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagRefsafe, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagRefsafe, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetSensorFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagSensor); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagSensor); } UsdAttribute MjcPhysicsSceneAPI::CreateSensorFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagSensor, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagSensor, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetMidPhaseFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagMidphase); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagMidphase); } UsdAttribute MjcPhysicsSceneAPI::CreateMidPhaseFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagMidphase, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagMidphase, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetNativeCCDFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagNativeccd); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagNativeccd); } UsdAttribute MjcPhysicsSceneAPI::CreateNativeCCDFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagNativeccd, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagNativeccd, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetEulerDampFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagEulerdamp); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagEulerdamp); } UsdAttribute MjcPhysicsSceneAPI::CreateEulerDampFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagEulerdamp, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagEulerdamp, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetAutoResetFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagAutoreset); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagAutoreset); } UsdAttribute MjcPhysicsSceneAPI::CreateAutoResetFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagAutoreset, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagAutoreset, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetOverrideFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagOverride); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagOverride); } UsdAttribute MjcPhysicsSceneAPI::CreateOverrideFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagOverride, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagOverride, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetEnergyFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagEnergy); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagEnergy); } UsdAttribute MjcPhysicsSceneAPI::CreateEnergyFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagEnergy, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagEnergy, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetFwdinvFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagFwdinv); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagFwdinv); } UsdAttribute MjcPhysicsSceneAPI::CreateFwdinvFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagFwdinv, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagFwdinv, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetInvDiscreteFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagInvdiscrete); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagInvdiscrete); } UsdAttribute MjcPhysicsSceneAPI::CreateInvDiscreteFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagInvdiscrete, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagInvdiscrete, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetMultiCCDFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagMulticcd); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagMulticcd); } UsdAttribute MjcPhysicsSceneAPI::CreateMultiCCDFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagMulticcd, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagMulticcd, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } UsdAttribute MjcPhysicsSceneAPI::GetIslandFlagAttr() const { - return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPhysicsFlagIsland); + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcFlagIsland); } UsdAttribute MjcPhysicsSceneAPI::CreateIslandFlagAttr( VtValue const &defaultValue, bool writeSparsely) const { return UsdSchemaBase::_CreateAttr( - MjcPhysicsTokens->mjcPhysicsFlagIsland, SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcFlagIsland, SdfValueTypeNames->Bool, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } @@ -630,55 +630,55 @@ static inline TfTokenVector _ConcatenateAttributeNames( const TfTokenVector &MjcPhysicsSceneAPI::GetSchemaAttributeNames( bool includeInherited) { static TfTokenVector localNames = { - MjcPhysicsTokens->mjcPhysicsTimestep, - MjcPhysicsTokens->mjcPhysicsApirate, - MjcPhysicsTokens->mjcPhysicsImpratio, - MjcPhysicsTokens->mjcPhysicsWind, - MjcPhysicsTokens->mjcPhysicsMagnetic, - MjcPhysicsTokens->mjcPhysicsDensity, - MjcPhysicsTokens->mjcPhysicsViscosity, - MjcPhysicsTokens->mjcPhysicsO_margin, - MjcPhysicsTokens->mjcPhysicsO_solref, - MjcPhysicsTokens->mjcPhysicsO_solimp, - MjcPhysicsTokens->mjcPhysicsO_friction, - MjcPhysicsTokens->mjcPhysicsIntegrator, - MjcPhysicsTokens->mjcPhysicsCone, - MjcPhysicsTokens->mjcPhysicsJacobian, - MjcPhysicsTokens->mjcPhysicsSolver, - MjcPhysicsTokens->mjcPhysicsIterations, - MjcPhysicsTokens->mjcPhysicsTolerance, - MjcPhysicsTokens->mjcPhysicsLs_iterations, - MjcPhysicsTokens->mjcPhysicsLs_tolerance, - MjcPhysicsTokens->mjcPhysicsNoslip_iterations, - MjcPhysicsTokens->mjcPhysicsNoslip_tolerance, - MjcPhysicsTokens->mjcPhysicsCcd_iterations, - MjcPhysicsTokens->mjcPhysicsCcd_tolerance, - MjcPhysicsTokens->mjcPhysicsSdf_iterations, - MjcPhysicsTokens->mjcPhysicsSdf_initpoints, - MjcPhysicsTokens->mjcPhysicsActuatorgroupdisable, - MjcPhysicsTokens->mjcPhysicsFlagConstraint, - MjcPhysicsTokens->mjcPhysicsFlagEquality, - MjcPhysicsTokens->mjcPhysicsFlagFrictionloss, - MjcPhysicsTokens->mjcPhysicsFlagLimit, - MjcPhysicsTokens->mjcPhysicsFlagContact, - MjcPhysicsTokens->mjcPhysicsFlagPassive, - MjcPhysicsTokens->mjcPhysicsFlagGravity, - MjcPhysicsTokens->mjcPhysicsFlagClampctrl, - MjcPhysicsTokens->mjcPhysicsFlagWarmstart, - MjcPhysicsTokens->mjcPhysicsFlagFilterparent, - MjcPhysicsTokens->mjcPhysicsFlagActuation, - MjcPhysicsTokens->mjcPhysicsFlagRefsafe, - MjcPhysicsTokens->mjcPhysicsFlagSensor, - MjcPhysicsTokens->mjcPhysicsFlagMidphase, - MjcPhysicsTokens->mjcPhysicsFlagNativeccd, - MjcPhysicsTokens->mjcPhysicsFlagEulerdamp, - MjcPhysicsTokens->mjcPhysicsFlagAutoreset, - MjcPhysicsTokens->mjcPhysicsFlagOverride, - MjcPhysicsTokens->mjcPhysicsFlagEnergy, - MjcPhysicsTokens->mjcPhysicsFlagFwdinv, - MjcPhysicsTokens->mjcPhysicsFlagInvdiscrete, - MjcPhysicsTokens->mjcPhysicsFlagMulticcd, - MjcPhysicsTokens->mjcPhysicsFlagIsland, + MjcPhysicsTokens->mjcOptionTimestep, + MjcPhysicsTokens->mjcOptionApirate, + MjcPhysicsTokens->mjcOptionImpratio, + MjcPhysicsTokens->mjcOptionWind, + MjcPhysicsTokens->mjcOptionMagnetic, + MjcPhysicsTokens->mjcOptionDensity, + MjcPhysicsTokens->mjcOptionViscosity, + MjcPhysicsTokens->mjcOptionO_margin, + MjcPhysicsTokens->mjcOptionO_solref, + MjcPhysicsTokens->mjcOptionO_solimp, + MjcPhysicsTokens->mjcOptionO_friction, + MjcPhysicsTokens->mjcOptionIntegrator, + MjcPhysicsTokens->mjcOptionCone, + MjcPhysicsTokens->mjcOptionJacobian, + MjcPhysicsTokens->mjcOptionSolver, + MjcPhysicsTokens->mjcOptionIterations, + MjcPhysicsTokens->mjcOptionTolerance, + MjcPhysicsTokens->mjcOptionLs_iterations, + MjcPhysicsTokens->mjcOptionLs_tolerance, + MjcPhysicsTokens->mjcOptionNoslip_iterations, + MjcPhysicsTokens->mjcOptionNoslip_tolerance, + MjcPhysicsTokens->mjcOptionCcd_iterations, + MjcPhysicsTokens->mjcOptionCcd_tolerance, + MjcPhysicsTokens->mjcOptionSdf_iterations, + MjcPhysicsTokens->mjcOptionSdf_initpoints, + MjcPhysicsTokens->mjcOptionActuatorgroupdisable, + MjcPhysicsTokens->mjcFlagConstraint, + MjcPhysicsTokens->mjcFlagEquality, + MjcPhysicsTokens->mjcFlagFrictionloss, + MjcPhysicsTokens->mjcFlagLimit, + MjcPhysicsTokens->mjcFlagContact, + MjcPhysicsTokens->mjcFlagPassive, + MjcPhysicsTokens->mjcFlagGravity, + MjcPhysicsTokens->mjcFlagClampctrl, + MjcPhysicsTokens->mjcFlagWarmstart, + MjcPhysicsTokens->mjcFlagFilterparent, + MjcPhysicsTokens->mjcFlagActuation, + MjcPhysicsTokens->mjcFlagRefsafe, + MjcPhysicsTokens->mjcFlagSensor, + MjcPhysicsTokens->mjcFlagMidphase, + MjcPhysicsTokens->mjcFlagNativeccd, + MjcPhysicsTokens->mjcFlagEulerdamp, + MjcPhysicsTokens->mjcFlagAutoreset, + MjcPhysicsTokens->mjcFlagOverride, + MjcPhysicsTokens->mjcFlagEnergy, + MjcPhysicsTokens->mjcFlagFwdinv, + MjcPhysicsTokens->mjcFlagInvdiscrete, + MjcPhysicsTokens->mjcFlagMulticcd, + MjcPhysicsTokens->mjcFlagIsland, }; static TfTokenVector allNames = _ConcatenateAttributeNames( UsdAPISchemaBase::GetSchemaAttributeNames(true), localNames); diff --git a/src/experimental/usd/mjcPhysics/sceneAPI.h b/src/experimental/usd/mjcPhysics/sceneAPI.h index 4b77202a..2ac745c7 100644 --- a/src/experimental/usd/mjcPhysics/sceneAPI.h +++ b/src/experimental/usd/mjcPhysics/sceneAPI.h @@ -154,7 +154,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform double mjc:physics:timestep = 0.002` | + /// | Declaration | `uniform double mjc:option:timestep = 0.002` | /// | C++ Type | double | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -179,7 +179,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform double mjc:physics:apirate = 100` | + /// | Declaration | `uniform double mjc:option:apirate = 100` | /// | C++ Type | double | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -204,7 +204,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform double mjc:physics:impratio = 1` | + /// | Declaration | `uniform double mjc:option:impratio = 1` | /// | C++ Type | double | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -228,7 +228,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform double3 mjc:physics:wind = (0, 0, 0)` | + /// | Declaration | `uniform double3 mjc:option:wind = (0, 0, 0)` | /// | C++ Type | GfVec3d | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double3 | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -252,7 +252,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform double3 mjc:physics:magnetic = (0, -0.5, 0)` | + /// | Declaration | `uniform double3 mjc:option:magnetic = (0, -0.5, 0)` | /// | C++ Type | GfVec3d | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double3 | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -276,7 +276,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform double mjc:physics:density = 0` | + /// | Declaration | `uniform double mjc:option:density = 0` | /// | C++ Type | double | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -300,7 +300,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform double mjc:physics:viscosity = 0` | + /// | Declaration | `uniform double mjc:option:viscosity = 0` | /// | C++ Type | double | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -325,7 +325,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform double mjc:physics:o_margin = 0` | + /// | Declaration | `uniform double mjc:option:o_margin = 0` | /// | C++ Type | double | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -350,7 +350,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform double[] mjc:physics:o_solref = [0.02, 1]` | + /// | Declaration | `uniform double[] mjc:option:o_solref = [0.02, 1]` | /// | C++ Type | VtArray | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->DoubleArray | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -375,10 +375,10 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform double[] mjc:physics:o_solimp = [0.9, 0.95, - /// 0.001, 0.5, 2]` | | C++ Type | VtArray | | \ref Usd_Datatypes "Usd - /// Type" | SdfValueTypeNames->DoubleArray | | \ref SdfVariability - /// "Variability" | SdfVariabilityUniform | + /// | Declaration | `uniform double[] mjc:option:o_solimp = [0.9, 0.95, 0.001, + /// 0.5, 2]` | | C++ Type | VtArray | | \ref Usd_Datatypes "Usd Type" + /// | SdfValueTypeNames->DoubleArray | | \ref SdfVariability "Variability" | + /// SdfVariabilityUniform | MJCPHYSICS_API UsdAttribute GetOSolImpAttr() const; @@ -400,7 +400,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform double[] mjc:physics:o_friction = [1, 1, 0.005, + /// | Declaration | `uniform double[] mjc:option:o_friction = [1, 1, 0.005, /// 0.0001, 0.0001]` | | C++ Type | VtArray | | \ref Usd_Datatypes /// "Usd Type" | SdfValueTypeNames->DoubleArray | | \ref SdfVariability /// "Variability" | SdfVariabilityUniform | @@ -424,7 +424,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform token mjc:physics:integrator = "euler"` | + /// | Declaration | `uniform token mjc:option:integrator = "euler"` | /// | C++ Type | TfToken | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Token | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -450,7 +450,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform token mjc:physics:cone = "pyramidal"` | + /// | Declaration | `uniform token mjc:option:cone = "pyramidal"` | /// | C++ Type | TfToken | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Token | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -475,7 +475,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform token mjc:physics:jacobian = "auto"` | + /// | Declaration | `uniform token mjc:option:jacobian = "auto"` | /// | C++ Type | TfToken | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Token | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -500,7 +500,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform token mjc:physics:solver = "newton"` | + /// | Declaration | `uniform token mjc:option:solver = "newton"` | /// | C++ Type | TfToken | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Token | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -525,7 +525,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform int mjc:physics:iterations = 100` | + /// | Declaration | `uniform int mjc:option:iterations = 100` | /// | C++ Type | int | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Int | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -550,7 +550,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform double mjc:physics:tolerance = 1e-8` | + /// | Declaration | `uniform double mjc:option:tolerance = 1e-8` | /// | C++ Type | double | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -575,7 +575,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform int mjc:physics:ls_iterations = 50` | + /// | Declaration | `uniform int mjc:option:ls_iterations = 50` | /// | C++ Type | int | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Int | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -600,7 +600,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform double mjc:physics:ls_tolerance = 0.01` | + /// | Declaration | `uniform double mjc:option:ls_tolerance = 0.01` | /// | C++ Type | double | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -624,7 +624,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform int mjc:physics:noslip_iterations = 0` | + /// | Declaration | `uniform int mjc:option:noslip_iterations = 0` | /// | C++ Type | int | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Int | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -649,7 +649,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform double mjc:physics:noslip_tolerance = 0.000001` | + /// | Declaration | `uniform double mjc:option:noslip_tolerance = 0.000001` | /// | C++ Type | double | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -674,7 +674,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform int mjc:physics:ccd_iterations = 50` | + /// | Declaration | `uniform int mjc:option:ccd_iterations = 50` | /// | C++ Type | int | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Int | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -699,7 +699,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform double mjc:physics:ccd_tolerance = 0.000001` | + /// | Declaration | `uniform double mjc:option:ccd_tolerance = 0.000001` | /// | C++ Type | double | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -724,7 +724,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform int mjc:physics:sdf_iterations = 10` | + /// | Declaration | `uniform int mjc:option:sdf_iterations = 10` | /// | C++ Type | int | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Int | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -749,7 +749,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform int mjc:physics:sdf_initpoints = 40` | + /// | Declaration | `uniform int mjc:option:sdf_initpoints = 40` | /// | C++ Type | int | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Int | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -773,7 +773,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform int[] mjc:physics:actuatorgroupdisable` | + /// | Declaration | `uniform int[] mjc:option:actuatorgroupdisable` | /// | C++ Type | VtArray | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->IntArray | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -798,7 +798,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:constraint = 1` | + /// | Declaration | `uniform bool mjc:flag:constraint = 1` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -822,7 +822,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:equality = 1` | + /// | Declaration | `uniform bool mjc:flag:equality = 1` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -846,7 +846,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:frictionloss = 1` | + /// | Declaration | `uniform bool mjc:flag:frictionloss = 1` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -872,7 +872,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:limit = 1` | + /// | Declaration | `uniform bool mjc:flag:limit = 1` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -897,7 +897,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:contact = 1` | + /// | Declaration | `uniform bool mjc:flag:contact = 1` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -922,7 +922,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:passive = 1` | + /// | Declaration | `uniform bool mjc:flag:passive = 1` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -947,7 +947,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:gravity = 1` | + /// | Declaration | `uniform bool mjc:flag:gravity = 1` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -972,7 +972,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:clampctrl = 1` | + /// | Declaration | `uniform bool mjc:flag:clampctrl = 1` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -997,7 +997,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:warmstart = 1` | + /// | Declaration | `uniform bool mjc:flag:warmstart = 1` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -1022,7 +1022,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:filterparent = 1` | + /// | Declaration | `uniform bool mjc:flag:filterparent = 1` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -1048,7 +1048,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:actuation = 1` | + /// | Declaration | `uniform bool mjc:flag:actuation = 1` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -1073,7 +1073,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:refsafe = 1` | + /// | Declaration | `uniform bool mjc:flag:refsafe = 1` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -1097,7 +1097,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:sensor = 1` | + /// | Declaration | `uniform bool mjc:flag:sensor = 1` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -1122,7 +1122,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:midphase = 1` | + /// | Declaration | `uniform bool mjc:flag:midphase = 1` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -1147,7 +1147,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:nativeccd = 1` | + /// | Declaration | `uniform bool mjc:flag:nativeccd = 1` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -1172,7 +1172,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:eulerdamp = 1` | + /// | Declaration | `uniform bool mjc:flag:eulerdamp = 1` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -1197,7 +1197,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:autoreset = 1` | + /// | Declaration | `uniform bool mjc:flag:autoreset = 1` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -1221,7 +1221,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:override = 0` | + /// | Declaration | `uniform bool mjc:flag:override = 0` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -1246,7 +1246,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:energy = 0` | + /// | Declaration | `uniform bool mjc:flag:energy = 0` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -1270,7 +1270,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:fwdinv = 0` | + /// | Declaration | `uniform bool mjc:flag:fwdinv = 0` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -1295,7 +1295,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:invdiscrete = 0` | + /// | Declaration | `uniform bool mjc:flag:invdiscrete = 0` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -1321,7 +1321,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:multiccd = 0` | + /// | Declaration | `uniform bool mjc:flag:multiccd = 0` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | @@ -1345,7 +1345,7 @@ class MjcPhysicsSceneAPI : public UsdAPISchemaBase { /// /// | || /// | -- | -- | - /// | Declaration | `uniform bool mjc:physics:flag:island = 0` | + /// | Declaration | `uniform bool mjc:flag:island = 0` | /// | C++ Type | bool | /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | diff --git a/src/experimental/usd/mjcPhysics/schema.usda b/src/experimental/usd/mjcPhysics/schema.usda index 3d416fc2..322a90a4 100644 --- a/src/experimental/usd/mjcPhysics/schema.usda +++ b/src/experimental/usd/mjcPhysics/schema.usda @@ -99,7 +99,7 @@ class "SceneAPI" inherits = ) { - uniform double mjc:physics:timestep = 0.002 ( + uniform double mjc:option:timestep = 0.002 ( customData = { string apiName = "Timestep" } @@ -107,7 +107,7 @@ class "SceneAPI" doc = """Controls the timestep in seconds used by MuJoCo.""" ) - uniform double mjc:physics:apirate = 100 ( + uniform double mjc:option:apirate = 100 ( customData = { string apiName = "ApiRate" } @@ -116,7 +116,7 @@ class "SceneAPI" the update function to be executed.""" ) - uniform double mjc:physics:impratio = 1.0 ( + uniform double mjc:option:impratio = 1.0 ( customData = { string apiName = "ImpRatio" } @@ -125,7 +125,7 @@ class "SceneAPI" friction cones.""" ) - uniform double3 mjc:physics:wind = (0.0, 0.0, 0.0) ( + uniform double3 mjc:option:wind = (0.0, 0.0, 0.0) ( customData = { string apiName = "Wind" } @@ -133,7 +133,7 @@ class "SceneAPI" doc = """Velocity vector of medium (i.e. wind).""" ) - uniform double3 mjc:physics:magnetic = (0.0, -0.5, 0.0) ( + uniform double3 mjc:option:magnetic = (0.0, -0.5, 0.0) ( customData = { string apiName = "Magnetic" } @@ -141,7 +141,7 @@ class "SceneAPI" doc = """Global magnetic flux.""" ) - uniform double mjc:physics:density = 0.0 ( + uniform double mjc:option:density = 0.0 ( customData = { string apiName = "Density" } @@ -149,7 +149,7 @@ class "SceneAPI" doc = """Density of medium.""" ) - uniform double mjc:physics:viscosity = 0.0 ( + uniform double mjc:option:viscosity = 0.0 ( customData = { string apiName = "Viscosity" } @@ -157,7 +157,7 @@ class "SceneAPI" doc = """Viscosity of medium.""" ) - uniform double mjc:physics:o_margin = 0.0 ( + uniform double mjc:option:o_margin = 0.0 ( customData = { string apiName = "OMargin" } @@ -166,7 +166,7 @@ class "SceneAPI" Contact override is enabled.""" ) - uniform double[] mjc:physics:o_solref = [0.02, 1.0] ( + uniform double[] mjc:option:o_solref = [0.02, 1.0] ( customData = { string apiName = "OSolRef" } @@ -175,7 +175,7 @@ class "SceneAPI" Contact override is enabled.""" ) - uniform double[] mjc:physics:o_solimp = [0.9, 0.95, 0.001, 0.5, 2.0] ( + uniform double[] mjc:option:o_solimp = [0.9, 0.95, 0.001, 0.5, 2.0] ( customData = { string apiName = "OSolImp" } @@ -184,7 +184,7 @@ class "SceneAPI" Contact override is enabled.""" ) - uniform double[] mjc:physics:o_friction = [1.0, 1.0, 0.005, 0.0001, 0.0001] ( + uniform double[] mjc:option:o_friction = [1.0, 1.0, 0.005, 0.0001, 0.0001] ( customData = { string apiName = "OFriction" } @@ -193,7 +193,7 @@ class "SceneAPI" Contact override is enabled.""" ) - uniform token mjc:physics:integrator = "euler" ( + uniform token mjc:option:integrator = "euler" ( allowedTokens = ["euler", "rk4", "implicit", "implicitfast"] customData = { string apiName = "Integrator" @@ -202,7 +202,7 @@ class "SceneAPI" doc = """Numerical integrator to be used.""" ) - uniform token mjc:physics:cone = "pyramidal" ( + uniform token mjc:option:cone = "pyramidal" ( allowedTokens = ["pyramidal", "elliptic"] customData = { string apiName = "Cone" @@ -211,7 +211,7 @@ class "SceneAPI" doc = """The type of contact friction cone.""" ) - uniform token mjc:physics:jacobian = "auto" ( + uniform token mjc:option:jacobian = "auto" ( allowedTokens = ["auto", "dense", "sparse"] customData = { string apiName = "Jacobian" @@ -220,7 +220,7 @@ class "SceneAPI" doc = """The type of constraint Jacobian and matrices computed from it.""" ) - uniform token mjc:physics:solver = "newton" ( + uniform token mjc:option:solver = "newton" ( allowedTokens = ["pgs", "cg", "newton"] customData = { string apiName = "Solver" @@ -229,7 +229,7 @@ class "SceneAPI" doc = """Constraint solver algorithm to be used.""" ) - uniform int mjc:physics:iterations = 100 ( + uniform int mjc:option:iterations = 100 ( customData = { string apiName = "Iterations" } @@ -237,7 +237,7 @@ class "SceneAPI" doc = """Maximum number of iterations of the constraint solver.""" ) - uniform double mjc:physics:tolerance = 1e-08 ( + uniform double mjc:option:tolerance = 1e-08 ( customData = { string apiName = "Tolerance" } @@ -246,7 +246,7 @@ class "SceneAPI" solver.""" ) - uniform int mjc:physics:ls_iterations = 50 ( + uniform int mjc:option:ls_iterations = 50 ( customData = { string apiName = "LSIterations" } @@ -255,7 +255,7 @@ class "SceneAPI" constraint solvers.""" ) - uniform double mjc:physics:ls_tolerance = 0.01 ( + uniform double mjc:option:ls_tolerance = 0.01 ( customData = { string apiName = "LSTolerance" } @@ -263,7 +263,7 @@ class "SceneAPI" doc = """Tolerance threshold used for early termination of the linesearch algorithm.""" ) - uniform int mjc:physics:noslip_iterations = 0 ( + uniform int mjc:option:noslip_iterations = 0 ( customData = { string apiName = "NoslipIterations" } @@ -271,7 +271,7 @@ class "SceneAPI" doc = """Maximum number of iterations of the Noslip solver.""" ) - uniform double mjc:physics:noslip_tolerance = 1e-06 ( + uniform double mjc:option:noslip_tolerance = 1e-06 ( customData = { string apiName = "NoslipTolerance" } @@ -279,7 +279,7 @@ class "SceneAPI" doc = """Tolerance threshold used for early termination of the Noslip solver.""" ) - uniform int mjc:physics:ccd_iterations = 50 ( + uniform int mjc:option:ccd_iterations = 50 ( customData = { string apiName = "CCDIterations" } @@ -287,7 +287,7 @@ class "SceneAPI" doc = """Maximum number of iterations of the algorithm used for convex collisions.""" ) - uniform double mjc:physics:ccd_tolerance = 1e-06 ( + uniform double mjc:option:ccd_tolerance = 1e-06 ( customData = { string apiName = "CCDTolerance" } @@ -296,7 +296,7 @@ class "SceneAPI" collision algorithm.""" ) - uniform int mjc:physics:sdf_iterations = 10 ( + uniform int mjc:option:sdf_iterations = 10 ( customData = { string apiName = "SDFIterations" } @@ -305,7 +305,7 @@ class "SceneAPI" (per initial point).""" ) - uniform int mjc:physics:sdf_initpoints = 40 ( + uniform int mjc:option:sdf_initpoints = 40 ( customData = { string apiName = "SDFInitPoints" } @@ -314,7 +314,7 @@ class "SceneAPI" Distance Field collisions.""" ) - uniform int[] mjc:physics:actuatorgroupdisable ( + uniform int[] mjc:option:actuatorgroupdisable ( customData = { string apiName = "ActuatorGroupDisable" } @@ -322,7 +322,7 @@ class "SceneAPI" doc = """List of actuator groups to disable.""" ) - uniform bool mjc:physics:flag:constraint = True ( + uniform bool mjc:flag:constraint = True ( customData = { string apiName = "ConstraintFlag" } @@ -330,7 +330,7 @@ class "SceneAPI" doc = """Enables constraint solver.""" ) - uniform bool mjc:physics:flag:equality = True ( + uniform bool mjc:flag:equality = True ( customData = { string apiName = "EqualityFlag" } @@ -338,7 +338,7 @@ class "SceneAPI" doc = """Enables all standard computations related to equality constraints.""" ) - uniform bool mjc:physics:flag:frictionloss = True ( + uniform bool mjc:flag:frictionloss = True ( customData = { string apiName = "FrictionLossFlag" } @@ -346,7 +346,7 @@ class "SceneAPI" doc = """Enables all standard computations related to friction loss constraints.""" ) - uniform bool mjc:physics:flag:limit = True ( + uniform bool mjc:flag:limit = True ( customData = { string apiName = "LimitFlag" } @@ -354,7 +354,7 @@ class "SceneAPI" doc = """Enables all standard computations related to joint and tendon limit constraints.""" ) - uniform bool mjc:physics:flag:contact = True ( + uniform bool mjc:flag:contact = True ( customData = { string apiName = "ContactFlag" } @@ -362,7 +362,7 @@ class "SceneAPI" doc = """Enables collision detection and all standard computations related to contact constraints.""" ) - uniform bool mjc:physics:flag:passive = True ( + uniform bool mjc:flag:passive = True ( customData = { string apiName = "PassiveFlag" } @@ -370,7 +370,7 @@ class "SceneAPI" doc = """Enables the simulation of joint and tendon spring-dampers, fluid dynamics forces, and custom passive forces.""" ) - uniform bool mjc:physics:flag:gravity = True ( + uniform bool mjc:flag:gravity = True ( customData = { string apiName = "GravityFlag" } @@ -378,7 +378,7 @@ class "SceneAPI" doc = """Enables the application of gravitational acceleration as defined in mjOption.""" ) - uniform bool mjc:physics:flag:clampctrl = True ( + uniform bool mjc:flag:clampctrl = True ( customData = { string apiName = "ClampCtrlFlag" } @@ -386,7 +386,7 @@ class "SceneAPI" doc = """Enables the clamping of control inputs to all actuators, according to actuator-specific attributes.""" ) - uniform bool mjc:physics:flag:warmstart = True ( + uniform bool mjc:flag:warmstart = True ( customData = { string apiName = "WarmStartFlag" } @@ -394,7 +394,7 @@ class "SceneAPI" doc = """Enables warm-starting of the constraint solver, using the solution from the previous time step to initialize the iterative optimization.""" ) - uniform bool mjc:physics:flag:filterparent = True ( + uniform bool mjc:flag:filterparent = True ( customData = { string apiName = "FilterParentFlag" } @@ -402,7 +402,7 @@ class "SceneAPI" doc = """Enables the filtering of contact pairs where the two geoms belong to a parent and child body.""" ) - uniform bool mjc:physics:flag:actuation = True ( + uniform bool mjc:flag:actuation = True ( customData = { string apiName = "ActuationFlag" } @@ -410,7 +410,7 @@ class "SceneAPI" doc = """Enables all standard computations related to actuator forces, including actuator dynamics.""" ) - uniform bool mjc:physics:flag:refsafe = True ( + uniform bool mjc:flag:refsafe = True ( customData = { string apiName = "RefSafeFlag" } @@ -418,7 +418,7 @@ class "SceneAPI" doc = """Enables a safety mechanism that prevents instabilities due to solref[0] being too small compared to the simulation timestep.""" ) - uniform bool mjc:physics:flag:sensor = True ( + uniform bool mjc:flag:sensor = True ( customData = { string apiName = "SensorFlag" } @@ -426,7 +426,7 @@ class "SceneAPI" doc = """Enables all computations related to sensors.""" ) - uniform bool mjc:physics:flag:midphase = True ( + uniform bool mjc:flag:midphase = True ( customData = { string apiName = "MidPhaseFlag" } @@ -434,7 +434,7 @@ class "SceneAPI" doc = """Enables mid-phase collision filtering using a static AABB bounding volume hierarchy (BVH).""" ) - uniform bool mjc:physics:flag:nativeccd = True ( + uniform bool mjc:flag:nativeccd = True ( customData = { string apiName = "NativeCCDFlag" } @@ -442,7 +442,7 @@ class "SceneAPI" doc = """Enables the native convex collision detection pipeline instead of using the libccd library.""" ) - uniform bool mjc:physics:flag:eulerdamp = True ( + uniform bool mjc:flag:eulerdamp = True ( customData = { string apiName = "EulerDampFlag" } @@ -450,7 +450,7 @@ class "SceneAPI" doc = """Enables implicit integration with respect to joint damping in the Euler integrator.""" ) - uniform bool mjc:physics:flag:autoreset = True ( + uniform bool mjc:flag:autoreset = True ( customData = { string apiName = "AutoResetFlag" } @@ -458,7 +458,7 @@ class "SceneAPI" doc = """Enables the automatic resetting of the simulation state when numerical issues are detected.""" ) - uniform bool mjc:physics:flag:override = False ( + uniform bool mjc:flag:override = False ( customData = { string apiName = "OverrideFlag" } @@ -466,7 +466,7 @@ class "SceneAPI" doc = """Enables the contact override mechanism.""" ) - uniform bool mjc:physics:flag:energy = False ( + uniform bool mjc:flag:energy = False ( customData = { string apiName = "EnergyFlag" } @@ -474,7 +474,7 @@ class "SceneAPI" doc = """Enables the computation of potential and kinetic energy (mjData.energy[0,1]).""" ) - uniform bool mjc:physics:flag:fwdinv = False ( + uniform bool mjc:flag:fwdinv = False ( customData = { string apiName = "FwdinvFlag" } @@ -482,7 +482,7 @@ class "SceneAPI" doc = """Enables the automatic comparison of forward and inverse dynamics.""" ) - uniform bool mjc:physics:flag:invdiscrete = False ( + uniform bool mjc:flag:invdiscrete = False ( customData = { string apiName = "InvDiscreteFlag" } @@ -490,7 +490,7 @@ class "SceneAPI" doc = """Enables discrete-time inverse dynamics with mj_inverse for integrators other than RK4.""" ) - uniform bool mjc:physics:flag:multiccd = False ( + uniform bool mjc:flag:multiccd = False ( customData = { string apiName = "MultiCCDFlag" } @@ -498,7 +498,7 @@ class "SceneAPI" doc = """Enables multiple-contact collision detection for geom pairs using a general-purpose convex-convex collider.""" ) - uniform bool mjc:physics:flag:island = False ( + uniform bool mjc:flag:island = False ( customData = { string apiName = "IslandFlag" } diff --git a/src/experimental/usd/mjcPhysics/tokens.cpp b/src/experimental/usd/mjcPhysics/tokens.cpp index 7b1868f8..2cc35a98 100644 --- a/src/experimental/usd/mjcPhysics/tokens.cpp +++ b/src/experimental/usd/mjcPhysics/tokens.cpp @@ -24,62 +24,58 @@ MjcPhysicsTokensType::MjcPhysicsTokensType() euler("euler", TfToken::Immortal), implicit("implicit", TfToken::Immortal), implicitfast("implicitfast", TfToken::Immortal), - mjcPhysicsActuatorgroupdisable("mjc:physics:actuatorgroupdisable", - TfToken::Immortal), - mjcPhysicsApirate("mjc:physics:apirate", TfToken::Immortal), - mjcPhysicsCcd_iterations("mjc:physics:ccd_iterations", TfToken::Immortal), - mjcPhysicsCcd_tolerance("mjc:physics:ccd_tolerance", TfToken::Immortal), - mjcPhysicsCone("mjc:physics:cone", TfToken::Immortal), - mjcPhysicsDensity("mjc:physics:density", TfToken::Immortal), - mjcPhysicsFlagActuation("mjc:physics:flag:actuation", TfToken::Immortal), - mjcPhysicsFlagAutoreset("mjc:physics:flag:autoreset", TfToken::Immortal), - mjcPhysicsFlagClampctrl("mjc:physics:flag:clampctrl", TfToken::Immortal), - mjcPhysicsFlagConstraint("mjc:physics:flag:constraint", - TfToken::Immortal), - mjcPhysicsFlagContact("mjc:physics:flag:contact", TfToken::Immortal), - mjcPhysicsFlagEnergy("mjc:physics:flag:energy", TfToken::Immortal), - mjcPhysicsFlagEquality("mjc:physics:flag:equality", TfToken::Immortal), - mjcPhysicsFlagEulerdamp("mjc:physics:flag:eulerdamp", TfToken::Immortal), - mjcPhysicsFlagFilterparent("mjc:physics:flag:filterparent", + mjcFlagActuation("mjc:flag:actuation", TfToken::Immortal), + mjcFlagAutoreset("mjc:flag:autoreset", TfToken::Immortal), + mjcFlagClampctrl("mjc:flag:clampctrl", TfToken::Immortal), + mjcFlagConstraint("mjc:flag:constraint", TfToken::Immortal), + mjcFlagContact("mjc:flag:contact", TfToken::Immortal), + mjcFlagEnergy("mjc:flag:energy", TfToken::Immortal), + mjcFlagEquality("mjc:flag:equality", TfToken::Immortal), + mjcFlagEulerdamp("mjc:flag:eulerdamp", TfToken::Immortal), + mjcFlagFilterparent("mjc:flag:filterparent", TfToken::Immortal), + mjcFlagFrictionloss("mjc:flag:frictionloss", TfToken::Immortal), + mjcFlagFwdinv("mjc:flag:fwdinv", TfToken::Immortal), + mjcFlagGravity("mjc:flag:gravity", TfToken::Immortal), + mjcFlagInvdiscrete("mjc:flag:invdiscrete", TfToken::Immortal), + mjcFlagIsland("mjc:flag:island", TfToken::Immortal), + mjcFlagLimit("mjc:flag:limit", TfToken::Immortal), + mjcFlagMidphase("mjc:flag:midphase", TfToken::Immortal), + mjcFlagMulticcd("mjc:flag:multiccd", TfToken::Immortal), + mjcFlagNativeccd("mjc:flag:nativeccd", TfToken::Immortal), + mjcFlagOverride("mjc:flag:override", TfToken::Immortal), + mjcFlagPassive("mjc:flag:passive", TfToken::Immortal), + mjcFlagRefsafe("mjc:flag:refsafe", TfToken::Immortal), + mjcFlagSensor("mjc:flag:sensor", TfToken::Immortal), + mjcFlagWarmstart("mjc:flag:warmstart", TfToken::Immortal), + mjcOptionActuatorgroupdisable("mjc:option:actuatorgroupdisable", + TfToken::Immortal), + mjcOptionApirate("mjc:option:apirate", TfToken::Immortal), + mjcOptionCcd_iterations("mjc:option:ccd_iterations", TfToken::Immortal), + mjcOptionCcd_tolerance("mjc:option:ccd_tolerance", TfToken::Immortal), + mjcOptionCone("mjc:option:cone", TfToken::Immortal), + mjcOptionDensity("mjc:option:density", TfToken::Immortal), + mjcOptionImpratio("mjc:option:impratio", TfToken::Immortal), + mjcOptionIntegrator("mjc:option:integrator", TfToken::Immortal), + mjcOptionIterations("mjc:option:iterations", TfToken::Immortal), + mjcOptionJacobian("mjc:option:jacobian", TfToken::Immortal), + mjcOptionLs_iterations("mjc:option:ls_iterations", TfToken::Immortal), + mjcOptionLs_tolerance("mjc:option:ls_tolerance", TfToken::Immortal), + mjcOptionMagnetic("mjc:option:magnetic", TfToken::Immortal), + mjcOptionNoslip_iterations("mjc:option:noslip_iterations", TfToken::Immortal), - mjcPhysicsFlagFrictionloss("mjc:physics:flag:frictionloss", - TfToken::Immortal), - mjcPhysicsFlagFwdinv("mjc:physics:flag:fwdinv", TfToken::Immortal), - mjcPhysicsFlagGravity("mjc:physics:flag:gravity", TfToken::Immortal), - mjcPhysicsFlagInvdiscrete("mjc:physics:flag:invdiscrete", + mjcOptionNoslip_tolerance("mjc:option:noslip_tolerance", TfToken::Immortal), - mjcPhysicsFlagIsland("mjc:physics:flag:island", TfToken::Immortal), - mjcPhysicsFlagLimit("mjc:physics:flag:limit", TfToken::Immortal), - mjcPhysicsFlagMidphase("mjc:physics:flag:midphase", TfToken::Immortal), - mjcPhysicsFlagMulticcd("mjc:physics:flag:multiccd", TfToken::Immortal), - mjcPhysicsFlagNativeccd("mjc:physics:flag:nativeccd", TfToken::Immortal), - mjcPhysicsFlagOverride("mjc:physics:flag:override", TfToken::Immortal), - mjcPhysicsFlagPassive("mjc:physics:flag:passive", TfToken::Immortal), - mjcPhysicsFlagRefsafe("mjc:physics:flag:refsafe", TfToken::Immortal), - mjcPhysicsFlagSensor("mjc:physics:flag:sensor", TfToken::Immortal), - mjcPhysicsFlagWarmstart("mjc:physics:flag:warmstart", TfToken::Immortal), - mjcPhysicsImpratio("mjc:physics:impratio", TfToken::Immortal), - mjcPhysicsIntegrator("mjc:physics:integrator", TfToken::Immortal), - mjcPhysicsIterations("mjc:physics:iterations", TfToken::Immortal), - mjcPhysicsJacobian("mjc:physics:jacobian", TfToken::Immortal), - mjcPhysicsLs_iterations("mjc:physics:ls_iterations", TfToken::Immortal), - mjcPhysicsLs_tolerance("mjc:physics:ls_tolerance", TfToken::Immortal), - mjcPhysicsMagnetic("mjc:physics:magnetic", TfToken::Immortal), - mjcPhysicsNoslip_iterations("mjc:physics:noslip_iterations", - TfToken::Immortal), - mjcPhysicsNoslip_tolerance("mjc:physics:noslip_tolerance", - TfToken::Immortal), - mjcPhysicsO_friction("mjc:physics:o_friction", TfToken::Immortal), - mjcPhysicsO_margin("mjc:physics:o_margin", TfToken::Immortal), - mjcPhysicsO_solimp("mjc:physics:o_solimp", TfToken::Immortal), - mjcPhysicsO_solref("mjc:physics:o_solref", TfToken::Immortal), - mjcPhysicsSdf_initpoints("mjc:physics:sdf_initpoints", TfToken::Immortal), - mjcPhysicsSdf_iterations("mjc:physics:sdf_iterations", TfToken::Immortal), - mjcPhysicsSolver("mjc:physics:solver", TfToken::Immortal), - mjcPhysicsTimestep("mjc:physics:timestep", TfToken::Immortal), - mjcPhysicsTolerance("mjc:physics:tolerance", TfToken::Immortal), - mjcPhysicsViscosity("mjc:physics:viscosity", TfToken::Immortal), - mjcPhysicsWind("mjc:physics:wind", TfToken::Immortal), + mjcOptionO_friction("mjc:option:o_friction", TfToken::Immortal), + mjcOptionO_margin("mjc:option:o_margin", TfToken::Immortal), + mjcOptionO_solimp("mjc:option:o_solimp", TfToken::Immortal), + mjcOptionO_solref("mjc:option:o_solref", TfToken::Immortal), + mjcOptionSdf_initpoints("mjc:option:sdf_initpoints", TfToken::Immortal), + mjcOptionSdf_iterations("mjc:option:sdf_iterations", TfToken::Immortal), + mjcOptionSolver("mjc:option:solver", TfToken::Immortal), + mjcOptionTimestep("mjc:option:timestep", TfToken::Immortal), + mjcOptionTolerance("mjc:option:tolerance", TfToken::Immortal), + mjcOptionViscosity("mjc:option:viscosity", TfToken::Immortal), + mjcOptionWind("mjc:option:wind", TfToken::Immortal), newton("newton", TfToken::Immortal), pgs("pgs", TfToken::Immortal), pyramidal("pyramidal", TfToken::Immortal), @@ -94,55 +90,55 @@ MjcPhysicsTokensType::MjcPhysicsTokensType() euler, implicit, implicitfast, - mjcPhysicsActuatorgroupdisable, - mjcPhysicsApirate, - mjcPhysicsCcd_iterations, - mjcPhysicsCcd_tolerance, - mjcPhysicsCone, - mjcPhysicsDensity, - mjcPhysicsFlagActuation, - mjcPhysicsFlagAutoreset, - mjcPhysicsFlagClampctrl, - mjcPhysicsFlagConstraint, - mjcPhysicsFlagContact, - mjcPhysicsFlagEnergy, - mjcPhysicsFlagEquality, - mjcPhysicsFlagEulerdamp, - mjcPhysicsFlagFilterparent, - mjcPhysicsFlagFrictionloss, - mjcPhysicsFlagFwdinv, - mjcPhysicsFlagGravity, - mjcPhysicsFlagInvdiscrete, - mjcPhysicsFlagIsland, - mjcPhysicsFlagLimit, - mjcPhysicsFlagMidphase, - mjcPhysicsFlagMulticcd, - mjcPhysicsFlagNativeccd, - mjcPhysicsFlagOverride, - mjcPhysicsFlagPassive, - mjcPhysicsFlagRefsafe, - mjcPhysicsFlagSensor, - mjcPhysicsFlagWarmstart, - mjcPhysicsImpratio, - mjcPhysicsIntegrator, - mjcPhysicsIterations, - mjcPhysicsJacobian, - mjcPhysicsLs_iterations, - mjcPhysicsLs_tolerance, - mjcPhysicsMagnetic, - mjcPhysicsNoslip_iterations, - mjcPhysicsNoslip_tolerance, - mjcPhysicsO_friction, - mjcPhysicsO_margin, - mjcPhysicsO_solimp, - mjcPhysicsO_solref, - mjcPhysicsSdf_initpoints, - mjcPhysicsSdf_iterations, - mjcPhysicsSolver, - mjcPhysicsTimestep, - mjcPhysicsTolerance, - mjcPhysicsViscosity, - mjcPhysicsWind, + mjcFlagActuation, + mjcFlagAutoreset, + mjcFlagClampctrl, + mjcFlagConstraint, + mjcFlagContact, + mjcFlagEnergy, + mjcFlagEquality, + mjcFlagEulerdamp, + mjcFlagFilterparent, + mjcFlagFrictionloss, + mjcFlagFwdinv, + mjcFlagGravity, + mjcFlagInvdiscrete, + mjcFlagIsland, + mjcFlagLimit, + mjcFlagMidphase, + mjcFlagMulticcd, + mjcFlagNativeccd, + mjcFlagOverride, + mjcFlagPassive, + mjcFlagRefsafe, + mjcFlagSensor, + mjcFlagWarmstart, + mjcOptionActuatorgroupdisable, + mjcOptionApirate, + mjcOptionCcd_iterations, + mjcOptionCcd_tolerance, + mjcOptionCone, + mjcOptionDensity, + mjcOptionImpratio, + mjcOptionIntegrator, + mjcOptionIterations, + mjcOptionJacobian, + mjcOptionLs_iterations, + mjcOptionLs_tolerance, + mjcOptionMagnetic, + mjcOptionNoslip_iterations, + mjcOptionNoslip_tolerance, + mjcOptionO_friction, + mjcOptionO_margin, + mjcOptionO_solimp, + mjcOptionO_solref, + mjcOptionSdf_initpoints, + mjcOptionSdf_iterations, + mjcOptionSolver, + mjcOptionTimestep, + mjcOptionTolerance, + mjcOptionViscosity, + mjcOptionWind, newton, pgs, pyramidal, diff --git a/src/experimental/usd/mjcPhysics/tokens.h b/src/experimental/usd/mjcPhysics/tokens.h index 50239a66..7bdf56f2 100644 --- a/src/experimental/usd/mjcPhysics/tokens.h +++ b/src/experimental/usd/mjcPhysics/tokens.h @@ -88,202 +88,202 @@ struct MjcPhysicsTokensType { /// Possible value for MjcPhysicsSceneAPI::GetIntegratorAttr(), This token /// represents the implicitfast numerical integrator. const TfToken implicitfast; - /// \brief "mjc:physics:actuatorgroupdisable" + /// \brief "mjc:flag:actuation" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsActuatorgroupdisable; - /// \brief "mjc:physics:apirate" + const TfToken mjcFlagActuation; + /// \brief "mjc:flag:autoreset" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsApirate; - /// \brief "mjc:physics:ccd_iterations" + const TfToken mjcFlagAutoreset; + /// \brief "mjc:flag:clampctrl" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsCcd_iterations; - /// \brief "mjc:physics:ccd_tolerance" + const TfToken mjcFlagClampctrl; + /// \brief "mjc:flag:constraint" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsCcd_tolerance; - /// \brief "mjc:physics:cone" + const TfToken mjcFlagConstraint; + /// \brief "mjc:flag:contact" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsCone; - /// \brief "mjc:physics:density" + const TfToken mjcFlagContact; + /// \brief "mjc:flag:energy" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsDensity; - /// \brief "mjc:physics:flag:actuation" + const TfToken mjcFlagEnergy; + /// \brief "mjc:flag:equality" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagActuation; - /// \brief "mjc:physics:flag:autoreset" + const TfToken mjcFlagEquality; + /// \brief "mjc:flag:eulerdamp" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagAutoreset; - /// \brief "mjc:physics:flag:clampctrl" + const TfToken mjcFlagEulerdamp; + /// \brief "mjc:flag:filterparent" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagClampctrl; - /// \brief "mjc:physics:flag:constraint" + const TfToken mjcFlagFilterparent; + /// \brief "mjc:flag:frictionloss" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagConstraint; - /// \brief "mjc:physics:flag:contact" + const TfToken mjcFlagFrictionloss; + /// \brief "mjc:flag:fwdinv" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagContact; - /// \brief "mjc:physics:flag:energy" + const TfToken mjcFlagFwdinv; + /// \brief "mjc:flag:gravity" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagEnergy; - /// \brief "mjc:physics:flag:equality" + const TfToken mjcFlagGravity; + /// \brief "mjc:flag:invdiscrete" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagEquality; - /// \brief "mjc:physics:flag:eulerdamp" + const TfToken mjcFlagInvdiscrete; + /// \brief "mjc:flag:island" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagEulerdamp; - /// \brief "mjc:physics:flag:filterparent" + const TfToken mjcFlagIsland; + /// \brief "mjc:flag:limit" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagFilterparent; - /// \brief "mjc:physics:flag:frictionloss" + const TfToken mjcFlagLimit; + /// \brief "mjc:flag:midphase" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagFrictionloss; - /// \brief "mjc:physics:flag:fwdinv" + const TfToken mjcFlagMidphase; + /// \brief "mjc:flag:multiccd" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagFwdinv; - /// \brief "mjc:physics:flag:gravity" + const TfToken mjcFlagMulticcd; + /// \brief "mjc:flag:nativeccd" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagGravity; - /// \brief "mjc:physics:flag:invdiscrete" + const TfToken mjcFlagNativeccd; + /// \brief "mjc:flag:override" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagInvdiscrete; - /// \brief "mjc:physics:flag:island" + const TfToken mjcFlagOverride; + /// \brief "mjc:flag:passive" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagIsland; - /// \brief "mjc:physics:flag:limit" + const TfToken mjcFlagPassive; + /// \brief "mjc:flag:refsafe" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagLimit; - /// \brief "mjc:physics:flag:midphase" + const TfToken mjcFlagRefsafe; + /// \brief "mjc:flag:sensor" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagMidphase; - /// \brief "mjc:physics:flag:multiccd" + const TfToken mjcFlagSensor; + /// \brief "mjc:flag:warmstart" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagMulticcd; - /// \brief "mjc:physics:flag:nativeccd" + const TfToken mjcFlagWarmstart; + /// \brief "mjc:option:actuatorgroupdisable" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagNativeccd; - /// \brief "mjc:physics:flag:override" + const TfToken mjcOptionActuatorgroupdisable; + /// \brief "mjc:option:apirate" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagOverride; - /// \brief "mjc:physics:flag:passive" + const TfToken mjcOptionApirate; + /// \brief "mjc:option:ccd_iterations" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagPassive; - /// \brief "mjc:physics:flag:refsafe" + const TfToken mjcOptionCcd_iterations; + /// \brief "mjc:option:ccd_tolerance" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagRefsafe; - /// \brief "mjc:physics:flag:sensor" + const TfToken mjcOptionCcd_tolerance; + /// \brief "mjc:option:cone" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagSensor; - /// \brief "mjc:physics:flag:warmstart" + const TfToken mjcOptionCone; + /// \brief "mjc:option:density" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsFlagWarmstart; - /// \brief "mjc:physics:impratio" + const TfToken mjcOptionDensity; + /// \brief "mjc:option:impratio" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsImpratio; - /// \brief "mjc:physics:integrator" + const TfToken mjcOptionImpratio; + /// \brief "mjc:option:integrator" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsIntegrator; - /// \brief "mjc:physics:iterations" + const TfToken mjcOptionIntegrator; + /// \brief "mjc:option:iterations" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsIterations; - /// \brief "mjc:physics:jacobian" + const TfToken mjcOptionIterations; + /// \brief "mjc:option:jacobian" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsJacobian; - /// \brief "mjc:physics:ls_iterations" + const TfToken mjcOptionJacobian; + /// \brief "mjc:option:ls_iterations" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsLs_iterations; - /// \brief "mjc:physics:ls_tolerance" + const TfToken mjcOptionLs_iterations; + /// \brief "mjc:option:ls_tolerance" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsLs_tolerance; - /// \brief "mjc:physics:magnetic" + const TfToken mjcOptionLs_tolerance; + /// \brief "mjc:option:magnetic" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsMagnetic; - /// \brief "mjc:physics:noslip_iterations" + const TfToken mjcOptionMagnetic; + /// \brief "mjc:option:noslip_iterations" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsNoslip_iterations; - /// \brief "mjc:physics:noslip_tolerance" + const TfToken mjcOptionNoslip_iterations; + /// \brief "mjc:option:noslip_tolerance" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsNoslip_tolerance; - /// \brief "mjc:physics:o_friction" + const TfToken mjcOptionNoslip_tolerance; + /// \brief "mjc:option:o_friction" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsO_friction; - /// \brief "mjc:physics:o_margin" + const TfToken mjcOptionO_friction; + /// \brief "mjc:option:o_margin" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsO_margin; - /// \brief "mjc:physics:o_solimp" + const TfToken mjcOptionO_margin; + /// \brief "mjc:option:o_solimp" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsO_solimp; - /// \brief "mjc:physics:o_solref" + const TfToken mjcOptionO_solimp; + /// \brief "mjc:option:o_solref" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsO_solref; - /// \brief "mjc:physics:sdf_initpoints" + const TfToken mjcOptionO_solref; + /// \brief "mjc:option:sdf_initpoints" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsSdf_initpoints; - /// \brief "mjc:physics:sdf_iterations" + const TfToken mjcOptionSdf_initpoints; + /// \brief "mjc:option:sdf_iterations" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsSdf_iterations; - /// \brief "mjc:physics:solver" + const TfToken mjcOptionSdf_iterations; + /// \brief "mjc:option:solver" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsSolver; - /// \brief "mjc:physics:timestep" + const TfToken mjcOptionSolver; + /// \brief "mjc:option:timestep" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsTimestep; - /// \brief "mjc:physics:tolerance" + const TfToken mjcOptionTimestep; + /// \brief "mjc:option:tolerance" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsTolerance; - /// \brief "mjc:physics:viscosity" + const TfToken mjcOptionTolerance; + /// \brief "mjc:option:viscosity" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsViscosity; - /// \brief "mjc:physics:wind" + const TfToken mjcOptionViscosity; + /// \brief "mjc:option:wind" /// /// MjcPhysicsSceneAPI - const TfToken mjcPhysicsWind; + const TfToken mjcOptionWind; /// \brief "newton" /// /// Fallback value for MjcPhysicsSceneAPI::GetSolverAttr(), This token From 84d658a5ad25173e5ed3b115194eb4403af79f28 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 19 May 2025 06:41:06 -0700 Subject: [PATCH 144/191] Remove redundant variable in mj_factorI PiperOrigin-RevId: 760604094 Change-Id: I664b4cc9d717bc12a4711feb3fa585777fb7a1aa --- src/engine/engine_core_smooth.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/engine/engine_core_smooth.c b/src/engine/engine_core_smooth.c index 7f30294a..d4deb5b3 100644 --- a/src/engine/engine_core_smooth.c +++ b/src/engine/engine_core_smooth.c @@ -1673,12 +1673,9 @@ void mj_factorI(mjtNum* mat, mjtNum* diaginv, int nv, // update triangle above row k for (int adr=end - 1; adr >= start; adr--) { - // tmp = L(k, i) / L(k, k) - mjtNum tmp = mat[adr] * invD; - // update row i < k: L(i, 0..i) -= L(i, 0..i) * L(k, i) / L(k, k) int i = colind[adr]; - mju_addToScl(mat + rowadr[i], mat + start, -tmp, rownnz[i]); + mju_addToScl(mat + rowadr[i], mat + start, -mat[adr] * invD, rownnz[i]); } // update row k: L(k, :) /= L(k, k) From 6c201d3e1c5ce34fecbb71dd5a184e24d2c74d91 Mon Sep 17 00:00:00 2001 From: Tom Power Date: Mon, 19 May 2025 07:33:54 -0700 Subject: [PATCH 145/191] Add examples of scaling assets into tutorial notebook. Fixes #2476 PiperOrigin-RevId: 760617809 Change-Id: I86d4fadb473cf8ad9bcfd058567551c431f2bf31 --- python/mjspec.ipynb | 420 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 397 insertions(+), 23 deletions(-) diff --git a/python/mjspec.ipynb b/python/mjspec.ipynb index 925da6bc..78b0f6f1 100644 --- a/python/mjspec.ipynb +++ b/python/mjspec.ipynb @@ -124,12 +124,12 @@ " highlighted = pygments.highlight(xml_string, lexer, formatter)\n", " display(HTML(f\"{highlighted}\"))\n", "\n", - "def render(model, data=None, height=300):\n", + "def render(model, data=None, height=300, camera=-1):\n", " if data is None:\n", " data = mj.MjData(model)\n", " with mj.Renderer(model, 480, 640) as renderer:\n", " mj.mj_forward(model, data)\n", - " renderer.update_scene(data)\n", + " renderer.update_scene(data, camera)\n", " media.show_image(renderer.render(), height=height)" ] }, @@ -148,6 +148,7 @@ "cell_type": "code", "execution_count": 0, "metadata": { + "cellView": "form", "id": "oummB7I7EfSq" }, "outputs": [], @@ -987,7 +988,7 @@ }, "outputs": [], "source": [ - "#@title Six Creatures on a floor.{vertical-output: true}\n", + "#@title Six Creatures on a floor {vertical-output: true}\n", "\n", "arena = mj.MjSpec()\n", "\n", @@ -1046,7 +1047,7 @@ }, "outputs": [], "source": [ - "#@title Video of the movement{vertical-output: true}\n", + "#@title Video of the movement {vertical-output: true}\n", "\n", "data = mj.MjData(model)\n", "duration = 10 # (Seconds)\n", @@ -1094,7 +1095,7 @@ }, "outputs": [], "source": [ - "#@title Movement trajectories{vertical-output: true}\n", + "#@title Movement trajectories {vertical-output: true}\n", "\n", "creature_colors = [torso.rgba[:3] for torso in torsos_model]\n", "fig, ax = plt.subplots(figsize=(4, 4))\n", @@ -1120,6 +1121,57 @@ "# Model editing" ] }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "cellView": "form", + "id": "m4sppBqvf7yd" + }, + "outputs": [], + "source": [ + "# @title Get resources\n", + "\n", + "# Get Models\n", + "print('Getting MuJoCo humanoid XML description from GitHub:')\n", + "!git clone https://github.com/google-deepmind/mujoco\n", + "humanoid_file = 'mujoco/model/humanoid/humanoid.xml'\n", + "humanoid100_file = 'mujoco/model/humanoid/humanoid100.xml'\n", + "print('Getting MuJoCo Fly and Franka XML description from GitHub:')\n", + "!git clone https://github.com/google-deepmind/mujoco_menagerie\n", + "fly_file = 'mujoco_menagerie/flybody/fruitfly.xml'\n", + "franka_file = 'mujoco_menagerie/franka_fr3/fr3.xml'\n", + "\n", + "# Camera options\n", + "cam = mj.MjvCamera()\n", + "mj.mjv_defaultCamera(cam)\n", + "cam.elevation = -10\n", + "cam.lookat = [0, 0, 1]\n", + "cam.distance = 4\n", + "cam.azimuth = 135\n", + "\n", + "# Arena\n", + "arena_xml = \"\"\"\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\"\"\"\n" + ] + }, { "cell_type": "markdown", "metadata": { @@ -1140,13 +1192,7 @@ }, "outputs": [], "source": [ - "#@title Traversing the spec.{vertical-output: true}\n", - "\n", - "# Get MuJoCo's humanoid model.\n", - "print('Getting MuJoCo humanoid XML description from GitHub:')\n", - "!git clone https://github.com/google-deepmind/mujoco\n", - "humanoid_file = 'mujoco/model/humanoid/humanoid.xml'\n", - "humanoid100_file = 'mujoco/model/humanoid/humanoid100.xml'\n", + "#@title Traversing the spec {vertical-output: true}\n", "\n", "spec = mj.MjSpec.from_file(humanoid_file)\n", "\n", @@ -1183,7 +1229,7 @@ }, "outputs": [], "source": [ - "#@title Model re-compilation with state preservation.{vertical-output: true}\n", + "#@title Model re-compilation with state preservation {vertical-output: true}\n", "\n", "spec = mj.MjSpec.from_file(humanoid100_file)\n", "model = spec.compile()\n", @@ -1239,7 +1285,7 @@ }, "outputs": [], "source": [ - "#@title Humanoid model.{vertical-output: true}\n", + "#@title Humanoid model {vertical-output: true}\n", "\n", "spec = mj.MjSpec.from_file(humanoid_file)\n", "\n", @@ -1264,7 +1310,7 @@ }, "outputs": [], "source": [ - "#@title Humanoid with arms replaced by legs.{vertical-output: true}\n", + "#@title Humanoid with arms replaced by legs {vertical-output: true}\n", "\n", "spec = mj.MjSpec.from_file(humanoid_file)\n", "spec.copy_during_attach = True\n", @@ -1309,11 +1355,7 @@ }, "outputs": [], "source": [ - "#@title Humanoid with Franka arm.{vertical-output: true}\n", - "\n", - "# Get Franka arm from the MuJoCo Menagerie.\n", - "!git clone https://github.com/google-deepmind/mujoco_menagerie\n", - "franka_file = 'mujoco_menagerie/franka_fr3/fr3.xml'\n", + "#@title Humanoid with Franka arm {vertical-output: true}\n", "\n", "spec = mj.MjSpec.from_file(humanoid_file)\n", "franka = mj.MjSpec.from_file(franka_file)\n", @@ -1350,11 +1392,12 @@ "cell_type": "code", "execution_count": 0, "metadata": { + "cellView": "form", "id": "50lOgJ7mQ2bV" }, "outputs": [], "source": [ - "#@title Imported actuators.{vertical-output: true}\n", + "#@title Imported actuators {vertical-output: true}\n", "\n", "for actuator in spec.actuators:\n", " print(actuator.name)" @@ -1377,7 +1420,7 @@ }, "outputs": [], "source": [ - "#@title Humanoid with randomized heads and arm poses.{vertical-output: true}\n", + "#@title Humanoid with randomized heads and arm poses {vertical-output: true}\n", "\n", "humanoid = mj.MjSpec.from_file(humanoid_file)\n", "spec = mj.MjSpec()\n", @@ -1412,13 +1455,344 @@ "model = spec.compile()\n", "render(model, height=400)" ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "iXgYCVzEWFTU" + }, + "source": [ + "## Model scaling" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "-hSJKyH4A2VY" + }, + "outputs": [], + "source": [ + "#@title Uniformly scale humanoid {vertical-output: true}\n", + "\n", + "def scale_spec(spec, scale):\n", + " scaled_spec = spec.copy()\n", + " # Traverse the kinematic tree, scaling all geoms\n", + " def scale_bodies(parent, scale=1.0):\n", + " body = parent.first_body()\n", + " while body:\n", + " if body.pos is not None:\n", + " body.pos = body.pos * scale\n", + " for geom in body.geoms:\n", + " geom.fromto = geom.fromto * scale\n", + " geom.size = geom.size * scale\n", + " if geom.pos is not None:\n", + " geom.pos = geom.pos * scale\n", + " scale_bodies(body, scale)\n", + " body = parent.next_body(body)\n", + "\n", + " scale_bodies(scaled_spec.body('world'), scale)\n", + " return scaled_spec\n", + "\n", + "spec = mj.MjSpec.from_string(arena_xml)\n", + "humanoid = mj.MjSpec.from_file(humanoid_file)\n", + "small_humanoid = scale_spec(humanoid, 0.75)\n", + "large_humanoid = scale_spec(humanoid, 1.25)\n", + "\n", + "# Create a line-up of humanoids\n", + "frame = spec.worldbody.add_frame(pos=[-1, 0, 0],\n", + " quat=[-np.sqrt(2)/2, 0, 0, np.sqrt(2) / 2])\n", + "frame.attach_body(humanoid.body('torso'), str(0))\n", + "\n", + "frame = spec.worldbody.add_frame(pos=[0, 0, 0],\n", + " quat=[-np.sqrt(2)/2, 0, 0, np.sqrt(2) / 2])\n", + "frame.attach_body(small_humanoid.body('torso'), str(1))\n", + "\n", + "frame = spec.worldbody.add_frame(pos=[1, 0, 0],\n", + " quat=[-np.sqrt(2)/2, 0, 0, np.sqrt(2) / 2] )\n", + "frame.attach_body(large_humanoid.body('torso'), str(2))\n", + "\n", + "\n", + "spec.worldbody.add_light(mode=mj.mjtCamLight.mjCAMLIGHT_TARGETBODYCOM,\n", + " targetbody='1torso', diffuse=[.8, .8, .8],\n", + " specular=[0.3, 0.3, 0.3], pos=[0, -6, 4], cutoff=30)\n", + "model = spec.compile()\n", + "render(model, height=400, camera=cam)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tBH5vmqJXleD" + }, + "source": [ + "We can scale the size of a model by traversing the kinematic tree and applying the the scale to the relevant geoms. Above we can see humanoids of three different sizes." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "cV4tkG6siFQp" + }, + "outputs": [], + "source": [ + "# @title Scaling actuator forces {vertical-output: true}\n", + "\n", + "def scale_spec(spec, scale, scale_actuators=False):\n", + " scaled_spec = spec.copy()\n", + " # Traverse the kinematic tree, scaling all geoms\n", + " def scale_bodies(parent, scale=1.0):\n", + " body = parent.first_body()\n", + " while body:\n", + " if body.pos is not None:\n", + " body.pos = body.pos * scale\n", + " for geom in body.geoms:\n", + " geom.fromto = geom.fromto * scale\n", + " geom.size = geom.size * scale\n", + " if geom.pos is not None:\n", + " geom.pos = geom.pos * scale\n", + " scale_bodies(body, scale)\n", + " body = parent.next_body(body)\n", + "\n", + " if scale_actuators:\n", + " # scale gear\n", + " for actuator in scaled_spec.actuators:\n", + " # scale the actuator gear by (scale ** 2),\n", + " # this is because muscle force-generating capacity\n", + " # scales with the cross-sectional area of the muscle\n", + " actuator.gear = actuator.gear * scale * scale\n", + "\n", + " # scale the z-position of the humanoid for all keypoints\n", + " for keypoint in scaled_spec.keys:\n", + " qpos = keypoint.qpos\n", + " qpos[2] = qpos[2] * scale\n", + " keypoint.qpos = qpos\n", + " keypoint.qpos[2] = keypoint.qpos[2] * scale\n", + "\n", + " scale_bodies(scaled_spec.body('world'), scale)\n", + " return scaled_spec\n", + "\n", + "# Create specs\n", + "scale = 0.6\n", + "spec = mj.MjSpec.from_string(arena_xml)\n", + "humanoid = mj.MjSpec.from_file(humanoid_file)\n", + "small_humanoid = scale_spec(humanoid, scale)\n", + "small_humanoid_actuators_scaled = scale_spec(humanoid, scale, True)\n", + "\n", + "# Create a line-up of humanoids\n", + "squat_qpos = []\n", + "\n", + "# Add unscaled humanoid\n", + "frame = spec.worldbody.add_frame(pos=[-1, 0, 0],\n", + " quat=[-np.sqrt(2)/2, 0, 0, np.sqrt(2) / 2])\n", + "frame.attach_body(humanoid.body('torso'), str(0))\n", + "# Record squat pose\n", + "humanoid_squat = humanoid.key('squat').qpos\n", + "humanoid_squat[:2] = frame.pos[:2]\n", + "humanoid_squat[3:7] = frame.quat\n", + "squat_qpos.append(humanoid_squat)\n", + "\n", + "# Add small humanoid\n", + "frame = spec.worldbody.add_frame(pos=[0, 0, 0],\n", + " quat=[-np.sqrt(2)/2, 0, 0, np.sqrt(2) / 2])\n", + "frame.attach_body(small_humanoid.body('torso'), str(1))\n", + "# Record squat pose\n", + "humanoid_squat = small_humanoid.key('squat').qpos\n", + "humanoid_squat[:2] = frame.pos[:2]\n", + "humanoid_squat[3:7] = frame.quat\n", + "squat_qpos.append(humanoid_squat)\n", + "\n", + "# Add small humanoid with scaled actuators\n", + "frame = spec.worldbody.add_frame(pos=[1, 0, 0],\n", + " quat=[-np.sqrt(2)/2, 0, 0, np.sqrt(2) / 2] )\n", + "frame.attach_body(small_humanoid_actuators_scaled.body('torso'), str(2))\n", + "# Record squat pose\n", + "humanoid_squat = small_humanoid_actuators_scaled.key('squat').qpos\n", + "humanoid_squat[:2] = frame.pos[:2]\n", + "humanoid_squat[3:7] = frame.quat\n", + "squat_qpos.append(humanoid_squat)\n", + "squat_qpos = np.concatenate(squat_qpos)\n", + "\n", + "spec.worldbody.add_light(mode=mj.mjtCamLight.mjCAMLIGHT_TARGETBODYCOM,\n", + " targetbody='1torso', diffuse=[.8, .8, .8],\n", + " specular=[0.3, 0.3, 0.3], pos=[0, -6, 4], cutoff=30)\n", + "model = spec.compile()\n", + "\n", + "# Initialize to squat position\n", + "data = mj.MjData(model)\n", + "data.qpos = squat_qpos\n", + "\n", + "# jumping motion\n", + "u_t = lambda t: 10.0 * t / duration\n", + "\n", + "# Simulate and display video.\n", + "duration = 2 # (seconds)\n", + "framerate = 30 # (Hz)\n", + "frames = []\n", + "\n", + "with mj.Renderer(model, 480, 640) as renderer:\n", + " while data.time < duration:\n", + " data.ctrl = u_t(data.time)\n", + " mj.mj_step(model, data)\n", + " if len(frames) < data.time * framerate:\n", + " renderer.update_scene(data, camera=cam)\n", + " pixels = renderer.render()\n", + " frames.append(pixels)\n", + "\n", + "media.show_video(frames, fps=framerate, height=400)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZSOra3S2YpIB" + }, + "source": [ + "We can also apply scaling to the actuators. In the humanoid case, scaling the geoms without scaling the `gear` parameter for the actuators results in a humanoid that can jump higher proportional to its size." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "9IuwQQ0F2ddA" + }, + "outputs": [], + "source": [ + "# @title Long-limbed humanoid {vertical-output: true}\n", + "\n", + "def scale_spec(spec, scale):\n", + " scaled_spec = spec.copy()\n", + " # Traverse the kinematic tree, scaling all geoms\n", + " def scale_bodies(parent, scale=1.0):\n", + " if parent is not None:\n", + " for geom in parent.geoms:\n", + " # Only scale fromto, not size to scale length of capsules\n", + " geom.fromto = geom.fromto * scale\n", + " if geom.pos is not None:\n", + " geom.pos = geom.pos * scale\n", + " body = parent.first_body()\n", + " while body:\n", + " if body.pos is not None:\n", + " body.pos = body.pos * scale\n", + " scale_bodies(body, scale)\n", + " body = parent.next_body(body)\n", + "\n", + " # Scale all the limbs\n", + " scale_bodies(scaled_spec.body('upper_arm_right'), scale)\n", + " scale_bodies(scaled_spec.body('upper_arm_left'), scale)\n", + " scale_bodies(scaled_spec.body('thigh_right'), scale)\n", + " scale_bodies(scaled_spec.body('thigh_left'), scale)\n", + " return scaled_spec\n", + "\n", + "spec = mj.MjSpec.from_string(arena_xml)\n", + "humanoid = mj.MjSpec.from_file(humanoid_file)\n", + "small_humanoid = scale_spec(humanoid, 1.25)\n", + "large_humanoid = scale_spec(humanoid, 2)\n", + "\n", + "# Create a line-up of humanoids by attaching\n", + "frame = spec.worldbody.add_frame(pos=[-1, 0, 0],\n", + " quat=[-np.sqrt(2)/2, 0, 0, np.sqrt(2) / 2])\n", + "frame.attach_body(humanoid.body('torso'), str(0), str(0))\n", + "\n", + "frame = spec.worldbody.add_frame(pos=[0, 0, 0.2],\n", + " quat=[-np.sqrt(2)/2, 0, 0, np.sqrt(2) / 2])\n", + "frame.attach_body(small_humanoid.body('torso'), str(0), str(1))\n", + "\n", + "frame = spec.worldbody.add_frame(pos=[1, 0, 0.8],\n", + " quat=[-np.sqrt(2)/2, 0, 0, np.sqrt(2) / 2] )\n", + "frame.attach_body(large_humanoid.body('torso'), str(0), str(2))\n", + "\n", + "\n", + "spec.worldbody.add_light(mode=mj.mjtCamLight.mjCAMLIGHT_TARGETBODYCOM,\n", + " targetbody='0torso1', diffuse=[.8, .8, .8],\n", + " specular=[0.3, 0.3, 0.3], pos=[0, -6, 4], cutoff=30)\n", + "model = spec.compile()\n", + "\n", + "# camera options\n", + "render(model, height=400, camera=cam)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "D2DrPBvBZjI0" + }, + "source": [ + "We can also apply scaling to the model non-uniformly. In this instance we scale the humanoid to have long limbs, by only applying the scale to the length of the capsule geoms for the arms, legs and feet." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "1G8VO45v2ddA" + }, + "outputs": [], + "source": [ + "# @title Meshes {vertical-output: true}\n", + "\n", + "def scale_spec(spec, scale):\n", + " scaled_spec = spec.copy()\n", + " # scale all meshes\n", + " for mesh in scaled_spec.meshes:\n", + " if mesh.scale is None:\n", + " mesh.scale = np.ones(3)\n", + " mesh.scale = mesh.scale * scale\n", + "\n", + " # Traverse the kinematic tree\n", + " def scale_bodies(parent, scale=1.0):\n", + " if parent is not None:\n", + " for geom in parent.geoms:\n", + " if geom.pos is not None:\n", + " geom.pos = geom.pos * scale\n", + " body = parent.first_body()\n", + " while body:\n", + " if body.pos is not None:\n", + " body.pos = body.pos * scale\n", + " scale_bodies(body, scale)\n", + " body = parent.next_body(body)\n", + "\n", + " # Scale all the limbs\n", + " scale_bodies(scaled_spec.body('world'), scale)\n", + "\n", + " return scaled_spec\n", + "\n", + "spec = mj.MjSpec.from_string(arena_xml)\n", + "fly = mj.MjSpec.from_file(fly_file)\n", + "# Remove lights from fly so they are not duplicated in line-up\n", + "for light in fly.lights:\n", + " light.delete()\n", + "\n", + "small_fly = scale_spec(fly, 1.25)\n", + "large_fly = scale_spec(fly, 2)\n", + "\n", + "# Create a line-up of flys by attaching\n", + "frame = spec.worldbody.add_frame(pos=[-1, 0, 0.25],\n", + " quat=[-np.sqrt(2)/2, 0, 0, np.sqrt(2) / 2])\n", + "frame.attach_body(fly.body('thorax'), str(0), str(0))\n", + "\n", + "frame = spec.worldbody.add_frame(pos=[0, 0, 0.25],\n", + " quat=[-np.sqrt(2)/2, 0, 0, np.sqrt(2) / 2])\n", + "frame.attach_body(small_fly.body('thorax'), str(0), str(1))\n", + "\n", + "frame = spec.worldbody.add_frame(pos=[1, 0, 0.25],\n", + " quat=[-np.sqrt(2)/2, 0, 0, np.sqrt(2) / 2] )\n", + "frame.attach_body(large_fly.body('thorax'), str(0), str(2))\n", + "\n", + "spec.worldbody.add_light(mode=mj.mjtCamLight.mjCAMLIGHT_TARGETBODYCOM,\n", + " targetbody='0thorax1', diffuse=[.8, .8, .8],\n", + " specular=[0.3, 0.3, 0.3], pos=[0, -6, 4], cutoff=30)\n", + "model = spec.compile()\n", + "render(model, height=400, camera=cam)\n" + ] } ], "metadata": { "accelerator": "GPU", "colab": { "collapsed_sections": [ - "sJFuNetilv4m", "yXY7HGfVsVlo" ], "gpuClass": "premium", From 670c97c4f753384d730f972d57e20a572492eba6 Mon Sep 17 00:00:00 2001 From: Tom Power Date: Mon, 19 May 2025 08:56:55 -0700 Subject: [PATCH 146/191] Reorder model editing notebook, placing dm_control example last PiperOrigin-RevId: 760642275 Change-Id: I7d8a625ab1d6d8364de93571390e8c14289e070a --- python/mjspec.ipynb | 574 ++++++++++++++++++++++---------------------- 1 file changed, 286 insertions(+), 288 deletions(-) diff --git a/python/mjspec.ipynb b/python/mjspec.ipynb index 78b0f6f1..926f9bfe 100644 --- a/python/mjspec.ipynb +++ b/python/mjspec.ipynb @@ -853,269 +853,7 @@ { "cell_type": "markdown", "metadata": { - "id": "3N4YEIVt75_T" - }, - "source": [ - "# `dm_control` example" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "TcQuv56BwaJf" - }, - "source": [ - "A key feature is the ability to easily attach multiple models into a larger one. Disambiguation of duplicated names from different\n", - "models, or multiple instances of the same model is handled via user-defined namespacing.\n", - "\n", - "One example use case is when we want robots with a variable number of joints, as this is a fundamental change to the kinematic structure. The snippets below follow the lines of the [example in dm_control](https://arxiv.org/abs/2006.12983), an older package with similar capabilities." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "id": "7C-hfbtj8nRV" - }, - "outputs": [], - "source": [ - "leg_model = \"\"\"\n", - "\n", - " \n", - "\n", - " \n", - " \n", - " \n", - " \n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - " \n", - " \n", - " \n", - " \n", - "\n", - "\"\"\"\n", - "\n", - "class Leg(object):\n", - " \"\"\"A 2-DoF leg with position actuators.\"\"\"\n", - " def __init__(self, length, rgba):\n", - " self.spec = mj.MjSpec.from_string(leg_model)\n", - "\n", - " # Thigh:\n", - " thigh = self.spec.body('thigh')\n", - " thigh.add_geom(fromto=[0, 0, 0, length, 0, 0], size=[length/4, 0, 0], rgba=rgba)\n", - "\n", - " # Hip:\n", - " shin = self.spec.body('shin')\n", - " shin.add_geom(fromto=[0, 0, 0, 0, 0, -length], size=[length/5, 0, 0], rgba=rgba)\n", - " shin.pos[0] = length" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "MQGsxnIB_RLO" - }, - "source": [ - "The `Leg` class describes an abstract articulated leg, with two joints and corresponding proportional-derivative actuators.\n", - "\n", - "Note that:\n", - "\n", - "- MJCF attributes correspond directly to arguments of the `add_()` methods.\n", - "- When referencing elements, e.g when specifying the joint to which an actuator is attached, the name string of the MJCF elements is used." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "id": "kMiuMyZW_XoB" - }, - "outputs": [], - "source": [ - "BODY_RADIUS = 0.1\n", - "random_state = np.random.RandomState(42)\n", - "creature_model = \"\"\"\n", - "\n", - " \n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\"\"\".format(BODY_RADIUS, BODY_RADIUS, BODY_RADIUS / 2)\n", - "\n", - "def make_creature(num_legs):\n", - " \"\"\"Constructs a creature with `num_legs` legs.\"\"\"\n", - " rgba = random_state.uniform([0, 0, 0, 1], [1, 1, 1, 1])\n", - " spec = mj.MjSpec.from_string(creature_model)\n", - " spec.copy_during_attach = True\n", - "\n", - " # Attach legs to equidistant sites on the circumference.\n", - " spec.worldbody.first_geom().rgba = rgba\n", - " leg = Leg(length=BODY_RADIUS, rgba=rgba)\n", - " for i in range(num_legs):\n", - " theta = 2 * i * np.pi / num_legs\n", - " hip_pos = BODY_RADIUS * np.array([np.cos(theta), np.sin(theta), 0])\n", - " hip_site = spec.worldbody.add_site(pos=hip_pos, euler=[0, 0, theta])\n", - " hip_site.attach_body(leg.spec.body('thigh'), '', '-' + str(i))\n", - "\n", - " return spec" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "QMQ3jc6-_toj" - }, - "source": [ - "The `make_creature` function uses the `attach()` method to procedurally attach legs to the torso. Note that at this stage both the torso and hip attachment sites are children of the `worldbody`, since their parent body has yet to be instantiated. We'll now make an arena with a chequered floor and two lights, and place our creatures in a grid." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "id": "vt2JwXd__1cT" - }, - "outputs": [], - "source": [ - "#@title Six Creatures on a floor {vertical-output: true}\n", - "\n", - "arena = mj.MjSpec()\n", - "\n", - "if hasattr(arena, 'compiler'):\n", - " arena.compiler.degree = False # MuJoCo dev (next release).\n", - "else:\n", - " arena.degree = False # MuJoCo release\n", - "\n", - "# Make arena with textured floor.\n", - "chequered = arena.add_texture(\n", - " name=\"chequered\", type=mj.mjtTexture.mjTEXTURE_2D,\n", - " builtin=mj.mjtBuiltin.mjBUILTIN_CHECKER,\n", - " width=300, height=300, rgb1=[.2, .3, .4], rgb2=[.3, .4, .5])\n", - "grid = arena.add_material(\n", - " name='grid', texrepeat=[5, 5], reflectance=.2\n", - " ).textures[mj.mjtTextureRole.mjTEXROLE_RGB] = 'chequered'\n", - "arena.worldbody.add_geom(\n", - " type=mj.mjtGeom.mjGEOM_PLANE, size=[2, 2, .1], material='grid')\n", - "for x in [-2, 2]:\n", - " arena.worldbody.add_light(pos=[x, -1, 3], dir=[-x, 1, -2])\n", - "\n", - "# Instantiate 6 creatures with 3 to 8 legs.\n", - "creatures = [make_creature(num_legs=num_legs) for num_legs in range(3, 9)]\n", - "\n", - "# Place them on a grid in the arena.\n", - "height = .15\n", - "grid = 5 * BODY_RADIUS\n", - "xpos, ypos, zpos = np.meshgrid([-grid, 0, grid], [0, grid], [height])\n", - "for i, spec in enumerate(creatures):\n", - " # Place spawn sites on a grid.\n", - " spawn_pos = (xpos.flat[i], ypos.flat[i], zpos.flat[i])\n", - " spawn_site = arena.worldbody.add_site(pos=spawn_pos, group=3)\n", - " # Attach to the arena at the spawn sites, with a free joint.\n", - " spawn_body = spawn_site.attach_body(spec.worldbody, '', '-' + str(i))\n", - " spawn_body.add_freejoint()\n", - "\n", - "# Instantiate the physics and render.\n", - "model = arena.compile()\n", - "render(model)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "mPUGkrCzAFMg" - }, - "source": [ - "Multi-legged creatures, ready to roam! Let's inject some controls and watch them move. We'll generate a sinusoidal open-loop control signal of fixed frequency and random phase, recording both video frames and the horizontal positions of the torso geoms, in order to plot the movement trajectories." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "id": "7gz9FfNzGxPO" - }, - "outputs": [], - "source": [ - "#@title Video of the movement {vertical-output: true}\n", - "\n", - "data = mj.MjData(model)\n", - "duration = 10 # (Seconds)\n", - "framerate = 30 # (Hz)\n", - "video = []\n", - "pos_x = []\n", - "pos_y = []\n", - "geoms = arena.worldbody.find_all(mj.mjtObj.mjOBJ_GEOM)\n", - "torsos_data = [data.bind(geom) for geom in geoms if 'torso' in geom.name]\n", - "torsos_model = [model.bind(geom) for geom in geoms if 'torso' in geom.name]\n", - "actuators = [data.bind(actuator) for actuator in arena.actuators]\n", - "\n", - "# Control signal frequency, phase, amplitude.\n", - "freq = 5\n", - "phase = 2 * np.pi * random_state.rand(len(arena.actuators))\n", - "amp = 0.9\n", - "\n", - "# Simulate, saving video frames and torso locations.\n", - "mj.mj_resetData(model, data)\n", - "with mj.Renderer(model) as renderer:\n", - " while data.time < duration:\n", - " # Inject controls and step the physics.\n", - " for i, actuator in enumerate(actuators):\n", - " actuator.ctrl = amp * np.sin(freq * data.time + phase[i])\n", - " mj.mj_step(model, data)\n", - "\n", - " # Save torso horizontal positions using name indexing.\n", - " pos_x.append([torso.xpos[0] for torso in torsos_data])\n", - " pos_y.append([torso.xpos[1] for torso in torsos_data])\n", - "\n", - " # Save video frames.\n", - " if len(video) < data.time * framerate:\n", - " renderer.update_scene(data)\n", - " pixels = renderer.render()\n", - " video.append(pixels.copy())\n", - "\n", - "media.show_video(video, fps=framerate)" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "id": "qt2L52e_Tcgt" - }, - "outputs": [], - "source": [ - "#@title Movement trajectories {vertical-output: true}\n", - "\n", - "creature_colors = [torso.rgba[:3] for torso in torsos_model]\n", - "fig, ax = plt.subplots(figsize=(4, 4))\n", - "ax.set_prop_cycle(color=creature_colors)\n", - "_ = ax.plot(pos_x, pos_y, linewidth=4)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "kSEUoxifxYJ4" - }, - "source": [ - "The plot above shows the corresponding movement trajectories of creature positions. Note how `mjSpec` attribute `id` were used to access both `xpos` and `rgba` values. This attribute is valid only after a model is compiled." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "QZ8alJZz8cB1" + "id": "IGd0uD64LdEJ" }, "source": [ "# Model editing" @@ -1125,8 +863,7 @@ "cell_type": "code", "execution_count": 0, "metadata": { - "cellView": "form", - "id": "m4sppBqvf7yd" + "id": "223KzKAzLdEJ" }, "outputs": [], "source": [ @@ -1175,7 +912,7 @@ { "cell_type": "markdown", "metadata": { - "id": "JN3Z4v0PyXKa" + "id": "eGgXNjQ8LdEK" }, "source": [ "`mjSpec` elements can be traversed in two ways:\n", @@ -1188,7 +925,7 @@ "cell_type": "code", "execution_count": 0, "metadata": { - "id": "8IcB7nezblyT" + "id": "Len0o_idLdEK" }, "outputs": [], "source": [ @@ -1215,7 +952,7 @@ { "cell_type": "markdown", "metadata": { - "id": "hcGI4orhyzvc" + "id": "GeiFFBYxLdEK" }, "source": [ "An `mjSpec` can be compiled multiple times. If the state has to be preserved between different compilations, then the function `recompile()` must be used, which returns a new `mjData` that contains the mapped state, possibly having a different dimension from the origin." @@ -1225,7 +962,7 @@ "cell_type": "code", "execution_count": 0, "metadata": { - "id": "uh_N1Fkqk-Mi" + "id": "eiRXgh9OLdEK" }, "outputs": [], "source": [ @@ -1271,7 +1008,7 @@ { "cell_type": "markdown", "metadata": { - "id": "XmSlXirVzLqt" + "id": "kuTWD415LdEK" }, "source": [ "Let us load the humanoid model and inspect it." @@ -1281,7 +1018,7 @@ "cell_type": "code", "execution_count": 0, "metadata": { - "id": "UywMzsp5Hnk2" + "id": "5d1wmQM2LdEK" }, "outputs": [], "source": [ @@ -1296,7 +1033,7 @@ { "cell_type": "markdown", "metadata": { - "id": "owcmKeuSzQRy" + "id": "38PXB1rWLdEK" }, "source": [ "We wish to remove the arms and replace them with the legs. This can be done by first storing the arm positions into frames attached to the torso. Then we can detach the arms and self-attach the legs into the frames." @@ -1306,7 +1043,7 @@ "cell_type": "code", "execution_count": 0, "metadata": { - "id": "qZCyv-B0IGiG" + "id": "0eaNq0Q7LdEK" }, "outputs": [], "source": [ @@ -1341,7 +1078,7 @@ { "cell_type": "markdown", "metadata": { - "id": "LnEwEjW3zdua" + "id": "HfhxL2EqLdEK" }, "source": [ "Similarly, different models can be attach together. Here, the right arm is detached and a robot arm from a different model is attached in its place." @@ -1351,7 +1088,7 @@ "cell_type": "code", "execution_count": 0, "metadata": { - "id": "w-NdFhSIIrLL" + "id": "uS4LGbI7LdEK" }, "outputs": [], "source": [ @@ -1382,7 +1119,7 @@ { "cell_type": "markdown", "metadata": { - "id": "e_idaggAznXu" + "id": "CWXYy_1uLdEK" }, "source": [ "When doing this, the actuators and all other objects referenced by the attached sub-tree are imported in the new model. All assets are currently imported, referenced or not." @@ -1392,8 +1129,7 @@ "cell_type": "code", "execution_count": 0, "metadata": { - "cellView": "form", - "id": "50lOgJ7mQ2bV" + "id": "UwWDD-NHLdEK" }, "outputs": [], "source": [ @@ -1406,7 +1142,7 @@ { "cell_type": "markdown", "metadata": { - "id": "APDoWK4mz0aJ" + "id": "hDvt3vcxLdEK" }, "source": [ "Domain randomization can be performed by attaching multiple times the same spec, edited each time with a new instance of randomized parameters." @@ -1416,7 +1152,7 @@ "cell_type": "code", "execution_count": 0, "metadata": { - "id": "oHjdgkISNLKy" + "id": "oPPFbWawLdEK" }, "outputs": [], "source": [ @@ -1459,7 +1195,7 @@ { "cell_type": "markdown", "metadata": { - "id": "iXgYCVzEWFTU" + "id": "PML2pxYgLdEK" }, "source": [ "## Model scaling" @@ -1469,7 +1205,7 @@ "cell_type": "code", "execution_count": 0, "metadata": { - "id": "-hSJKyH4A2VY" + "id": "pcUNLmQBLdEK" }, "outputs": [], "source": [ @@ -1523,7 +1259,7 @@ { "cell_type": "markdown", "metadata": { - "id": "tBH5vmqJXleD" + "id": "RYbaTPNmLdEK" }, "source": [ "We can scale the size of a model by traversing the kinematic tree and applying the the scale to the relevant geoms. Above we can see humanoids of three different sizes." @@ -1533,7 +1269,7 @@ "cell_type": "code", "execution_count": 0, "metadata": { - "id": "cV4tkG6siFQp" + "id": "u-ejx8lKLdEK" }, "outputs": [], "source": [ @@ -1646,7 +1382,7 @@ { "cell_type": "markdown", "metadata": { - "id": "ZSOra3S2YpIB" + "id": "uKhDI_IfLdEK" }, "source": [ "We can also apply scaling to the actuators. In the humanoid case, scaling the geoms without scaling the `gear` parameter for the actuators results in a humanoid that can jump higher proportional to its size." @@ -1656,7 +1392,7 @@ "cell_type": "code", "execution_count": 0, "metadata": { - "id": "9IuwQQ0F2ddA" + "id": "ovQIAxn7LdEK" }, "outputs": [], "source": [ @@ -1717,7 +1453,7 @@ { "cell_type": "markdown", "metadata": { - "id": "D2DrPBvBZjI0" + "id": "8o1daXIOLdEK" }, "source": [ "We can also apply scaling to the model non-uniformly. In this instance we scale the humanoid to have long limbs, by only applying the scale to the length of the capsule geoms for the arms, legs and feet." @@ -1727,7 +1463,7 @@ "cell_type": "code", "execution_count": 0, "metadata": { - "id": "1G8VO45v2ddA" + "id": "UBSo2nfQLdEK" }, "outputs": [], "source": [ @@ -1787,6 +1523,268 @@ "model = spec.compile()\n", "render(model, height=400, camera=cam)\n" ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qhXwxLe3LdEK" + }, + "source": [ + "# dm_control example" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "enuJ_YIqLdEK" + }, + "source": [ + "A key feature is the ability to easily attach multiple models into a larger one. Disambiguation of duplicated names from different\n", + "models, or multiple instances of the same model is handled via user-defined namespacing.\n", + "\n", + "One example use case is when we want robots with a variable number of joints, as this is a fundamental change to the kinematic structure. The snippets below follow the lines of the [example in dm_control](https://arxiv.org/abs/2006.12983), an older package with similar capabilities." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "4p3P_dP8LdEK" + }, + "outputs": [], + "source": [ + "leg_model = \"\"\"\n", + "\n", + " \n", + "\n", + " \n", + " \n", + " \n", + " \n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + " \n", + " \n", + " \n", + " \n", + "\n", + "\"\"\"\n", + "\n", + "class Leg(object):\n", + " \"\"\"A 2-DoF leg with position actuators.\"\"\"\n", + " def __init__(self, length, rgba):\n", + " self.spec = mj.MjSpec.from_string(leg_model)\n", + "\n", + " # Thigh:\n", + " thigh = self.spec.body('thigh')\n", + " thigh.add_geom(fromto=[0, 0, 0, length, 0, 0], size=[length/4, 0, 0], rgba=rgba)\n", + "\n", + " # Hip:\n", + " shin = self.spec.body('shin')\n", + " shin.add_geom(fromto=[0, 0, 0, 0, 0, -length], size=[length/5, 0, 0], rgba=rgba)\n", + " shin.pos[0] = length" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Mqr8rXLILdEK" + }, + "source": [ + "The `Leg` class describes an abstract articulated leg, with two joints and corresponding proportional-derivative actuators.\n", + "\n", + "Note that:\n", + "\n", + "- MJCF attributes correspond directly to arguments of the `add_()` methods.\n", + "- When referencing elements, e.g when specifying the joint to which an actuator is attached, the name string of the MJCF elements is used." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "1z2NBpAPLdEK" + }, + "outputs": [], + "source": [ + "BODY_RADIUS = 0.1\n", + "random_state = np.random.RandomState(42)\n", + "creature_model = \"\"\"\n", + "\n", + " \n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\"\"\".format(BODY_RADIUS, BODY_RADIUS, BODY_RADIUS / 2)\n", + "\n", + "def make_creature(num_legs):\n", + " \"\"\"Constructs a creature with `num_legs` legs.\"\"\"\n", + " rgba = random_state.uniform([0, 0, 0, 1], [1, 1, 1, 1])\n", + " spec = mj.MjSpec.from_string(creature_model)\n", + " spec.copy_during_attach = True\n", + "\n", + " # Attach legs to equidistant sites on the circumference.\n", + " spec.worldbody.first_geom().rgba = rgba\n", + " leg = Leg(length=BODY_RADIUS, rgba=rgba)\n", + " for i in range(num_legs):\n", + " theta = 2 * i * np.pi / num_legs\n", + " hip_pos = BODY_RADIUS * np.array([np.cos(theta), np.sin(theta), 0])\n", + " hip_site = spec.worldbody.add_site(pos=hip_pos, euler=[0, 0, theta])\n", + " hip_site.attach_body(leg.spec.body('thigh'), '', '-' + str(i))\n", + "\n", + " return spec" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "865FGuntLdEL" + }, + "source": [ + "The `make_creature` function uses the `attach()` method to procedurally attach legs to the torso. Note that at this stage both the torso and hip attachment sites are children of the `worldbody`, since their parent body has yet to be instantiated. We'll now make an arena with a chequered floor and two lights, and place our creatures in a grid." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "2fPaSkgfLdEL" + }, + "outputs": [], + "source": [ + "#@title Six Creatures on a floor {vertical-output: true}\n", + "\n", + "arena = mj.MjSpec()\n", + "\n", + "if hasattr(arena, 'compiler'):\n", + " arena.compiler.degree = False # MuJoCo dev (next release).\n", + "else:\n", + " arena.degree = False # MuJoCo release\n", + "\n", + "# Make arena with textured floor.\n", + "chequered = arena.add_texture(\n", + " name=\"chequered\", type=mj.mjtTexture.mjTEXTURE_2D,\n", + " builtin=mj.mjtBuiltin.mjBUILTIN_CHECKER,\n", + " width=300, height=300, rgb1=[.2, .3, .4], rgb2=[.3, .4, .5])\n", + "grid = arena.add_material(\n", + " name='grid', texrepeat=[5, 5], reflectance=.2\n", + " ).textures[mj.mjtTextureRole.mjTEXROLE_RGB] = 'chequered'\n", + "arena.worldbody.add_geom(\n", + " type=mj.mjtGeom.mjGEOM_PLANE, size=[2, 2, .1], material='grid')\n", + "for x in [-2, 2]:\n", + " arena.worldbody.add_light(pos=[x, -1, 3], dir=[-x, 1, -2])\n", + "\n", + "# Instantiate 6 creatures with 3 to 8 legs.\n", + "creatures = [make_creature(num_legs=num_legs) for num_legs in range(3, 9)]\n", + "\n", + "# Place them on a grid in the arena.\n", + "height = .15\n", + "grid = 5 * BODY_RADIUS\n", + "xpos, ypos, zpos = np.meshgrid([-grid, 0, grid], [0, grid], [height])\n", + "for i, spec in enumerate(creatures):\n", + " # Place spawn sites on a grid.\n", + " spawn_pos = (xpos.flat[i], ypos.flat[i], zpos.flat[i])\n", + " spawn_site = arena.worldbody.add_site(pos=spawn_pos, group=3)\n", + " # Attach to the arena at the spawn sites, with a free joint.\n", + " spawn_body = spawn_site.attach_body(spec.worldbody, '', '-' + str(i))\n", + " spawn_body.add_freejoint()\n", + "\n", + "# Instantiate the physics and render.\n", + "model = arena.compile()\n", + "render(model)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tq5mKlc_LdEL" + }, + "source": [ + "Multi-legged creatures, ready to roam! Let's inject some controls and watch them move. We'll generate a sinusoidal open-loop control signal of fixed frequency and random phase, recording both video frames and the horizontal positions of the torso geoms, in order to plot the movement trajectories." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "i37FpwCeLdEL" + }, + "outputs": [], + "source": [ + "#@title Video of the movement {vertical-output: true}\n", + "\n", + "data = mj.MjData(model)\n", + "duration = 10 # (Seconds)\n", + "framerate = 30 # (Hz)\n", + "video = []\n", + "pos_x = []\n", + "pos_y = []\n", + "geoms = arena.worldbody.find_all(mj.mjtObj.mjOBJ_GEOM)\n", + "torsos_data = [data.bind(geom) for geom in geoms if 'torso' in geom.name]\n", + "torsos_model = [model.bind(geom) for geom in geoms if 'torso' in geom.name]\n", + "actuators = [data.bind(actuator) for actuator in arena.actuators]\n", + "\n", + "# Control signal frequency, phase, amplitude.\n", + "freq = 5\n", + "phase = 2 * np.pi * random_state.rand(len(arena.actuators))\n", + "amp = 0.9\n", + "\n", + "# Simulate, saving video frames and torso locations.\n", + "mj.mj_resetData(model, data)\n", + "with mj.Renderer(model) as renderer:\n", + " while data.time < duration:\n", + " # Inject controls and step the physics.\n", + " for i, actuator in enumerate(actuators):\n", + " actuator.ctrl = amp * np.sin(freq * data.time + phase[i])\n", + " mj.mj_step(model, data)\n", + "\n", + " # Save torso horizontal positions using name indexing.\n", + " pos_x.append([torso.xpos[0] for torso in torsos_data])\n", + " pos_y.append([torso.xpos[1] for torso in torsos_data])\n", + "\n", + " # Save video frames.\n", + " if len(video) < data.time * framerate:\n", + " renderer.update_scene(data)\n", + " pixels = renderer.render()\n", + " video.append(pixels.copy())\n", + "\n", + "media.show_video(video, fps=framerate)" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "uFrvaih4LdEL" + }, + "outputs": [], + "source": [ + "#@title Movement trajectories {vertical-output: true}\n", + "\n", + "creature_colors = [torso.rgba[:3] for torso in torsos_model]\n", + "fig, ax = plt.subplots(figsize=(4, 4))\n", + "ax.set_prop_cycle(color=creature_colors)\n", + "_ = ax.plot(pos_x, pos_y, linewidth=4)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FMW4l-fSLdEL" + }, + "source": [ + "The plot above shows the corresponding movement trajectories of creature positions. Note how `mjSpec` attribute `id` were used to access both `xpos` and `rgba` values. This attribute is valid only after a model is compiled." + ] } ], "metadata": { From 8268c6d1ff5603f055c5356c86bd0f62c024b0a7 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 19 May 2025 08:58:45 -0700 Subject: [PATCH 147/191] Add `mjv_copyData` PiperOrigin-RevId: 760642972 Change-Id: I16227fd91bf42618fda684b15b3650758ae29b13 --- doc/APIreference/functions.rst | 9 ++ doc/includes/references.h | 1 + include/mujoco/mjxmacro.h | 206 +++++++++++++------------- include/mujoco/mujoco.h | 3 + python/mujoco/introspect/functions.py | 28 ++++ src/engine/engine_io.c | 63 ++++++-- src/engine/engine_io.h | 3 + test/engine/engine_io_test.cc | 33 ++++- unity/Runtime/Bindings/MjBindings.cs | 3 + 9 files changed, 236 insertions(+), 113 deletions(-) diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index a86386d5..49656292 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -1364,6 +1364,15 @@ If the model buffer is unallocated the initial configuration will not be set. Copy mjData. m is only required to contain the size fields from MJMODEL_INTS. +.. _mjv_copyData: + +`mjv_copyData <#mjv_copyData>`__ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. mujoco-include:: mjv_copyData + +Copy mjData, skip large arrays not required for visualization. + .. _mj_resetData: `mj_resetData <#mj_resetData>`__ diff --git a/doc/includes/references.h b/doc/includes/references.h index 59320911..f55992d0 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -3283,6 +3283,7 @@ void mj_deleteModel(mjModel* m); int mj_sizeModel(const mjModel* m); mjData* mj_makeData(const mjModel* m); mjData* mj_copyData(mjData* dest, const mjModel* m, const mjData* src); +mjData* mjv_copyData(mjData* dest, const mjModel* m, const mjData* src); void mj_resetData(const mjModel* m, mjData* d); void mj_resetDataDebug(const mjModel* m, mjData* d, unsigned char debug_value); void mj_resetDataKeyframe(const mjModel* m, mjData* d, int key); diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index ecd40310..9ba47b19 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -268,7 +268,7 @@ X ( mjtNum, geom_friction, ngeom, 3 ) \ X ( mjtNum, geom_margin, ngeom, 1 ) \ X ( mjtNum, geom_gap, ngeom, 1 ) \ - X ( mjtNum, geom_fluid, ngeom, mjNFLUID ) \ + XNV ( mjtNum, geom_fluid, ngeom, mjNFLUID ) \ X ( mjtNum, geom_user, ngeom, MJ_M(nuser_geom) ) \ XMJV( float, geom_rgba, ngeom, 4 ) \ XMJV( int, site_type, nsite, 1 ) \ @@ -401,15 +401,15 @@ XNV ( int, mesh_facetexcoord, nmeshface, 3 ) \ XNV ( int, mesh_graph, nmeshgraph, 1 ) \ XMJV( int, mesh_pathadr, nmesh, 1 ) \ - X ( int, mesh_polynum, nmesh, 1 ) \ - X ( int, mesh_polyadr, nmesh, 1 ) \ - X ( mjtNum, mesh_polynormal, nmeshpoly, 3 ) \ - X ( int, mesh_polyvertadr, nmeshpoly, 1 ) \ - X ( int, mesh_polyvertnum, nmeshpoly, 1 ) \ - X ( int, mesh_polyvert, nmeshpolyvert, 1 ) \ - X ( int, mesh_polymapadr, nmeshvert, 1 ) \ - X ( int, mesh_polymapnum, nmeshvert, 1 ) \ - X ( int, mesh_polymap, nmeshpolymap, 1 ) \ + XNV ( int, mesh_polynum, nmesh, 1 ) \ + XNV ( int, mesh_polyadr, nmesh, 1 ) \ + XNV ( mjtNum, mesh_polynormal, nmeshpoly, 3 ) \ + XNV ( int, mesh_polyvertadr, nmeshpoly, 1 ) \ + XNV ( int, mesh_polyvertnum, nmeshpoly, 1 ) \ + XNV ( int, mesh_polyvert, nmeshpolyvert, 1 ) \ + XNV ( int, mesh_polymapadr, nmeshvert, 1 ) \ + XNV ( int, mesh_polymapnum, nmeshvert, 1 ) \ + XNV ( int, mesh_polymap, nmeshpolymap, 1 ) \ XMJV( int, skin_matid, nskin, 1 ) \ XMJV( int, skin_group, nskin, 1 ) \ XMJV( float, skin_rgba, nskin, 4 ) \ @@ -655,10 +655,10 @@ X ( int, moment_rowadr, nu, 1 ) \ X ( int, moment_colind, nJmom, 1 ) \ X ( mjtNum, actuator_moment, nJmom, 1 ) \ - X ( mjtNum, crb, nbody, 10 ) \ - X ( mjtNum, qM, nM, 1 ) \ - X ( mjtNum, M, nC, 1 ) \ - X ( mjtNum, qLD, nC, 1 ) \ + XNV ( mjtNum, crb, nbody, 10 ) \ + XNV ( mjtNum, qM, nM, 1 ) \ + XNV ( mjtNum, M, nC, 1 ) \ + XNV ( mjtNum, qLD, nC, 1 ) \ X ( mjtNum, qLDiagInv, nv, 1 ) \ XMJV( mjtNum, bvh_aabb_dyn, nbvhdynamic, 6 ) \ XMJV( mjtByte, bvh_active, nbvh, 1 ) \ @@ -675,23 +675,23 @@ X ( mjtNum, qfrc_passive, nv, 1 ) \ X ( mjtNum, subtree_linvel, nbody, 3 ) \ X ( mjtNum, subtree_angmom, nbody, 3 ) \ - X ( mjtNum, qH, nC, 1 ) \ + XNV ( mjtNum, qH, nC, 1 ) \ X ( mjtNum, qHDiagInv, nv, 1 ) \ - X ( int, B_rownnz, nbody, 1 ) \ - X ( int, B_rowadr, nbody, 1 ) \ - X ( int, B_colind, nB, 1 ) \ - X ( int, M_rownnz, nv, 1 ) \ - X ( int, M_rowadr, nv, 1 ) \ - X ( int, M_colind, nC, 1 ) \ - X ( int, mapM2M, nC, 1 ) \ - X ( int, D_rownnz, nv, 1 ) \ - X ( int, D_rowadr, nv, 1 ) \ - X ( int, D_diag, nv, 1 ) \ - X ( int, D_colind, nD, 1 ) \ - X ( int, mapM2D, nD, 1 ) \ - X ( int, mapD2M, nM, 1 ) \ - X ( mjtNum, qDeriv, nD, 1 ) \ - X ( mjtNum, qLU, nD, 1 ) \ + XNV ( int, B_rownnz, nbody, 1 ) \ + XNV ( int, B_rowadr, nbody, 1 ) \ + XNV ( int, B_colind, nB, 1 ) \ + XNV ( int, M_rownnz, nv, 1 ) \ + XNV ( int, M_rowadr, nv, 1 ) \ + XNV ( int, M_colind, nC, 1 ) \ + XNV ( int, mapM2M, nC, 1 ) \ + XNV ( int, D_rownnz, nv, 1 ) \ + XNV ( int, D_rowadr, nv, 1 ) \ + XNV ( int, D_diag, nv, 1 ) \ + XNV ( int, D_colind, nD, 1 ) \ + XNV ( int, mapM2D, nD, 1 ) \ + XNV ( int, mapD2M, nM, 1 ) \ + XNV ( mjtNum, qDeriv, nD, 1 ) \ + XNV ( mjtNum, qLU, nD, 1 ) \ X ( mjtNum, actuator_force, nu, 1 ) \ X ( mjtNum, qfrc_actuator, nv, 1 ) \ X ( mjtNum, qfrc_smooth, nv, 1 ) \ @@ -712,83 +712,83 @@ X( mjContact, contact, MJ_D(ncon), 1 ) // array fields of mjData that are used in the primal problem -#define MJDATA_ARENA_POINTERS_SOLVER \ - X( int, efc_type, MJ_D(nefc), 1 ) \ - X( int, efc_id, MJ_D(nefc), 1 ) \ - X( int, efc_J_rownnz, MJ_D(nefc), 1 ) \ - X( int, efc_J_rowadr, MJ_D(nefc), 1 ) \ - X( int, efc_J_rowsuper, MJ_D(nefc), 1 ) \ - X( int, efc_J_colind, MJ_D(nJ), 1 ) \ - X( int, efc_JT_rownnz, MJ_M(nv), 1 ) \ - X( int, efc_JT_rowadr, MJ_M(nv), 1 ) \ - X( int, efc_JT_rowsuper, MJ_M(nv), 1 ) \ - X( int, efc_JT_colind, MJ_D(nJ), 1 ) \ - X( mjtNum, efc_J, MJ_D(nJ), 1 ) \ - X( mjtNum, efc_JT, MJ_D(nJ), 1 ) \ - X( mjtNum, efc_pos, MJ_D(nefc), 1 ) \ - X( mjtNum, efc_margin, MJ_D(nefc), 1 ) \ - X( mjtNum, efc_frictionloss, MJ_D(nefc), 1 ) \ - X( mjtNum, efc_diagApprox, MJ_D(nefc), 1 ) \ - X( mjtNum, efc_KBIP, MJ_D(nefc), 4 ) \ - X( mjtNum, efc_D, MJ_D(nefc), 1 ) \ - X( mjtNum, efc_R, MJ_D(nefc), 1 ) \ - X( int, tendon_efcadr, MJ_M(ntendon), 1 ) \ - X( mjtNum, efc_vel, MJ_D(nefc), 1 ) \ - X( mjtNum, efc_aref, MJ_D(nefc), 1 ) \ - X( mjtNum, efc_b, MJ_D(nefc), 1 ) \ - X( mjtNum, efc_force, MJ_D(nefc), 1 ) \ - X( int, efc_state, MJ_D(nefc), 1 ) +#define MJDATA_ARENA_POINTERS_SOLVER \ + X ( int, efc_type, MJ_D(nefc), 1 ) \ + X ( int, efc_id, MJ_D(nefc), 1 ) \ + XNV( int, efc_J_rownnz, MJ_D(nefc), 1 ) \ + XNV( int, efc_J_rowadr, MJ_D(nefc), 1 ) \ + XNV( int, efc_J_rowsuper, MJ_D(nefc), 1 ) \ + XNV( int, efc_J_colind, MJ_D(nJ), 1 ) \ + XNV( int, efc_JT_rownnz, MJ_M(nv), 1 ) \ + XNV( int, efc_JT_rowadr, MJ_M(nv), 1 ) \ + XNV( int, efc_JT_rowsuper, MJ_M(nv), 1 ) \ + XNV( int, efc_JT_colind, MJ_D(nJ), 1 ) \ + XNV( mjtNum, efc_J, MJ_D(nJ), 1 ) \ + XNV( mjtNum, efc_JT, MJ_D(nJ), 1 ) \ + X ( mjtNum, efc_pos, MJ_D(nefc), 1 ) \ + X ( mjtNum, efc_margin, MJ_D(nefc), 1 ) \ + X ( mjtNum, efc_frictionloss, MJ_D(nefc), 1 ) \ + X ( mjtNum, efc_diagApprox, MJ_D(nefc), 1 ) \ + X ( mjtNum, efc_KBIP, MJ_D(nefc), 4 ) \ + X ( mjtNum, efc_D, MJ_D(nefc), 1 ) \ + X ( mjtNum, efc_R, MJ_D(nefc), 1 ) \ + X ( int, tendon_efcadr, MJ_M(ntendon), 1 ) \ + X ( mjtNum, efc_vel, MJ_D(nefc), 1 ) \ + X ( mjtNum, efc_aref, MJ_D(nefc), 1 ) \ + X ( mjtNum, efc_b, MJ_D(nefc), 1 ) \ + X ( mjtNum, efc_force, MJ_D(nefc), 1 ) \ + X ( int, efc_state, MJ_D(nefc), 1 ) // array fields of mjData that are used in the dual problem -#define MJDATA_ARENA_POINTERS_DUAL \ - X( int, efc_AR_rownnz, MJ_D(nefc), 1 ) \ - X( int, efc_AR_rowadr, MJ_D(nefc), 1 ) \ - X( int, efc_AR_colind, MJ_D(nA), 1 ) \ - X( mjtNum, efc_AR, MJ_D(nA), 1 ) +#define MJDATA_ARENA_POINTERS_DUAL \ + XNV( int, efc_AR_rownnz, MJ_D(nefc), 1 ) \ + XNV( int, efc_AR_rowadr, MJ_D(nefc), 1 ) \ + XNV( int, efc_AR_colind, MJ_D(nA), 1 ) \ + XNV( mjtNum, efc_AR, MJ_D(nA), 1 ) // array fields of mjData that are used for constraint islands -#define MJDATA_ARENA_POINTERS_ISLAND \ - X( int, dof_island, MJ_M(nv), 1 ) \ - X( int, island_nv, MJ_D(nisland), 1 ) \ - X( int, island_idofadr, MJ_D(nisland), 1 ) \ - X( int, island_dofadr, MJ_D(nisland), 1 ) \ - X( int, map_dof2idof, MJ_M(nv), 1 ) \ - X( int, map_idof2dof, MJ_M(nv), 1 ) \ - X( mjtNum, ifrc_smooth, MJ_D(nidof), 1 ) \ - X( mjtNum, iacc_smooth, MJ_D(nidof), 1 ) \ - X( int, iM_rownnz, MJ_D(nidof), 1 ) \ - X( int, iM_rowadr, MJ_D(nidof), 1 ) \ - X( int, iM_colind, MJ_M(nC), 1 ) \ - X( mjtNum, iM, MJ_M(nC), 1 ) \ - X( mjtNum, iLD, MJ_M(nC), 1 ) \ - X( mjtNum, iLDiagInv, MJ_D(nidof), 1 ) \ - X( mjtNum, iacc, MJ_D(nidof), 1 ) \ - X( int, efc_island, MJ_D(nefc), 1 ) \ - X( int, island_ne, MJ_D(nisland), 1 ) \ - X( int, island_nf, MJ_D(nisland), 1 ) \ - X( int, island_nefc, MJ_D(nisland), 1 ) \ - X( int, island_iefcadr, MJ_D(nisland), 1 ) \ - X( int, map_efc2iefc, MJ_D(nefc), 1 ) \ - X( int, map_iefc2efc, MJ_D(nefc), 1 ) \ - X( int, iefc_type, MJ_D(nefc), 1 ) \ - X( int, iefc_id, MJ_D(nefc), 1 ) \ - X( int, iefc_J_rownnz, MJ_D(nefc), 1 ) \ - X( int, iefc_J_rowadr, MJ_D(nefc), 1 ) \ - X( int, iefc_J_rowsuper, MJ_D(nefc), 1 ) \ - X( int, iefc_J_colind, MJ_D(nJ), 1 ) \ - X( int, iefc_JT_rownnz, MJ_D(nidof), 1 ) \ - X( int, iefc_JT_rowadr, MJ_D(nidof), 1 ) \ - X( int, iefc_JT_rowsuper, MJ_D(nidof), 1 ) \ - X( int, iefc_JT_colind, MJ_D(nJ), 1 ) \ - X( mjtNum, iefc_J, MJ_D(nJ), 1 ) \ - X( mjtNum, iefc_JT, MJ_D(nJ), 1 ) \ - X( mjtNum, iefc_frictionloss, MJ_D(nefc), 1 ) \ - X( mjtNum, iefc_D, MJ_D(nefc), 1 ) \ - X( mjtNum, iefc_R, MJ_D(nefc), 1 ) \ - X( mjtNum, iefc_aref, MJ_D(nefc), 1 ) \ - X( int, iefc_state, MJ_D(nefc), 1 ) \ - X( mjtNum, iefc_force, MJ_D(nefc), 1 ) \ - X( mjtNum, ifrc_constraint, MJ_D(nidof), 1 ) +#define MJDATA_ARENA_POINTERS_ISLAND \ + X ( int, dof_island, MJ_M(nv), 1 ) \ + X ( int, island_nv, MJ_D(nisland), 1 ) \ + X ( int, island_idofadr, MJ_D(nisland), 1 ) \ + X ( int, island_dofadr, MJ_D(nisland), 1 ) \ + X ( int, map_dof2idof, MJ_M(nv), 1 ) \ + X ( int, map_idof2dof, MJ_M(nv), 1 ) \ + X ( mjtNum, ifrc_smooth, MJ_D(nidof), 1 ) \ + X ( mjtNum, iacc_smooth, MJ_D(nidof), 1 ) \ + XNV( int, iM_rownnz, MJ_D(nidof), 1 ) \ + XNV( int, iM_rowadr, MJ_D(nidof), 1 ) \ + XNV( int, iM_colind, MJ_M(nC), 1 ) \ + XNV( mjtNum, iM, MJ_M(nC), 1 ) \ + XNV( mjtNum, iLD, MJ_M(nC), 1 ) \ + X ( mjtNum, iLDiagInv, MJ_D(nidof), 1 ) \ + X ( mjtNum, iacc, MJ_D(nidof), 1 ) \ + X ( int, efc_island, MJ_D(nefc), 1 ) \ + X ( int, island_ne, MJ_D(nisland), 1 ) \ + X ( int, island_nf, MJ_D(nisland), 1 ) \ + X ( int, island_nefc, MJ_D(nisland), 1 ) \ + X ( int, island_iefcadr, MJ_D(nisland), 1 ) \ + X ( int, map_efc2iefc, MJ_D(nefc), 1 ) \ + X ( int, map_iefc2efc, MJ_D(nefc), 1 ) \ + X ( int, iefc_type, MJ_D(nefc), 1 ) \ + X ( int, iefc_id, MJ_D(nefc), 1 ) \ + XNV( int, iefc_J_rownnz, MJ_D(nefc), 1 ) \ + XNV( int, iefc_J_rowadr, MJ_D(nefc), 1 ) \ + XNV( int, iefc_J_rowsuper, MJ_D(nefc), 1 ) \ + XNV( int, iefc_J_colind, MJ_D(nJ), 1 ) \ + XNV( int, iefc_JT_rownnz, MJ_D(nidof), 1 ) \ + XNV( int, iefc_JT_rowadr, MJ_D(nidof), 1 ) \ + XNV( int, iefc_JT_rowsuper, MJ_D(nidof), 1 ) \ + XNV( int, iefc_JT_colind, MJ_D(nJ), 1 ) \ + XNV( mjtNum, iefc_J, MJ_D(nJ), 1 ) \ + XNV( mjtNum, iefc_JT, MJ_D(nJ), 1 ) \ + X ( mjtNum, iefc_frictionloss, MJ_D(nefc), 1 ) \ + X ( mjtNum, iefc_D, MJ_D(nefc), 1 ) \ + X ( mjtNum, iefc_R, MJ_D(nefc), 1 ) \ + X ( mjtNum, iefc_aref, MJ_D(nefc), 1 ) \ + X ( int, iefc_state, MJ_D(nefc), 1 ) \ + X ( mjtNum, iefc_force, MJ_D(nefc), 1 ) \ + X ( mjtNum, ifrc_constraint, MJ_D(nidof), 1 ) // array fields of mjData that live in d->arena #define MJDATA_ARENA_POINTERS \ diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index 784d1ab8..8d8cb8c2 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -187,6 +187,9 @@ MJAPI mjData* mj_makeData(const mjModel* m); // m is only required to contain the size fields from MJMODEL_INTS. MJAPI mjData* mj_copyData(mjData* dest, const mjModel* m, const mjData* src); +// Copy mjData, skip large arrays not required for visualization. +MJAPI mjData* mjv_copyData(mjData* dest, const mjModel* m, const mjData* src); + // Reset data to defaults. MJAPI void mj_resetData(const mjModel* m, mjData* d); diff --git a/python/mujoco/introspect/functions.py b/python/mujoco/introspect/functions.py index ef8bcd58..1f495c03 100644 --- a/python/mujoco/introspect/functions.py +++ b/python/mujoco/introspect/functions.py @@ -745,6 +745,34 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), doc='Copy mjData. m is only required to contain the size fields from MJMODEL_INTS.', # pylint: disable=line-too-long )), + ('mjv_copyData', + FunctionDecl( + name='mjv_copyData', + return_type=PointerType( + inner_type=ValueType(name='mjData'), + ), + parameters=( + FunctionParameterDecl( + name='dest', + type=PointerType( + inner_type=ValueType(name='mjData'), + ), + ), + FunctionParameterDecl( + name='m', + type=PointerType( + inner_type=ValueType(name='mjModel', is_const=True), + ), + ), + FunctionParameterDecl( + name='src', + type=PointerType( + inner_type=ValueType(name='mjData', is_const=True), + ), + ), + ), + doc='Copy mjData, skip large arrays not required for visualization.', + )), ('mj_resetData', FunctionDecl( name='mj_resetData', diff --git a/src/engine/engine_io.c b/src/engine/engine_io.c index d090f229..49325b30 100644 --- a/src/engine/engine_io.c +++ b/src/engine/engine_io.c @@ -1415,8 +1415,9 @@ mjData* mj_makeData(const mjModel* m) { -// copy mjData, if dest==NULL create new data -mjData* mj_copyData(mjData* dest, const mjModel* m, const mjData* src) { +// copy mjData, if dest==NULL create new data; +// flg_all 1: copy all fields, 0: skip fields not required for visualization +mjData* mj_copyDataVisual(mjData* dest, const mjModel* m, const mjData* src, int flg_all) { void* save_buffer; void* save_arena; @@ -1461,10 +1462,25 @@ mjData* mj_copyData(mjData* dest, const mjModel* m, const mjData* src) { // copy buffer { MJDATA_POINTERS_PREAMBLE(m) - #define X(type, name, nr, nc) \ - memcpy((char*)dest->name, (const char*)src->name, sizeof(type)*(m->nr)*nc); - MJDATA_POINTERS - #undef X + if (flg_all) { + #define X(type, name, nr, nc) \ + memcpy((char*)dest->name, (const char*)src->name, sizeof(type)*(m->nr)*nc); + MJDATA_POINTERS + #undef X + } else { + // redefine XNV to nothing + #undef XNV + #define XNV(type, name, nr, nc) + + #define X(type, name, nr, nc) \ + memcpy((char*)dest->name, (const char*)src->name, sizeof(type)*(m->nr)*nc); + MJDATA_POINTERS + #undef X + + // redefine XNV to be the same as X + #undef XNV + #define XNV X + } } @@ -1474,7 +1490,8 @@ mjData* mj_copyData(mjData* dest, const mjModel* m, const mjData* src) { #undef MJ_M #define MJ_M(n) (m->n) - #define X(type, name, nr, nc) \ + if (flg_all) { + #define X(type, name, nr, nc) \ if (src->name) { \ dest->name = (type*)((char*)dest->arena + PTRDIFF(src->name, src->arena)); \ ASAN_UNPOISON_MEMORY_REGION(dest->name, sizeof(type) * nr * nc); \ @@ -1482,8 +1499,28 @@ mjData* mj_copyData(mjData* dest, const mjModel* m, const mjData* src) { } else { \ dest->name = NULL; \ } - MJDATA_ARENA_POINTERS - #undef X + MJDATA_ARENA_POINTERS + #undef X + } else { + // redefine XNV to nothing + #undef XNV + #define XNV(type, name, nr, nc) + + #define X(type, name, nr, nc) \ + if (src->name) { \ + dest->name = (type*)((char*)dest->arena + PTRDIFF(src->name, src->arena)); \ + ASAN_UNPOISON_MEMORY_REGION(dest->name, sizeof(type) * nr * nc); \ + memcpy((char*)dest->name, (const char*)src->name, sizeof(type) * nr * nc); \ + } else { \ + dest->name = NULL; \ + } + MJDATA_ARENA_POINTERS + #undef X + + // redefine XNV to be the same as X + #undef XNV + #define XNV X + } #undef MJ_M #define MJ_M(n) n @@ -1515,6 +1552,14 @@ mjData* mj_copyData(mjData* dest, const mjModel* m, const mjData* src) { } +mjData* mj_copyData(mjData* dest, const mjModel* m, const mjData* src) { + return mj_copyDataVisual(dest, m, src, /*flg_all=*/1); +} + + +mjData* mjv_copyData(mjData* dest, const mjModel* m, const mjData* src) { + return mj_copyDataVisual(dest, m, src, /*flg_all=*/0); +} static void maybe_lock_alloc_mutex(mjData* d) { if (d->threadpool != 0) { diff --git a/src/engine/engine_io.h b/src/engine/engine_io.h index 809d9e83..bdf0e2ce 100644 --- a/src/engine/engine_io.h +++ b/src/engine/engine_io.h @@ -99,6 +99,9 @@ MJAPI void mj_makeRawData(mjData** dest, const mjModel* m); // m is only required to contain the size fields from MJMODEL_INTS. MJAPI mjData* mj_copyData(mjData* dest, const mjModel* m, const mjData* src); +// copy mjData, skip large arrays not required for abstract visualization +MJAPI mjData* mjv_copyData(mjData* dest, const mjModel* m, const mjData* src); + // set data to defaults MJAPI void mj_resetData(const mjModel* m, mjData* d); diff --git a/test/engine/engine_io_test.cc b/test/engine/engine_io_test.cc index 470f6f02..72e942d1 100644 --- a/test/engine/engine_io_test.cc +++ b/test/engine/engine_io_test.cc @@ -226,11 +226,42 @@ TEST_F(EngineIoTest, MjvCopyModel) { EXPECT_FLOAT_EQ(model2->mesh_vert[0], 0.1); // unchanged EXPECT_FLOAT_EQ(model2->geom_rgba[0], 0.4); - // mj_deleteData(data); mj_deleteModel(model2); mj_deleteModel(model1); } +TEST_F(EngineIoTest, MjvCopyData) { + static constexpr char xml[] = R"( + + + + + + + + + + )"; + char error[1024]; + mjModel* model = LoadModelFromString(xml, error, sizeof(error)); + ASSERT_THAT(model, NotNull()) << error; + + mjData* data1 = mj_makeData(model); + mj_forward(model, data1); + EXPECT_THAT(data1->efc_J, NotNull()); + + mjData* data2 = mj_copyData(nullptr, model, data1); + EXPECT_THAT(data2->efc_J, NotNull()); + + mj_deleteData(data2); + data2 = mjv_copyData(nullptr, model, data1); + EXPECT_THAT(data2->efc_J, IsNull()); + + mj_deleteData(data2); + mj_deleteData(data1); + mj_deleteModel(model); +} + using ValidateReferencesTest = MujocoTest; TEST_F(ValidateReferencesTest, BodyReferences) { diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index d0dd61aa..cdeeca54 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -6622,6 +6622,9 @@ public static unsafe extern mjData_* mj_makeData(mjModel_* m); [DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] public static unsafe extern mjData_* mj_copyData(mjData_* dest, mjModel_* m, mjData_* src); +[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] +public static unsafe extern mjData_* mjv_copyData(mjData_* dest, mjModel_* m, mjData_* src); + [DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] public static unsafe extern void mj_resetData(mjModel_* m, mjData_* d); From ced630181dfbe83c1b5ecb0d1e71a083920d4264 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 19 May 2025 09:50:49 -0700 Subject: [PATCH 148/191] Replace `mjv_sceneState` and related code with `mjv_copy{Model,Data}` PiperOrigin-RevId: 760662776 Change-Id: I512bda9187fbc19ade6924b91d5cb2275078de42 --- doc/APIreference/APItypes.rst | 11 - doc/APIreference/functions.rst | 63 - doc/changelog.rst | 6 + doc/includes/references.h | 299 ----- include/mujoco/mjvisualize.h | 288 ----- include/mujoco/mjxmacro.h | 574 +++++---- include/mujoco/mujoco.h | 29 - python/mujoco/introspect/functions.py | 208 ---- python/mujoco/introspect/structs.py | 1645 ------------------------- simulate/simulate.cc | 131 +- simulate/simulate.h | 7 +- src/engine/CMakeLists.txt | 2 - src/engine/engine_vis_state.c | 409 ------ src/engine/engine_vis_state.h | 63 - test/engine/CMakeLists.txt | 7 - test/engine/engine_vis_state_test.cc | 123 -- unity/Runtime/Bindings/MjBindings.cs | 285 ----- 17 files changed, 357 insertions(+), 3793 deletions(-) delete mode 100644 src/engine/engine_vis_state.c delete mode 100644 src/engine/engine_vis_state.h delete mode 100644 test/engine/engine_vis_state_test.cc diff --git a/doc/APIreference/APItypes.rst b/doc/APIreference/APItypes.rst index 205b7762..4ee6367d 100644 --- a/doc/APIreference/APItypes.rst +++ b/doc/APIreference/APItypes.rst @@ -987,17 +987,6 @@ This structure contains everything needed to render the 3D scene in OpenGL. .. mujoco-include:: mjvScene -.. _mjvSceneState: - -mjvSceneState -~~~~~~~~~~~~~ - -This structure contains the portions of :ref:`mjModel` and :ref:`mjData` that are required for -various ``mjv_*`` functions. - -.. mujoco-include:: mjvSceneState - - .. _mjvFigure: mjvFigure diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index 49656292..5b985861 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -2004,15 +2004,6 @@ Rotate 3D vec in horizontal plane by angle between (0,1) and (forward_x,forward_ Move camera with mouse; action is mjtMouse. -.. _mjv_moveCameraFromState: - -`mjv_moveCameraFromState <#mjv_moveCameraFromState>`__ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. mujoco-include:: mjv_moveCameraFromState - -Move camera with mouse given a scene state; action is mjtMouse. - .. _mjv_movePerturb: `mjv_movePerturb <#mjv_movePerturb>`__ @@ -2022,15 +2013,6 @@ Move camera with mouse given a scene state; action is mjtMouse. Move perturb object with mouse; action is mjtMouse. -.. _mjv_movePerturbFromState: - -`mjv_movePerturbFromState <#mjv_movePerturbFromState>`__ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. mujoco-include:: mjv_movePerturbFromState - -Move perturb object with mouse given a scene state; action is mjtMouse. - .. _mjv_moveModel: `mjv_moveModel <#mjv_moveModel>`__ @@ -2173,15 +2155,6 @@ Free abstract scene. Update entire scene given model state. -.. _mjv_updateSceneFromState: - -`mjv_updateSceneFromState <#mjv_updateSceneFromState>`__ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. mujoco-include:: mjv_updateSceneFromState - -Update entire scene from a scene state, return the number of new mjWARN_VGEOMFULL warnings. - .. _mjv_copyModel: `mjv_copyModel <#mjv_copyModel>`__ @@ -2191,42 +2164,6 @@ Update entire scene from a scene state, return the number of new mjWARN_VGEOMFUL Copy mjModel, skip large arrays not required for abstract visualization. -.. _mjv_defaultSceneState: - -`mjv_defaultSceneState <#mjv_defaultSceneState>`__ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. mujoco-include:: mjv_defaultSceneState - -Set default scene state. - -.. _mjv_makeSceneState: - -`mjv_makeSceneState <#mjv_makeSceneState>`__ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. mujoco-include:: mjv_makeSceneState - -Allocate resources and initialize a scene state object. - -.. _mjv_freeSceneState: - -`mjv_freeSceneState <#mjv_freeSceneState>`__ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. mujoco-include:: mjv_freeSceneState - -Free scene state. - -.. _mjv_updateSceneState: - -`mjv_updateSceneState <#mjv_updateSceneState>`__ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. mujoco-include:: mjv_updateSceneState - -Update a scene state from model and data. - .. _mjv_addGeoms: `mjv_addGeoms <#mjv_addGeoms>`__ diff --git a/doc/changelog.rst b/doc/changelog.rst index 4dbaf0b2..4255c8f0 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -10,6 +10,12 @@ General - Refactored island implementation so that island data is memory-contiguous. This speeds up island processing in the solver and clears the way for the addition of the Newton and PGS solvers (currently only CG is supported). +simulate +^^^^^^^^ +- The struct ``mjv_sceneState`` has been removed. This struct was used for partial synchronization of ``mjModel`` and + ``mjData`` when the Python viewer is used in passive mode. This functionality is now provided by :ref:`mjv_copyModel` + and :ref:`mjv_copyData`, which don't copy arrays which are not required for visualization. + Version 3.3.2 (April 28, 2025) ------------------------------ diff --git a/doc/includes/references.h b/doc/includes/references.h index f55992d0..9a973092 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -2965,290 +2965,6 @@ struct mjvFigure_ { // abstract 2D figure passed to OpenGL rendere float yaxisdata[2]; // range of y-axis in data units }; typedef struct mjvFigure_ mjvFigure; -struct mjvSceneState_ { - int nbuffer; // size of the buffer in bytes - void* buffer; // heap-allocated memory for all arrays in this struct - int maxgeom; // maximum number of mjvGeom supported by this state object - mjvScene scratch; // scratch space for vis geoms inserted by the user and plugins - - // fields in mjModel that are necessary to re-render a scene - struct { - int nv; - int nu; - int na; - int nbody; - int nbvh; - int nbvhstatic; - int njnt; - int ngeom; - int nsite; - int ncam; - int nlight; - int nmesh; - int nskin; - int nflex; - int nflexvert; - int nflextexcoord; - int nskinvert; - int nskinface; - int nskinbone; - int nskinbonevert; - int nmat; - int neq; - int ntendon; - int ntree; - int nwrap; - int nsensor; - int nnames; - int npaths; - int nsensordata; - int narena; - - mjOption opt; - mjVisual vis; - mjStatistic stat; - - int* body_parentid; - int* body_rootid; - int* body_weldid; - int* body_mocapid; - int* body_jntnum; - int* body_jntadr; - int* body_dofnum; - int* body_dofadr; - int* body_geomnum; - int* body_geomadr; - mjtNum* body_iquat; - mjtNum* body_mass; - mjtNum* body_inertia; - int* body_bvhadr; - int* body_bvhnum; - - int* bvh_depth; - int* bvh_child; - int* bvh_nodeid; - mjtNum* bvh_aabb; - - int* jnt_type; - int* jnt_bodyid; - int* jnt_group; - - int* geom_type; - int* geom_bodyid; - int* geom_contype; - int* geom_conaffinity; - int* geom_dataid; - int* geom_matid; - int* geom_group; - mjtNum* geom_size; - mjtNum* geom_aabb; - mjtNum* geom_rbound; - float* geom_rgba; - - int* site_type; - int* site_bodyid; - int* site_matid; - int* site_group; - mjtNum* site_size; - float* site_rgba; - - int* cam_orthographic; - mjtNum* cam_fovy; - mjtNum* cam_ipd; - int* cam_resolution; - float* cam_sensorsize; - float* cam_intrinsic; - - mjtByte* light_directional; - mjtByte* light_castshadow; - float* light_bulbradius; - float* light_intensity; - float* light_range; - mjtByte* light_active; - float* light_attenuation; - float* light_cutoff; - float* light_exponent; - float* light_ambient; - float* light_diffuse; - float* light_specular; - - mjtByte* flex_flatskin; - int* flex_dim; - int* flex_matid; - int* flex_group; - int* flex_interp; - int* flex_nodeadr; - int* flex_nodenum; - int* flex_nodebodyid; - int* flex_vertadr; - int* flex_vertnum; - int* flex_elem; - int* flex_elemtexcoord; - int* flex_elemlayer; - int* flex_elemadr; - int* flex_elemnum; - int* flex_elemdataadr; - int* flex_shell; - int* flex_shellnum; - int* flex_shelldataadr; - int* flex_texcoordadr; - int* flex_bvhadr; - int* flex_bvhnum; - mjtByte* flex_centered; - mjtNum* flex_node; - mjtNum* flex_radius; - float* flex_rgba; - float* flex_texcoord; - - int* hfield_pathadr; - - int* mesh_bvhadr; - int* mesh_bvhnum; - int* mesh_texcoordadr; - int* mesh_graphadr; - int* mesh_pathadr; - - int* skin_matid; - int* skin_group; - float* skin_rgba; - float* skin_inflate; - int* skin_vertadr; - int* skin_vertnum; - int* skin_texcoordadr; - int* skin_faceadr; - int* skin_facenum; - int* skin_boneadr; - int* skin_bonenum; - float* skin_vert; - int* skin_face; - int* skin_bonevertadr; - int* skin_bonevertnum; - float* skin_bonebindpos; - float* skin_bonebindquat; - int* skin_bonebodyid; - int* skin_bonevertid; - float* skin_bonevertweight; - int* skin_pathadr; - - int* tex_pathadr; - - int* mat_texid; - mjtByte* mat_texuniform; - float* mat_texrepeat; - float* mat_emission; - float* mat_specular; - float* mat_shininess; - float* mat_reflectance; - float* mat_metallic; - float* mat_roughness; - float* mat_rgba; - - int* eq_type; - int* eq_obj1id; - int* eq_obj2id; - int* eq_objtype; - mjtNum* eq_data; - - int* tendon_num; - int* tendon_matid; - int* tendon_group; - mjtByte* tendon_limited; - mjtByte* tendon_actfrclimited; - mjtNum* tendon_width; - mjtNum* tendon_range; - mjtNum* tendon_actfrcrange; - mjtNum* tendon_stiffness; - mjtNum* tendon_damping; - mjtNum* tendon_frictionloss; - mjtNum* tendon_lengthspring; - float* tendon_rgba; - - int* actuator_trntype; - int* actuator_dyntype; - int* actuator_trnid; - int* actuator_actadr; - int* actuator_actnum; - int* actuator_group; - mjtByte* actuator_ctrllimited; - mjtByte* actuator_actlimited; - mjtNum* actuator_ctrlrange; - mjtNum* actuator_actrange; - mjtNum* actuator_cranklength; - - int* sensor_type; - int* sensor_objid; - int* sensor_adr; - - int* name_bodyadr; - int* name_jntadr; - int* name_geomadr; - int* name_siteadr; - int* name_camadr; - int* name_lightadr; - int* name_eqadr; - int* name_tendonadr; - int* name_actuatoradr; - char* names; - char* paths; - } model; - - // fields in mjData that are necessary to re-render a scene - struct { - mjWarningStat warning[mjNWARNING]; - - int nefc; - int ncon; - int nisland; - - mjtNum time; - - mjtNum* act; - - mjtNum* ctrl; - mjtNum* xfrc_applied; - mjtByte* eq_active; - - mjtNum* sensordata; - - mjtNum* xpos; - mjtNum* xquat; - mjtNum* xmat; - mjtNum* xipos; - mjtNum* ximat; - mjtNum* xanchor; - mjtNum* xaxis; - mjtNum* geom_xpos; - mjtNum* geom_xmat; - mjtNum* site_xpos; - mjtNum* site_xmat; - mjtNum* cam_xpos; - mjtNum* cam_xmat; - mjtNum* light_xpos; - mjtNum* light_xdir; - - mjtNum* subtree_com; - - int* ten_wrapadr; - int* ten_wrapnum; - int* wrap_obj; - mjtNum* ten_length; - mjtNum* wrap_xpos; - - mjtNum* bvh_aabb_dyn; - mjtByte* bvh_active; - int* island_dofadr; - int* dof_island; - int* efc_island; - int* tendon_efcadr; - - mjtNum* flexvert_xpos; - - mjContact* contact; - mjtNum* efc_force; - void* arena; - } data; -}; -typedef struct mjvSceneState_ mjvSceneState; //----------------------------- MJAPI FUNCTIONS -------------------------------- void mj_defaultVFS(mjVFS* vfs); @@ -3437,14 +3153,8 @@ mjtNum mjv_frustumHeight(const mjvScene* scn); void mjv_alignToCamera(mjtNum res[3], const mjtNum vec[3], const mjtNum forward[3]); void mjv_moveCamera(const mjModel* m, int action, mjtNum reldx, mjtNum reldy, const mjvScene* scn, mjvCamera* cam); -void mjv_moveCameraFromState(const mjvSceneState* scnstate, int action, - mjtNum reldx, mjtNum reldy, - const mjvScene* scn, mjvCamera* cam); void mjv_movePerturb(const mjModel* m, const mjData* d, int action, mjtNum reldx, mjtNum reldy, const mjvScene* scn, mjvPerturb* pert); -void mjv_movePerturbFromState(const mjvSceneState* scnstate, int action, - mjtNum reldx, mjtNum reldy, - const mjvScene* scn, mjvPerturb* pert); void mjv_moveModel(const mjModel* m, int action, mjtNum reldx, mjtNum reldy, const mjtNum roomup[3], mjvScene* scn); void mjv_initPerturb(const mjModel* m, mjData* d, const mjvScene* scn, mjvPerturb* pert); @@ -3467,16 +3177,7 @@ void mjv_makeScene(const mjModel* m, mjvScene* scn, int maxgeom); void mjv_freeScene(mjvScene* scn); void mjv_updateScene(const mjModel* m, mjData* d, const mjvOption* opt, const mjvPerturb* pert, mjvCamera* cam, int catmask, mjvScene* scn); -int mjv_updateSceneFromState(const mjvSceneState* scnstate, const mjvOption* opt, - const mjvPerturb* pert, mjvCamera* cam, int catmask, - mjvScene* scn); void mjv_copyModel(mjModel* dest, const mjModel* src); -void mjv_defaultSceneState(mjvSceneState* scnstate); -void mjv_makeSceneState(const mjModel* m, const mjData* d, - mjvSceneState* scnstate, int maxgeom); -void mjv_freeSceneState(mjvSceneState* scnstate); -void mjv_updateSceneState(const mjModel* m, mjData* d, const mjvOption* opt, - mjvSceneState* scnstate); void mjv_addGeoms(const mjModel* m, mjData* d, const mjvOption* opt, const mjvPerturb* pert, int catmask, mjvScene* scn); void mjv_makeLights(const mjModel* m, const mjData* d, mjvScene* scn); diff --git a/include/mujoco/mjvisualize.h b/include/mujoco/mjvisualize.h index 0093216b..560b8f35 100644 --- a/include/mujoco/mjvisualize.h +++ b/include/mujoco/mjvisualize.h @@ -406,292 +406,4 @@ struct mjvFigure_ { // abstract 2D figure passed to OpenGL rendere }; typedef struct mjvFigure_ mjvFigure; - -//---------------------------------- mjvSceneState ------------------------------------------------- - -struct mjvSceneState_ { - int nbuffer; // size of the buffer in bytes - void* buffer; // heap-allocated memory for all arrays in this struct - int maxgeom; // maximum number of mjvGeom supported by this state object - mjvScene scratch; // scratch space for vis geoms inserted by the user and plugins - - // fields in mjModel that are necessary to re-render a scene - struct { - int nv; - int nu; - int na; - int nbody; - int nbvh; - int nbvhstatic; - int njnt; - int ngeom; - int nsite; - int ncam; - int nlight; - int nmesh; - int nskin; - int nflex; - int nflexvert; - int nflextexcoord; - int nskinvert; - int nskinface; - int nskinbone; - int nskinbonevert; - int nmat; - int neq; - int ntendon; - int ntree; - int nwrap; - int nsensor; - int nnames; - int npaths; - int nsensordata; - int narena; - - mjOption opt; - mjVisual vis; - mjStatistic stat; - - int* body_parentid; - int* body_rootid; - int* body_weldid; - int* body_mocapid; - int* body_jntnum; - int* body_jntadr; - int* body_dofnum; - int* body_dofadr; - int* body_geomnum; - int* body_geomadr; - mjtNum* body_iquat; - mjtNum* body_mass; - mjtNum* body_inertia; - int* body_bvhadr; - int* body_bvhnum; - - int* bvh_depth; - int* bvh_child; - int* bvh_nodeid; - mjtNum* bvh_aabb; - - int* jnt_type; - int* jnt_bodyid; - int* jnt_group; - - int* geom_type; - int* geom_bodyid; - int* geom_contype; - int* geom_conaffinity; - int* geom_dataid; - int* geom_matid; - int* geom_group; - mjtNum* geom_size; - mjtNum* geom_aabb; - mjtNum* geom_rbound; - float* geom_rgba; - - int* site_type; - int* site_bodyid; - int* site_matid; - int* site_group; - mjtNum* site_size; - float* site_rgba; - - int* cam_orthographic; - mjtNum* cam_fovy; - mjtNum* cam_ipd; - int* cam_resolution; - float* cam_sensorsize; - float* cam_intrinsic; - - mjtByte* light_directional; - mjtByte* light_castshadow; - float* light_bulbradius; - float* light_intensity; - float* light_range; - mjtByte* light_active; - float* light_attenuation; - float* light_cutoff; - float* light_exponent; - float* light_ambient; - float* light_diffuse; - float* light_specular; - - mjtByte* flex_flatskin; - int* flex_dim; - int* flex_matid; - int* flex_group; - int* flex_interp; - int* flex_nodeadr; - int* flex_nodenum; - int* flex_nodebodyid; - int* flex_vertadr; - int* flex_vertnum; - int* flex_elem; - int* flex_elemtexcoord; - int* flex_elemlayer; - int* flex_elemadr; - int* flex_elemnum; - int* flex_elemdataadr; - int* flex_shell; - int* flex_shellnum; - int* flex_shelldataadr; - int* flex_texcoordadr; - int* flex_bvhadr; - int* flex_bvhnum; - mjtByte* flex_centered; - mjtNum* flex_node; - mjtNum* flex_radius; - float* flex_rgba; - float* flex_texcoord; - - int* hfield_pathadr; - - int* mesh_bvhadr; - int* mesh_bvhnum; - int* mesh_texcoordadr; - int* mesh_graphadr; - int* mesh_pathadr; - - int* skin_matid; - int* skin_group; - float* skin_rgba; - float* skin_inflate; - int* skin_vertadr; - int* skin_vertnum; - int* skin_texcoordadr; - int* skin_faceadr; - int* skin_facenum; - int* skin_boneadr; - int* skin_bonenum; - float* skin_vert; - int* skin_face; - int* skin_bonevertadr; - int* skin_bonevertnum; - float* skin_bonebindpos; - float* skin_bonebindquat; - int* skin_bonebodyid; - int* skin_bonevertid; - float* skin_bonevertweight; - int* skin_pathadr; - - int* tex_pathadr; - - int* mat_texid; - mjtByte* mat_texuniform; - float* mat_texrepeat; - float* mat_emission; - float* mat_specular; - float* mat_shininess; - float* mat_reflectance; - float* mat_metallic; - float* mat_roughness; - float* mat_rgba; - - int* eq_type; - int* eq_obj1id; - int* eq_obj2id; - int* eq_objtype; - mjtNum* eq_data; - - int* tendon_num; - int* tendon_matid; - int* tendon_group; - mjtByte* tendon_limited; - mjtByte* tendon_actfrclimited; - mjtNum* tendon_width; - mjtNum* tendon_range; - mjtNum* tendon_actfrcrange; - mjtNum* tendon_stiffness; - mjtNum* tendon_damping; - mjtNum* tendon_frictionloss; - mjtNum* tendon_lengthspring; - float* tendon_rgba; - - int* actuator_trntype; - int* actuator_dyntype; - int* actuator_trnid; - int* actuator_actadr; - int* actuator_actnum; - int* actuator_group; - mjtByte* actuator_ctrllimited; - mjtByte* actuator_actlimited; - mjtNum* actuator_ctrlrange; - mjtNum* actuator_actrange; - mjtNum* actuator_cranklength; - - int* sensor_type; - int* sensor_objid; - int* sensor_adr; - - int* name_bodyadr; - int* name_jntadr; - int* name_geomadr; - int* name_siteadr; - int* name_camadr; - int* name_lightadr; - int* name_eqadr; - int* name_tendonadr; - int* name_actuatoradr; - char* names; - char* paths; - } model; - - // fields in mjData that are necessary to re-render a scene - struct { - mjWarningStat warning[mjNWARNING]; - - int nefc; - int ncon; - int nisland; - - mjtNum time; - - mjtNum* act; - - mjtNum* ctrl; - mjtNum* xfrc_applied; - mjtByte* eq_active; - - mjtNum* sensordata; - - mjtNum* xpos; - mjtNum* xquat; - mjtNum* xmat; - mjtNum* xipos; - mjtNum* ximat; - mjtNum* xanchor; - mjtNum* xaxis; - mjtNum* geom_xpos; - mjtNum* geom_xmat; - mjtNum* site_xpos; - mjtNum* site_xmat; - mjtNum* cam_xpos; - mjtNum* cam_xmat; - mjtNum* light_xpos; - mjtNum* light_xdir; - - mjtNum* subtree_com; - - int* ten_wrapadr; - int* ten_wrapnum; - int* wrap_obj; - mjtNum* ten_length; - mjtNum* wrap_xpos; - - mjtNum* bvh_aabb_dyn; - mjtByte* bvh_active; - int* island_dofadr; - int* dof_island; - int* efc_island; - int* tendon_efcadr; - - mjtNum* flexvert_xpos; - - mjContact* contact; - mjtNum* efc_force; - void* arena; - } data; -}; -typedef struct mjvSceneState_ mjvSceneState; - #endif // MUJOCO_MJVISUALIZE_H_ diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index 9ba47b19..5ffe5764 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -66,92 +66,92 @@ //-------------------------------- mjModel --------------------------------------------------------- // int fields of mjModel -#define MJMODEL_INTS \ - X ( nq ) \ - XMJV( nv ) \ - XMJV( nu ) \ - XMJV( na ) \ - XMJV( nbody ) \ - XMJV( nbvh ) \ - XMJV( nbvhstatic ) \ - X ( nbvhdynamic ) \ - XMJV( njnt ) \ - XMJV( ngeom ) \ - XMJV( nsite ) \ - XMJV( ncam ) \ - XMJV( nlight ) \ - XMJV( nflex ) \ - X ( nflexnode ) \ - XMJV( nflexvert ) \ - X ( nflexedge ) \ - X ( nflexelem ) \ - X ( nflexelemdata ) \ - X ( nflexelemedge ) \ - X ( nflexshelldata ) \ - X ( nflexevpair ) \ - XMJV( nflextexcoord ) \ - XMJV( nmesh ) \ - X ( nmeshvert ) \ - X ( nmeshnormal ) \ - X ( nmeshtexcoord ) \ - X ( nmeshface ) \ - X ( nmeshgraph ) \ - X ( nmeshpoly ) \ - X ( nmeshpolyvert ) \ - X ( nmeshpolymap ) \ - XMJV( nskin ) \ - XMJV( nskinvert ) \ - X ( nskintexvert ) \ - XMJV( nskinface ) \ - XMJV( nskinbone ) \ - XMJV( nskinbonevert ) \ - X ( nhfield ) \ - X ( nhfielddata ) \ - X ( ntex ) \ - X ( ntexdata ) \ - XMJV( nmat ) \ - X ( npair ) \ - X ( nexclude ) \ - XMJV( neq ) \ - XMJV( ntendon ) \ - XMJV( nwrap ) \ - XMJV( nsensor ) \ - X ( nnumeric ) \ - X ( nnumericdata ) \ - X ( ntext ) \ - X ( ntextdata ) \ - X ( ntuple ) \ - X ( ntupledata ) \ - X ( nkey ) \ - X ( nmocap ) \ - X ( nplugin ) \ - X ( npluginattr ) \ - X ( nuser_body ) \ - X ( nuser_jnt ) \ - X ( nuser_geom ) \ - X ( nuser_site ) \ - X ( nuser_cam ) \ - X ( nuser_tendon ) \ - X ( nuser_actuator ) \ - X ( nuser_sensor ) \ - XMJV( nnames ) \ - XMJV( npaths ) \ - X ( nnames_map ) \ - X ( nM ) \ - X ( nB ) \ - X ( nC ) \ - X ( nD ) \ - X ( nJmom ) \ - XMJV( ntree ) \ - X ( ngravcomp ) \ - X ( nemax ) \ - X ( njmax ) \ - X ( nconmax ) \ - X ( nuserdata ) \ - XMJV( nsensordata ) \ - X ( npluginstate ) \ - X ( narena ) \ - X ( nbuffer ) +#define MJMODEL_INTS \ + X( nq ) \ + X( nv ) \ + X( nu ) \ + X( na ) \ + X( nbody ) \ + X( nbvh ) \ + X( nbvhstatic ) \ + X( nbvhdynamic ) \ + X( njnt ) \ + X( ngeom ) \ + X( nsite ) \ + X( ncam ) \ + X( nlight ) \ + X( nflex ) \ + X( nflexnode ) \ + X( nflexvert ) \ + X( nflexedge ) \ + X( nflexelem ) \ + X( nflexelemdata ) \ + X( nflexelemedge ) \ + X( nflexshelldata ) \ + X( nflexevpair ) \ + X( nflextexcoord ) \ + X( nmesh ) \ + X( nmeshvert ) \ + X( nmeshnormal ) \ + X( nmeshtexcoord ) \ + X( nmeshface ) \ + X( nmeshgraph ) \ + X( nmeshpoly ) \ + X( nmeshpolyvert ) \ + X( nmeshpolymap ) \ + X( nskin ) \ + X( nskinvert ) \ + X( nskintexvert ) \ + X( nskinface ) \ + X( nskinbone ) \ + X( nskinbonevert ) \ + X( nhfield ) \ + X( nhfielddata ) \ + X( ntex ) \ + X( ntexdata ) \ + X( nmat ) \ + X( npair ) \ + X( nexclude ) \ + X( neq ) \ + X( ntendon ) \ + X( nwrap ) \ + X( nsensor ) \ + X( nnumeric ) \ + X( nnumericdata ) \ + X( ntext ) \ + X( ntextdata ) \ + X( ntuple ) \ + X( ntupledata ) \ + X( nkey ) \ + X( nmocap ) \ + X( nplugin ) \ + X( npluginattr ) \ + X( nuser_body ) \ + X( nuser_jnt ) \ + X( nuser_geom ) \ + X( nuser_site ) \ + X( nuser_cam ) \ + X( nuser_tendon ) \ + X( nuser_actuator ) \ + X( nuser_sensor ) \ + X( nnames ) \ + X( npaths ) \ + X( nnames_map ) \ + X( nM ) \ + X( nB ) \ + X( nC ) \ + X( nD ) \ + X( nJmom ) \ + X( ntree ) \ + X( ngravcomp ) \ + X( nemax ) \ + X( njmax ) \ + X( nconmax ) \ + X( nuserdata ) \ + X( nsensordata ) \ + X( npluginstate ) \ + X( narena ) \ + X( nbuffer ) /* nbuffer needs to be the final field */ @@ -178,31 +178,31 @@ // pointer fields of mjModel -// XMJV means that the field is required to construct mjvScene -// (by default we define XMJV to be the same as X) +// XNV means that the field is not required to construct mjvScene +// (by default we define XNV to be the same as X) #define MJMODEL_POINTERS \ X ( mjtNum, qpos0, nq, 1 ) \ X ( mjtNum, qpos_spring, nq, 1 ) \ - XMJV( int, body_parentid, nbody, 1 ) \ - XMJV( int, body_rootid, nbody, 1 ) \ - XMJV( int, body_weldid, nbody, 1 ) \ - XMJV( int, body_mocapid, nbody, 1 ) \ - XMJV( int, body_jntnum, nbody, 1 ) \ - XMJV( int, body_jntadr, nbody, 1 ) \ - XMJV( int, body_dofnum, nbody, 1 ) \ - XMJV( int, body_dofadr, nbody, 1 ) \ + X ( int, body_parentid, nbody, 1 ) \ + X ( int, body_rootid, nbody, 1 ) \ + X ( int, body_weldid, nbody, 1 ) \ + X ( int, body_mocapid, nbody, 1 ) \ + X ( int, body_jntnum, nbody, 1 ) \ + X ( int, body_jntadr, nbody, 1 ) \ + X ( int, body_dofnum, nbody, 1 ) \ + X ( int, body_dofadr, nbody, 1 ) \ X ( int, body_treeid, nbody, 1 ) \ - XMJV( int, body_geomnum, nbody, 1 ) \ - XMJV( int, body_geomadr, nbody, 1 ) \ + X ( int, body_geomnum, nbody, 1 ) \ + X ( int, body_geomadr, nbody, 1 ) \ X ( mjtByte, body_simple, nbody, 1 ) \ X ( mjtByte, body_sameframe, nbody, 1 ) \ X ( mjtNum, body_pos, nbody, 3 ) \ X ( mjtNum, body_quat, nbody, 4 ) \ X ( mjtNum, body_ipos, nbody, 3 ) \ - XMJV( mjtNum, body_iquat, nbody, 4 ) \ - XMJV( mjtNum, body_mass, nbody, 1 ) \ + X ( mjtNum, body_iquat, nbody, 4 ) \ + X ( mjtNum, body_mass, nbody, 1 ) \ X ( mjtNum, body_subtreemass, nbody, 1 ) \ - XMJV( mjtNum, body_inertia, nbody, 3 ) \ + X ( mjtNum, body_inertia, nbody, 3 ) \ X ( mjtNum, body_invweight0, nbody, 2 ) \ X ( mjtNum, body_gravcomp, nbody, 1 ) \ X ( mjtNum, body_margin, nbody, 1 ) \ @@ -210,17 +210,17 @@ X ( int, body_plugin, nbody, 1 ) \ X ( int, body_contype, nbody, 1 ) \ X ( int, body_conaffinity, nbody, 1 ) \ - XMJV( int, body_bvhadr, nbody, 1 ) \ - XMJV( int, body_bvhnum, nbody, 1 ) \ - XMJV( int, bvh_depth, nbvh, 1 ) \ - XMJV( int, bvh_child, nbvh, 2 ) \ - XMJV( int, bvh_nodeid, nbvh, 1 ) \ - XMJV( mjtNum, bvh_aabb, nbvhstatic, 6 ) \ - XMJV( int, jnt_type, njnt, 1 ) \ + X ( int, body_bvhadr, nbody, 1 ) \ + X ( int, body_bvhnum, nbody, 1 ) \ + X ( int, bvh_depth, nbvh, 1 ) \ + X ( int, bvh_child, nbvh, 2 ) \ + X ( int, bvh_nodeid, nbvh, 1 ) \ + X ( mjtNum, bvh_aabb, nbvhstatic, 6 ) \ + X ( int, jnt_type, njnt, 1 ) \ X ( int, jnt_qposadr, njnt, 1 ) \ X ( int, jnt_dofadr, njnt, 1 ) \ - XMJV( int, jnt_bodyid, njnt, 1 ) \ - XMJV( int, jnt_group, njnt, 1 ) \ + X ( int, jnt_bodyid, njnt, 1 ) \ + X ( int, jnt_group, njnt, 1 ) \ X ( mjtByte, jnt_limited, njnt, 1 ) \ X ( mjtByte, jnt_actfrclimited, njnt, 1 ) \ X ( mjtByte, jnt_actgravcomp, njnt, 1 ) \ @@ -246,23 +246,23 @@ X ( mjtNum, dof_damping, nv, 1 ) \ X ( mjtNum, dof_invweight0, nv, 1 ) \ X ( mjtNum, dof_M0, nv, 1 ) \ - XMJV( int, geom_type, ngeom, 1 ) \ - XMJV( int, geom_contype, ngeom, 1 ) \ - XMJV( int, geom_conaffinity, ngeom, 1 ) \ + X ( int, geom_type, ngeom, 1 ) \ + X ( int, geom_contype, ngeom, 1 ) \ + X ( int, geom_conaffinity, ngeom, 1 ) \ X ( int, geom_condim, ngeom, 1 ) \ - XMJV( int, geom_bodyid, ngeom, 1 ) \ - XMJV( int, geom_dataid, ngeom, 1 ) \ - XMJV( int, geom_matid, ngeom, 1 ) \ - XMJV( int, geom_group, ngeom, 1 ) \ + X ( int, geom_bodyid, ngeom, 1 ) \ + X ( int, geom_dataid, ngeom, 1 ) \ + X ( int, geom_matid, ngeom, 1 ) \ + X ( int, geom_group, ngeom, 1 ) \ X ( int, geom_priority, ngeom, 1 ) \ X ( int, geom_plugin, ngeom, 1 ) \ X ( mjtByte, geom_sameframe, ngeom, 1 ) \ X ( mjtNum, geom_solmix, ngeom, 1 ) \ X ( mjtNum, geom_solref, ngeom, mjNREF ) \ X ( mjtNum, geom_solimp, ngeom, mjNIMP ) \ - XMJV( mjtNum, geom_size, ngeom, 3 ) \ - XMJV( mjtNum, geom_aabb, ngeom, 6 ) \ - XMJV( mjtNum, geom_rbound, ngeom, 1 ) \ + X ( mjtNum, geom_size, ngeom, 3 ) \ + X ( mjtNum, geom_aabb, ngeom, 6 ) \ + X ( mjtNum, geom_rbound, ngeom, 1 ) \ X ( mjtNum, geom_pos, ngeom, 3 ) \ X ( mjtNum, geom_quat, ngeom, 4 ) \ X ( mjtNum, geom_friction, ngeom, 3 ) \ @@ -270,17 +270,17 @@ X ( mjtNum, geom_gap, ngeom, 1 ) \ XNV ( mjtNum, geom_fluid, ngeom, mjNFLUID ) \ X ( mjtNum, geom_user, ngeom, MJ_M(nuser_geom) ) \ - XMJV( float, geom_rgba, ngeom, 4 ) \ - XMJV( int, site_type, nsite, 1 ) \ - XMJV( int, site_bodyid, nsite, 1 ) \ - XMJV( int, site_matid, nsite, 1 ) \ - XMJV( int, site_group, nsite, 1 ) \ + X ( float, geom_rgba, ngeom, 4 ) \ + X ( int, site_type, nsite, 1 ) \ + X ( int, site_bodyid, nsite, 1 ) \ + X ( int, site_matid, nsite, 1 ) \ + X ( int, site_group, nsite, 1 ) \ X ( mjtByte, site_sameframe, nsite, 1 ) \ - XMJV( mjtNum, site_size, nsite, 3 ) \ + X ( mjtNum, site_size, nsite, 3 ) \ X ( mjtNum, site_pos, nsite, 3 ) \ X ( mjtNum, site_quat, nsite, 4 ) \ X ( mjtNum, site_user, nsite, MJ_M(nuser_site) ) \ - XMJV( float, site_rgba, nsite, 4 ) \ + X ( float, site_rgba, nsite, 4 ) \ X ( int, cam_mode, ncam, 1 ) \ X ( int, cam_bodyid, ncam, 1 ) \ X ( int, cam_targetbodyid, ncam, 1 ) \ @@ -289,33 +289,33 @@ X ( mjtNum, cam_poscom0, ncam, 3 ) \ X ( mjtNum, cam_pos0, ncam, 3 ) \ X ( mjtNum, cam_mat0, ncam, 9 ) \ - XMJV( int, cam_orthographic, ncam, 1 ) \ - XMJV( mjtNum, cam_fovy, ncam, 1 ) \ - XMJV( mjtNum, cam_ipd, ncam, 1 ) \ - XMJV( int, cam_resolution, ncam, 2 ) \ - XMJV( float, cam_sensorsize, ncam, 2 ) \ - XMJV( float, cam_intrinsic, ncam, 4 ) \ + X ( int, cam_orthographic, ncam, 1 ) \ + X ( mjtNum, cam_fovy, ncam, 1 ) \ + X ( mjtNum, cam_ipd, ncam, 1 ) \ + X ( int, cam_resolution, ncam, 2 ) \ + X ( float, cam_sensorsize, ncam, 2 ) \ + X ( float, cam_intrinsic, ncam, 4 ) \ X ( mjtNum, cam_user, ncam, MJ_M(nuser_cam) ) \ X ( int, light_mode, nlight, 1 ) \ X ( int, light_bodyid, nlight, 1 ) \ X ( int, light_targetbodyid, nlight, 1 ) \ - XMJV( mjtByte, light_directional, nlight, 1 ) \ - XMJV( mjtByte, light_castshadow, nlight, 1 ) \ - XMJV( float, light_bulbradius, nlight, 1 ) \ - XMJV( float, light_intensity, nlight, 1 ) \ - XMJV( float, light_range, nlight, 1 ) \ - XMJV( mjtByte, light_active, nlight, 1 ) \ + X ( mjtByte, light_directional, nlight, 1 ) \ + X ( mjtByte, light_castshadow, nlight, 1 ) \ + X ( float, light_bulbradius, nlight, 1 ) \ + X ( float, light_intensity, nlight, 1 ) \ + X ( float, light_range, nlight, 1 ) \ + X ( mjtByte, light_active, nlight, 1 ) \ X ( mjtNum, light_pos, nlight, 3 ) \ X ( mjtNum, light_dir, nlight, 3 ) \ X ( mjtNum, light_poscom0, nlight, 3 ) \ X ( mjtNum, light_pos0, nlight, 3 ) \ X ( mjtNum, light_dir0, nlight, 3 ) \ - XMJV( float, light_attenuation, nlight, 3 ) \ - XMJV( float, light_cutoff, nlight, 1 ) \ - XMJV( float, light_exponent, nlight, 1 ) \ - XMJV( float, light_ambient, nlight, 3 ) \ - XMJV( float, light_diffuse, nlight, 3 ) \ - XMJV( float, light_specular, nlight, 3 ) \ + X ( float, light_attenuation, nlight, 3 ) \ + X ( float, light_cutoff, nlight, 1 ) \ + X ( float, light_exponent, nlight, 1 ) \ + X ( float, light_ambient, nlight, 3 ) \ + X ( float, light_diffuse, nlight, 3 ) \ + X ( float, light_specular, nlight, 3 ) \ X ( int, flex_contype, nflex, 1 ) \ X ( int, flex_conaffinity, nflex, 1 ) \ X ( int, flex_condim, nflex, 1 ) \ @@ -329,42 +329,42 @@ X ( mjtByte, flex_internal, nflex, 1 ) \ X ( int, flex_selfcollide, nflex, 1 ) \ X ( int, flex_activelayers, nflex, 1 ) \ - XMJV( int, flex_dim, nflex, 1 ) \ - XMJV( int, flex_matid, nflex, 1 ) \ - XMJV( int, flex_group, nflex, 1 ) \ - XMJV( int, flex_interp, nflex, 1 ) \ - XMJV( int, flex_nodeadr, nflex, 1 ) \ - XMJV( int, flex_nodenum, nflex, 1 ) \ - XMJV( int, flex_vertadr, nflex, 1 ) \ - XMJV( int, flex_vertnum, nflex, 1 ) \ + X ( int, flex_dim, nflex, 1 ) \ + X ( int, flex_matid, nflex, 1 ) \ + X ( int, flex_group, nflex, 1 ) \ + X ( int, flex_interp, nflex, 1 ) \ + X ( int, flex_nodeadr, nflex, 1 ) \ + X ( int, flex_nodenum, nflex, 1 ) \ + X ( int, flex_vertadr, nflex, 1 ) \ + X ( int, flex_vertnum, nflex, 1 ) \ X ( int, flex_edgeadr, nflex, 1 ) \ X ( int, flex_edgenum, nflex, 1 ) \ - XMJV( int, flex_elemadr, nflex, 1 ) \ - XMJV( int, flex_elemnum, nflex, 1 ) \ - XMJV( int, flex_elemdataadr, nflex, 1 ) \ + X ( int, flex_elemadr, nflex, 1 ) \ + X ( int, flex_elemnum, nflex, 1 ) \ + X ( int, flex_elemdataadr, nflex, 1 ) \ X ( int, flex_elemedgeadr, nflex, 1 ) \ - XMJV( int, flex_shellnum, nflex, 1 ) \ - XMJV( int, flex_shelldataadr, nflex, 1 ) \ + X ( int, flex_shellnum, nflex, 1 ) \ + X ( int, flex_shelldataadr, nflex, 1 ) \ X ( int, flex_evpairadr, nflex, 1 ) \ X ( int, flex_evpairnum, nflex, 1 ) \ - XMJV( int, flex_texcoordadr, nflex, 1 ) \ - XMJV( int, flex_nodebodyid, nflexnode, 1 ) \ + X ( int, flex_texcoordadr, nflex, 1 ) \ + X ( int, flex_nodebodyid, nflexnode, 1 ) \ X ( int, flex_vertbodyid, nflexvert, 1 ) \ X ( int, flex_edge, nflexedge, 2 ) \ X ( int, flex_edgeflap, nflexedge, 2 ) \ - XMJV( int, flex_elem, nflexelemdata, 1 ) \ - XMJV( int, flex_elemtexcoord, nflexelemdata, 1 ) \ + X ( int, flex_elem, nflexelemdata, 1 ) \ + X ( int, flex_elemtexcoord, nflexelemdata, 1 ) \ X ( int, flex_elemedge, nflexelemedge, 1 ) \ - XMJV( int, flex_elemlayer, nflexelem, 1 ) \ - XMJV( int, flex_shell, nflexshelldata,1 ) \ + X ( int, flex_elemlayer, nflexelem, 1 ) \ + X ( int, flex_shell, nflexshelldata,1 ) \ X ( int, flex_evpair, nflexevpair, 2 ) \ X ( mjtNum, flex_vert, nflexvert, 3 ) \ X ( mjtNum, flex_vert0, nflexvert, 3 ) \ - XMJV( mjtNum, flex_node, nflexnode, 3 ) \ + X ( mjtNum, flex_node, nflexnode, 3 ) \ X ( mjtNum, flex_node0, nflexnode, 3 ) \ X ( mjtNum, flexedge_length0, nflexedge, 1 ) \ X ( mjtNum, flexedge_invweight0, nflexedge, 1 ) \ - XMJV( mjtNum, flex_radius, nflex, 1 ) \ + X ( mjtNum, flex_radius, nflex, 1 ) \ X ( mjtNum, flex_stiffness, nflexelem, 21 ) \ X ( mjtNum, flex_bending, nflexedge, 16 ) \ X ( mjtNum, flex_damping, nflex, 1 ) \ @@ -373,23 +373,23 @@ X ( mjtByte, flex_edgeequality, nflex, 1 ) \ X ( mjtByte, flex_rigid, nflex, 1 ) \ X ( mjtByte, flexedge_rigid, nflexedge, 1 ) \ - XMJV( mjtByte, flex_centered, nflex, 1 ) \ - XMJV( mjtByte, flex_flatskin, nflex, 1 ) \ - XMJV( int, flex_bvhadr, nflex, 1 ) \ - XMJV( int, flex_bvhnum, nflex, 1 ) \ - XMJV( float, flex_rgba, nflex, 4 ) \ - XMJV( float, flex_texcoord, nflextexcoord, 2 ) \ + X ( mjtByte, flex_centered, nflex, 1 ) \ + X ( mjtByte, flex_flatskin, nflex, 1 ) \ + X ( int, flex_bvhadr, nflex, 1 ) \ + X ( int, flex_bvhnum, nflex, 1 ) \ + X ( float, flex_rgba, nflex, 4 ) \ + X ( float, flex_texcoord, nflextexcoord, 2 ) \ X ( int, mesh_vertadr, nmesh, 1 ) \ X ( int, mesh_vertnum, nmesh, 1 ) \ X ( int, mesh_normaladr, nmesh, 1 ) \ X ( int, mesh_normalnum, nmesh, 1 ) \ - XMJV( int, mesh_texcoordadr, nmesh, 1 ) \ + X ( int, mesh_texcoordadr, nmesh, 1 ) \ X ( int, mesh_texcoordnum, nmesh, 1 ) \ X ( int, mesh_faceadr, nmesh, 1 ) \ X ( int, mesh_facenum, nmesh, 1 ) \ - XMJV( int, mesh_bvhadr, nmesh, 1 ) \ - XMJV( int, mesh_bvhnum, nmesh, 1 ) \ - XMJV( int, mesh_graphadr, nmesh, 1 ) \ + X ( int, mesh_bvhadr, nmesh, 1 ) \ + X ( int, mesh_bvhnum, nmesh, 1 ) \ + X ( int, mesh_graphadr, nmesh, 1 ) \ X ( mjtNum, mesh_scale, nmesh, 3 ) \ X ( mjtNum, mesh_pos, nmesh, 3 ) \ X ( mjtNum, mesh_quat, nmesh, 4 ) \ @@ -400,7 +400,7 @@ XNV ( int, mesh_facenormal, nmeshface, 3 ) \ XNV ( int, mesh_facetexcoord, nmeshface, 3 ) \ XNV ( int, mesh_graph, nmeshgraph, 1 ) \ - XMJV( int, mesh_pathadr, nmesh, 1 ) \ + X ( int, mesh_pathadr, nmesh, 1 ) \ XNV ( int, mesh_polynum, nmesh, 1 ) \ XNV ( int, mesh_polyadr, nmesh, 1 ) \ XNV ( mjtNum, mesh_polynormal, nmeshpoly, 3 ) \ @@ -410,51 +410,51 @@ XNV ( int, mesh_polymapadr, nmeshvert, 1 ) \ XNV ( int, mesh_polymapnum, nmeshvert, 1 ) \ XNV ( int, mesh_polymap, nmeshpolymap, 1 ) \ - XMJV( int, skin_matid, nskin, 1 ) \ - XMJV( int, skin_group, nskin, 1 ) \ - XMJV( float, skin_rgba, nskin, 4 ) \ - XMJV( float, skin_inflate, nskin, 1 ) \ - XMJV( int, skin_vertadr, nskin, 1 ) \ - XMJV( int, skin_vertnum, nskin, 1 ) \ - XMJV( int, skin_texcoordadr, nskin, 1 ) \ - XMJV( int, skin_faceadr, nskin, 1 ) \ - XMJV( int, skin_facenum, nskin, 1 ) \ - XMJV( int, skin_boneadr, nskin, 1 ) \ - XMJV( int, skin_bonenum, nskin, 1 ) \ - XMJV( float, skin_vert, nskinvert, 3 ) \ + X ( int, skin_matid, nskin, 1 ) \ + X ( int, skin_group, nskin, 1 ) \ + X ( float, skin_rgba, nskin, 4 ) \ + X ( float, skin_inflate, nskin, 1 ) \ + X ( int, skin_vertadr, nskin, 1 ) \ + X ( int, skin_vertnum, nskin, 1 ) \ + X ( int, skin_texcoordadr, nskin, 1 ) \ + X ( int, skin_faceadr, nskin, 1 ) \ + X ( int, skin_facenum, nskin, 1 ) \ + X ( int, skin_boneadr, nskin, 1 ) \ + X ( int, skin_bonenum, nskin, 1 ) \ + X ( float, skin_vert, nskinvert, 3 ) \ X ( float, skin_texcoord, nskintexvert, 2 ) \ - XMJV( int, skin_face, nskinface, 3 ) \ - XMJV( int, skin_bonevertadr, nskinbone, 1 ) \ - XMJV( int, skin_bonevertnum, nskinbone, 1 ) \ - XMJV( float, skin_bonebindpos, nskinbone, 3 ) \ - XMJV( float, skin_bonebindquat, nskinbone, 4 ) \ - XMJV( int, skin_bonebodyid, nskinbone, 1 ) \ - XMJV( int, skin_bonevertid, nskinbonevert, 1 ) \ - XMJV( float, skin_bonevertweight, nskinbonevert, 1 ) \ - XMJV( int, skin_pathadr, nskin, 1 ) \ + X ( int, skin_face, nskinface, 3 ) \ + X ( int, skin_bonevertadr, nskinbone, 1 ) \ + X ( int, skin_bonevertnum, nskinbone, 1 ) \ + X ( float, skin_bonebindpos, nskinbone, 3 ) \ + X ( float, skin_bonebindquat, nskinbone, 4 ) \ + X ( int, skin_bonebodyid, nskinbone, 1 ) \ + X ( int, skin_bonevertid, nskinbonevert, 1 ) \ + X ( float, skin_bonevertweight, nskinbonevert, 1 ) \ + X ( int, skin_pathadr, nskin, 1 ) \ X ( mjtNum, hfield_size, nhfield, 4 ) \ X ( int, hfield_nrow, nhfield, 1 ) \ X ( int, hfield_ncol, nhfield, 1 ) \ X ( int, hfield_adr, nhfield, 1 ) \ XNV ( float, hfield_data, nhfielddata, 1 ) \ - XMJV( int, hfield_pathadr, nhfield, 1 ) \ + X ( int, hfield_pathadr, nhfield, 1 ) \ X ( int, tex_type, ntex, 1 ) \ X ( int, tex_height, ntex, 1 ) \ X ( int, tex_width, ntex, 1 ) \ X ( int, tex_nchannel, ntex, 1 ) \ X ( int, tex_adr, ntex, 1 ) \ XNV ( mjtByte, tex_data, ntexdata, 1 ) \ - XMJV( int, tex_pathadr, ntex, 1 ) \ - XMJV( int, mat_texid, nmat, mjNTEXROLE ) \ - XMJV( mjtByte, mat_texuniform, nmat, 1 ) \ - XMJV( float, mat_texrepeat, nmat, 2 ) \ - XMJV( float, mat_emission, nmat, 1 ) \ - XMJV( float, mat_specular, nmat, 1 ) \ - XMJV( float, mat_shininess, nmat, 1 ) \ - XMJV( float, mat_reflectance, nmat, 1 ) \ - XMJV( float, mat_metallic, nmat, 1 ) \ - XMJV( float, mat_roughness, nmat, 1 ) \ - XMJV( float, mat_rgba, nmat, 4 ) \ + X ( int, tex_pathadr, ntex, 1 ) \ + X ( int, mat_texid, nmat, mjNTEXROLE ) \ + X ( mjtByte, mat_texuniform, nmat, 1 ) \ + X ( float, mat_texrepeat, nmat, 2 ) \ + X ( float, mat_emission, nmat, 1 ) \ + X ( float, mat_specular, nmat, 1 ) \ + X ( float, mat_shininess, nmat, 1 ) \ + X ( float, mat_reflectance, nmat, 1 ) \ + X ( float, mat_metallic, nmat, 1 ) \ + X ( float, mat_roughness, nmat, 1 ) \ + X ( float, mat_rgba, nmat, 4 ) \ X ( int, pair_dim, npair, 1 ) \ X ( int, pair_geom1, npair, 1 ) \ X ( int, pair_geom2, npair, 1 ) \ @@ -466,74 +466,74 @@ X ( mjtNum, pair_gap, npair, 1 ) \ X ( mjtNum, pair_friction, npair, 5 ) \ X ( int, exclude_signature, nexclude, 1 ) \ - XMJV( int, eq_type, neq, 1 ) \ - XMJV( int, eq_obj1id, neq, 1 ) \ - XMJV( int, eq_obj2id, neq, 1 ) \ - XMJV( int, eq_objtype, neq, 1 ) \ + X ( int, eq_type, neq, 1 ) \ + X ( int, eq_obj1id, neq, 1 ) \ + X ( int, eq_obj2id, neq, 1 ) \ + X ( int, eq_objtype, neq, 1 ) \ X ( mjtByte, eq_active0, neq, 1 ) \ X ( mjtNum, eq_solref, neq, mjNREF ) \ X ( mjtNum, eq_solimp, neq, mjNIMP ) \ - XMJV( mjtNum, eq_data, neq, mjNEQDATA ) \ + X ( mjtNum, eq_data, neq, mjNEQDATA ) \ X ( int, tendon_adr, ntendon, 1 ) \ - XMJV( int, tendon_num, ntendon, 1 ) \ - XMJV( int, tendon_matid, ntendon, 1 ) \ - XMJV( int, tendon_group, ntendon, 1 ) \ - XMJV( mjtByte, tendon_limited, ntendon, 1 ) \ - XMJV( mjtByte, tendon_actfrclimited, ntendon, 1 ) \ - XMJV( mjtNum, tendon_width, ntendon, 1 ) \ + X ( int, tendon_num, ntendon, 1 ) \ + X ( int, tendon_matid, ntendon, 1 ) \ + X ( int, tendon_group, ntendon, 1 ) \ + X ( mjtByte, tendon_limited, ntendon, 1 ) \ + X ( mjtByte, tendon_actfrclimited, ntendon, 1 ) \ + X ( mjtNum, tendon_width, ntendon, 1 ) \ X ( mjtNum, tendon_solref_lim, ntendon, mjNREF ) \ X ( mjtNum, tendon_solimp_lim, ntendon, mjNIMP ) \ X ( mjtNum, tendon_solref_fri, ntendon, mjNREF ) \ X ( mjtNum, tendon_solimp_fri, ntendon, mjNIMP ) \ - XMJV( mjtNum, tendon_range, ntendon, 2 ) \ - XMJV( mjtNum, tendon_actfrcrange, ntendon, 2 ) \ + X ( mjtNum, tendon_range, ntendon, 2 ) \ + X ( mjtNum, tendon_actfrcrange, ntendon, 2 ) \ X ( mjtNum, tendon_margin, ntendon, 1 ) \ - XMJV( mjtNum, tendon_stiffness, ntendon, 1 ) \ - XMJV( mjtNum, tendon_damping, ntendon, 1 ) \ + X ( mjtNum, tendon_stiffness, ntendon, 1 ) \ + X ( mjtNum, tendon_damping, ntendon, 1 ) \ X ( mjtNum, tendon_armature, ntendon, 1 ) \ - XMJV( mjtNum, tendon_frictionloss, ntendon, 1 ) \ - XMJV( mjtNum, tendon_lengthspring, ntendon, 2 ) \ + X ( mjtNum, tendon_frictionloss, ntendon, 1 ) \ + X ( mjtNum, tendon_lengthspring, ntendon, 2 ) \ X ( mjtNum, tendon_length0, ntendon, 1 ) \ X ( mjtNum, tendon_invweight0, ntendon, 1 ) \ X ( mjtNum, tendon_user, ntendon, MJ_M(nuser_tendon) ) \ - XMJV( float, tendon_rgba, ntendon, 4 ) \ + X ( float, tendon_rgba, ntendon, 4 ) \ X ( int, wrap_type, nwrap, 1 ) \ X ( int, wrap_objid, nwrap, 1 ) \ X ( mjtNum, wrap_prm, nwrap, 1 ) \ - XMJV( int, actuator_trntype, nu, 1 ) \ - XMJV( int, actuator_dyntype, nu, 1 ) \ + X ( int, actuator_trntype, nu, 1 ) \ + X ( int, actuator_dyntype, nu, 1 ) \ X ( int, actuator_gaintype, nu, 1 ) \ X ( int, actuator_biastype, nu, 1 ) \ - XMJV( int, actuator_trnid, nu, 2 ) \ - XMJV( int, actuator_actadr, nu, 1 ) \ - XMJV( int, actuator_actnum, nu, 1 ) \ - XMJV( int, actuator_group, nu, 1 ) \ - XMJV( mjtByte, actuator_ctrllimited, nu, 1 ) \ + X ( int, actuator_trnid, nu, 2 ) \ + X ( int, actuator_actadr, nu, 1 ) \ + X ( int, actuator_actnum, nu, 1 ) \ + X ( int, actuator_group, nu, 1 ) \ + X ( mjtByte, actuator_ctrllimited, nu, 1 ) \ X ( mjtByte, actuator_forcelimited, nu, 1 ) \ - XMJV( mjtByte, actuator_actlimited, nu, 1 ) \ + X ( mjtByte, actuator_actlimited, nu, 1 ) \ X ( mjtNum, actuator_dynprm, nu, mjNDYN ) \ X ( mjtNum, actuator_gainprm, nu, mjNGAIN ) \ X ( mjtNum, actuator_biasprm, nu, mjNBIAS ) \ X ( mjtByte, actuator_actearly, nu, 1 ) \ - XMJV( mjtNum, actuator_ctrlrange, nu, 2 ) \ + X ( mjtNum, actuator_ctrlrange, nu, 2 ) \ X ( mjtNum, actuator_forcerange, nu, 2 ) \ - XMJV( mjtNum, actuator_actrange, nu, 2 ) \ + X ( mjtNum, actuator_actrange, nu, 2 ) \ X ( mjtNum, actuator_gear, nu, 6 ) \ - XMJV( mjtNum, actuator_cranklength, nu, 1 ) \ + X ( mjtNum, actuator_cranklength, nu, 1 ) \ X ( mjtNum, actuator_acc0, nu, 1 ) \ X ( mjtNum, actuator_length0, nu, 1 ) \ X ( mjtNum, actuator_lengthrange, nu, 2 ) \ X ( mjtNum, actuator_user, nu, MJ_M(nuser_actuator) ) \ X ( int, actuator_plugin, nu, 1 ) \ - XMJV( int, sensor_type, nsensor, 1 ) \ + X ( int, sensor_type, nsensor, 1 ) \ X ( int, sensor_datatype, nsensor, 1 ) \ X ( int, sensor_needstage, nsensor, 1 ) \ X ( int, sensor_objtype, nsensor, 1 ) \ - XMJV( int, sensor_objid, nsensor, 1 ) \ + X ( int, sensor_objid, nsensor, 1 ) \ X ( int, sensor_reftype, nsensor, 1 ) \ X ( int, sensor_refid, nsensor, 1 ) \ X ( int, sensor_dim, nsensor, 1 ) \ - XMJV( int, sensor_adr, nsensor, 1 ) \ + X ( int, sensor_adr, nsensor, 1 ) \ X ( mjtNum, sensor_cutoff, nsensor, 1 ) \ X ( mjtNum, sensor_noise, nsensor, 1 ) \ X ( mjtNum, sensor_user, nsensor, MJ_M(nuser_sensor) ) \ @@ -561,12 +561,12 @@ X ( mjtNum, key_mpos, nkey, MJ_M(nmocap)*3 ) \ X ( mjtNum, key_mquat, nkey, MJ_M(nmocap)*4 ) \ X ( mjtNum, key_ctrl, nkey, MJ_M(nu) ) \ - XMJV( int, name_bodyadr, nbody, 1 ) \ - XMJV( int, name_jntadr, njnt, 1 ) \ - XMJV( int, name_geomadr, ngeom, 1 ) \ - XMJV( int, name_siteadr, nsite, 1 ) \ - XMJV( int, name_camadr, ncam, 1 ) \ - XMJV( int, name_lightadr, nlight, 1 ) \ + X ( int, name_bodyadr, nbody, 1 ) \ + X ( int, name_jntadr, njnt, 1 ) \ + X ( int, name_geomadr, ngeom, 1 ) \ + X ( int, name_siteadr, nsite, 1 ) \ + X ( int, name_camadr, ncam, 1 ) \ + X ( int, name_lightadr, nlight, 1 ) \ X ( int, name_flexadr, nflex, 1 ) \ X ( int, name_meshadr, nmesh, 1 ) \ X ( int, name_skinadr, nskin, 1 ) \ @@ -575,18 +575,18 @@ X ( int, name_matadr, nmat, 1 ) \ X ( int, name_pairadr, npair, 1 ) \ X ( int, name_excludeadr, nexclude, 1 ) \ - XMJV( int, name_eqadr, neq, 1 ) \ - XMJV( int, name_tendonadr, ntendon, 1 ) \ - XMJV( int, name_actuatoradr, nu, 1 ) \ + X ( int, name_eqadr, neq, 1 ) \ + X ( int, name_tendonadr, ntendon, 1 ) \ + X ( int, name_actuatoradr, nu, 1 ) \ X ( int, name_sensoradr, nsensor, 1 ) \ X ( int, name_numericadr, nnumeric, 1 ) \ X ( int, name_textadr, ntext, 1 ) \ X ( int, name_tupleadr, ntuple, 1 ) \ X ( int, name_keyadr, nkey, 1 ) \ X ( int, name_pluginadr, nplugin, 1 ) \ - XMJV( char, names, nnames, 1 ) \ + X ( char, names, nnames, 1 ) \ X ( int, names_map, nnames_map, 1 ) \ - XMJV( char, paths, npaths, 1 ) \ + X ( char, paths, npaths, 1 ) \ //-------------------------------- mjData ---------------------------------------------------------- @@ -596,60 +596,60 @@ // pointer fields of mjData -// XMJV means that the field is required to construct mjvScene -// (by default we define XMJV to be the same as X) +// XNV means that the field is not required to construct mjvScene +// (by default we define XNV to be the same as X) #define MJDATA_POINTERS \ X ( mjtNum, qpos, nq, 1 ) \ X ( mjtNum, qvel, nv, 1 ) \ - XMJV( mjtNum, act, na, 1 ) \ + X ( mjtNum, act, na, 1 ) \ X ( mjtNum, qacc_warmstart, nv, 1 ) \ X ( mjtNum, plugin_state, npluginstate, 1 ) \ - XMJV( mjtNum, ctrl, nu, 1 ) \ + X ( mjtNum, ctrl, nu, 1 ) \ X ( mjtNum, qfrc_applied, nv, 1 ) \ - XMJV( mjtNum, xfrc_applied, nbody, 6 ) \ - XMJV( mjtByte, eq_active, neq, 1 ) \ + X ( mjtNum, xfrc_applied, nbody, 6 ) \ + X ( mjtByte, eq_active, neq, 1 ) \ X ( mjtNum, mocap_pos, nmocap, 3 ) \ X ( mjtNum, mocap_quat, nmocap, 4 ) \ X ( mjtNum, qacc, nv, 1 ) \ X ( mjtNum, act_dot, na, 1 ) \ X ( mjtNum, userdata, nuserdata, 1 ) \ - XMJV( mjtNum, sensordata, nsensordata, 1 ) \ + X ( mjtNum, sensordata, nsensordata, 1 ) \ X ( int, plugin, nplugin, 1 ) \ X ( uintptr_t, plugin_data, nplugin, 1 ) \ - XMJV( mjtNum, xpos, nbody, 3 ) \ - XMJV( mjtNum, xquat, nbody, 4 ) \ - XMJV( mjtNum, xmat, nbody, 9 ) \ - XMJV( mjtNum, xipos, nbody, 3 ) \ - XMJV( mjtNum, ximat, nbody, 9 ) \ - XMJV( mjtNum, xanchor, njnt, 3 ) \ - XMJV( mjtNum, xaxis, njnt, 3 ) \ - XMJV( mjtNum, geom_xpos, ngeom, 3 ) \ - XMJV( mjtNum, geom_xmat, ngeom, 9 ) \ - XMJV( mjtNum, site_xpos, nsite, 3 ) \ - XMJV( mjtNum, site_xmat, nsite, 9 ) \ - XMJV( mjtNum, cam_xpos, ncam, 3 ) \ - XMJV( mjtNum, cam_xmat, ncam, 9 ) \ - XMJV( mjtNum, light_xpos, nlight, 3 ) \ - XMJV( mjtNum, light_xdir, nlight, 3 ) \ - XMJV( mjtNum, subtree_com, nbody, 3 ) \ + X ( mjtNum, xpos, nbody, 3 ) \ + X ( mjtNum, xquat, nbody, 4 ) \ + X ( mjtNum, xmat, nbody, 9 ) \ + X ( mjtNum, xipos, nbody, 3 ) \ + X ( mjtNum, ximat, nbody, 9 ) \ + X ( mjtNum, xanchor, njnt, 3 ) \ + X ( mjtNum, xaxis, njnt, 3 ) \ + X ( mjtNum, geom_xpos, ngeom, 3 ) \ + X ( mjtNum, geom_xmat, ngeom, 9 ) \ + X ( mjtNum, site_xpos, nsite, 3 ) \ + X ( mjtNum, site_xmat, nsite, 9 ) \ + X ( mjtNum, cam_xpos, ncam, 3 ) \ + X ( mjtNum, cam_xmat, ncam, 9 ) \ + X ( mjtNum, light_xpos, nlight, 3 ) \ + X ( mjtNum, light_xdir, nlight, 3 ) \ + X ( mjtNum, subtree_com, nbody, 3 ) \ X ( mjtNum, cdof, nv, 6 ) \ X ( mjtNum, cinert, nbody, 10 ) \ - XMJV( mjtNum, flexvert_xpos, nflexvert, 3 ) \ + X ( mjtNum, flexvert_xpos, nflexvert, 3 ) \ X ( mjtNum, flexelem_aabb, nflexelem, 6 ) \ X ( int, flexedge_J_rownnz, nflexedge, 1 ) \ X ( int, flexedge_J_rowadr, nflexedge, 1 ) \ X ( int, flexedge_J_colind, nflexedge, MJ_M(nv) ) \ X ( mjtNum, flexedge_J, nflexedge, MJ_M(nv) ) \ X ( mjtNum, flexedge_length, nflexedge, 1 ) \ - XMJV( int, ten_wrapadr, ntendon, 1 ) \ - XMJV( int, ten_wrapnum, ntendon, 1 ) \ + X ( int, ten_wrapadr, ntendon, 1 ) \ + X ( int, ten_wrapnum, ntendon, 1 ) \ X ( int, ten_J_rownnz, ntendon, 1 ) \ X ( int, ten_J_rowadr, ntendon, 1 ) \ X ( int, ten_J_colind, ntendon, MJ_M(nv) ) \ - XMJV( mjtNum, ten_length, ntendon, 1 ) \ + X ( mjtNum, ten_length, ntendon, 1 ) \ X ( mjtNum, ten_J, ntendon, MJ_M(nv) ) \ - XMJV( int, wrap_obj, nwrap, 2 ) \ - XMJV( mjtNum, wrap_xpos, nwrap, 6 ) \ + X ( int, wrap_obj, nwrap, 2 ) \ + X ( mjtNum, wrap_xpos, nwrap, 6 ) \ X ( mjtNum, actuator_length, nu, 1 ) \ X ( int, moment_rownnz, nu, 1 ) \ X ( int, moment_rowadr, nu, 1 ) \ @@ -660,8 +660,8 @@ XNV ( mjtNum, M, nC, 1 ) \ XNV ( mjtNum, qLD, nC, 1 ) \ X ( mjtNum, qLDiagInv, nv, 1 ) \ - XMJV( mjtNum, bvh_aabb_dyn, nbvhdynamic, 6 ) \ - XMJV( mjtByte, bvh_active, nbvh, 1 ) \ + X ( mjtNum, bvh_aabb_dyn, nbvhdynamic, 6 ) \ + X ( mjtByte, bvh_active, nbvh, 1 ) \ X ( mjtNum, flexedge_velocity, nflexedge, 1 ) \ X ( mjtNum, ten_velocity, ntendon, 1 ) \ X ( mjtNum, actuator_velocity, nu, 1 ) \ @@ -834,12 +834,6 @@ X( mjtNum, solver_fwdinv, 2, 1 ) \ X( mjtNum, energy, 2, 1 ) - -// alias XMJV to be the same as X -// to obtain only X macros for fields that are relevant for mjvScene creation, -// redefine X to expand to nothing, and XMJV to do what's required -#define XMJV X - // alias XNV to be the same as X // to obtain only X macros for fields that are relevant for mjvScene creation, // redefine XNV to expand to nothing diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index 8d8cb8c2..6c715427 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -625,20 +625,10 @@ MJAPI void mjv_alignToCamera(mjtNum res[3], const mjtNum vec[3], const mjtNum fo MJAPI void mjv_moveCamera(const mjModel* m, int action, mjtNum reldx, mjtNum reldy, const mjvScene* scn, mjvCamera* cam); -// Move camera with mouse given a scene state; action is mjtMouse. -MJAPI void mjv_moveCameraFromState(const mjvSceneState* scnstate, int action, - mjtNum reldx, mjtNum reldy, - const mjvScene* scn, mjvCamera* cam); - // Move perturb object with mouse; action is mjtMouse. MJAPI void mjv_movePerturb(const mjModel* m, const mjData* d, int action, mjtNum reldx, mjtNum reldy, const mjvScene* scn, mjvPerturb* pert); -// Move perturb object with mouse given a scene state; action is mjtMouse. -MJAPI void mjv_movePerturbFromState(const mjvSceneState* scnstate, int action, - mjtNum reldx, mjtNum reldy, - const mjvScene* scn, mjvPerturb* pert); - // Move model with mouse; action is mjtMouse. MJAPI void mjv_moveModel(const mjModel* m, int action, mjtNum reldx, mjtNum reldy, const mjtNum roomup[3], mjvScene* scn); @@ -695,28 +685,9 @@ MJAPI void mjv_freeScene(mjvScene* scn); MJAPI void mjv_updateScene(const mjModel* m, mjData* d, const mjvOption* opt, const mjvPerturb* pert, mjvCamera* cam, int catmask, mjvScene* scn); -// Update entire scene from a scene state, return the number of new mjWARN_VGEOMFULL warnings. -MJAPI int mjv_updateSceneFromState(const mjvSceneState* scnstate, const mjvOption* opt, - const mjvPerturb* pert, mjvCamera* cam, int catmask, - mjvScene* scn); - // Copy mjModel, skip large arrays not required for abstract visualization. MJAPI void mjv_copyModel(mjModel* dest, const mjModel* src); -// Set default scene state. -MJAPI void mjv_defaultSceneState(mjvSceneState* scnstate); - -// Allocate resources and initialize a scene state object. -MJAPI void mjv_makeSceneState(const mjModel* m, const mjData* d, - mjvSceneState* scnstate, int maxgeom); - -// Free scene state. -MJAPI void mjv_freeSceneState(mjvSceneState* scnstate); - -// Update a scene state from model and data. -MJAPI void mjv_updateSceneState(const mjModel* m, mjData* d, const mjvOption* opt, - mjvSceneState* scnstate); - // Add geoms from selected categories. MJAPI void mjv_addGeoms(const mjModel* m, mjData* d, const mjvOption* opt, const mjvPerturb* pert, int catmask, mjvScene* scn); diff --git a/python/mujoco/introspect/functions.py b/python/mujoco/introspect/functions.py index 1f495c03..cf85a819 100644 --- a/python/mujoco/introspect/functions.py +++ b/python/mujoco/introspect/functions.py @@ -4011,44 +4011,6 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), doc='Move camera with mouse; action is mjtMouse.', )), - ('mjv_moveCameraFromState', - FunctionDecl( - name='mjv_moveCameraFromState', - return_type=ValueType(name='void'), - parameters=( - FunctionParameterDecl( - name='scnstate', - type=PointerType( - inner_type=ValueType(name='mjvSceneState', is_const=True), - ), - ), - FunctionParameterDecl( - name='action', - type=ValueType(name='int'), - ), - FunctionParameterDecl( - name='reldx', - type=ValueType(name='mjtNum'), - ), - FunctionParameterDecl( - name='reldy', - type=ValueType(name='mjtNum'), - ), - FunctionParameterDecl( - name='scn', - type=PointerType( - inner_type=ValueType(name='mjvScene', is_const=True), - ), - ), - FunctionParameterDecl( - name='cam', - type=PointerType( - inner_type=ValueType(name='mjvCamera'), - ), - ), - ), - doc='Move camera with mouse given a scene state; action is mjtMouse.', - )), ('mjv_movePerturb', FunctionDecl( name='mjv_movePerturb', @@ -4093,44 +4055,6 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), doc='Move perturb object with mouse; action is mjtMouse.', )), - ('mjv_movePerturbFromState', - FunctionDecl( - name='mjv_movePerturbFromState', - return_type=ValueType(name='void'), - parameters=( - FunctionParameterDecl( - name='scnstate', - type=PointerType( - inner_type=ValueType(name='mjvSceneState', is_const=True), - ), - ), - FunctionParameterDecl( - name='action', - type=ValueType(name='int'), - ), - FunctionParameterDecl( - name='reldx', - type=ValueType(name='mjtNum'), - ), - FunctionParameterDecl( - name='reldy', - type=ValueType(name='mjtNum'), - ), - FunctionParameterDecl( - name='scn', - type=PointerType( - inner_type=ValueType(name='mjvScene', is_const=True), - ), - ), - FunctionParameterDecl( - name='pert', - type=PointerType( - inner_type=ValueType(name='mjvPerturb'), - ), - ), - ), - doc='Move perturb object with mouse given a scene state; action is mjtMouse.', # pylint: disable=line-too-long - )), ('mjv_moveModel', FunctionDecl( name='mjv_moveModel', @@ -4560,48 +4484,6 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), doc='Update entire scene given model state.', )), - ('mjv_updateSceneFromState', - FunctionDecl( - name='mjv_updateSceneFromState', - return_type=ValueType(name='int'), - parameters=( - FunctionParameterDecl( - name='scnstate', - type=PointerType( - inner_type=ValueType(name='mjvSceneState', is_const=True), - ), - ), - FunctionParameterDecl( - name='opt', - type=PointerType( - inner_type=ValueType(name='mjvOption', is_const=True), - ), - ), - FunctionParameterDecl( - name='pert', - type=PointerType( - inner_type=ValueType(name='mjvPerturb', is_const=True), - ), - ), - FunctionParameterDecl( - name='cam', - type=PointerType( - inner_type=ValueType(name='mjvCamera'), - ), - ), - FunctionParameterDecl( - name='catmask', - type=ValueType(name='int'), - ), - FunctionParameterDecl( - name='scn', - type=PointerType( - inner_type=ValueType(name='mjvScene'), - ), - ), - ), - doc='Update entire scene from a scene state, return the number of new mjWARN_VGEOMFULL warnings.', # pylint: disable=line-too-long - )), ('mjv_copyModel', FunctionDecl( name='mjv_copyModel', @@ -4622,96 +4504,6 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), doc='Copy mjModel, skip large arrays not required for abstract visualization.', # pylint: disable=line-too-long )), - ('mjv_defaultSceneState', - FunctionDecl( - name='mjv_defaultSceneState', - return_type=ValueType(name='void'), - parameters=( - FunctionParameterDecl( - name='scnstate', - type=PointerType( - inner_type=ValueType(name='mjvSceneState'), - ), - ), - ), - doc='Set default scene state.', - )), - ('mjv_makeSceneState', - FunctionDecl( - name='mjv_makeSceneState', - return_type=ValueType(name='void'), - parameters=( - FunctionParameterDecl( - name='m', - type=PointerType( - inner_type=ValueType(name='mjModel', is_const=True), - ), - ), - FunctionParameterDecl( - name='d', - type=PointerType( - inner_type=ValueType(name='mjData', is_const=True), - ), - ), - FunctionParameterDecl( - name='scnstate', - type=PointerType( - inner_type=ValueType(name='mjvSceneState'), - ), - ), - FunctionParameterDecl( - name='maxgeom', - type=ValueType(name='int'), - ), - ), - doc='Allocate resources and initialize a scene state object.', - )), - ('mjv_freeSceneState', - FunctionDecl( - name='mjv_freeSceneState', - return_type=ValueType(name='void'), - parameters=( - FunctionParameterDecl( - name='scnstate', - type=PointerType( - inner_type=ValueType(name='mjvSceneState'), - ), - ), - ), - doc='Free scene state.', - )), - ('mjv_updateSceneState', - FunctionDecl( - name='mjv_updateSceneState', - return_type=ValueType(name='void'), - parameters=( - FunctionParameterDecl( - name='m', - type=PointerType( - inner_type=ValueType(name='mjModel', is_const=True), - ), - ), - FunctionParameterDecl( - name='d', - type=PointerType( - inner_type=ValueType(name='mjData'), - ), - ), - FunctionParameterDecl( - name='opt', - type=PointerType( - inner_type=ValueType(name='mjvOption', is_const=True), - ), - ), - FunctionParameterDecl( - name='scnstate', - type=PointerType( - inner_type=ValueType(name='mjvSceneState'), - ), - ), - ), - doc='Update a scene state from model and data.', - )), ('mjv_addGeoms', FunctionDecl( name='mjv_addGeoms', diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index 4aac02a9..379cdc84 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -7345,1651 +7345,6 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), ), )), - ('mjvSceneState', - StructDecl( - name='mjvSceneState', - declname='struct mjvSceneState_', - fields=( - StructFieldDecl( - name='nbuffer', - type=ValueType(name='int'), - doc='size of the buffer in bytes', - ), - StructFieldDecl( - name='buffer', - type=PointerType( - inner_type=ValueType(name='void'), - ), - doc='heap-allocated memory for all arrays in this struct', - ), - StructFieldDecl( - name='maxgeom', - type=ValueType(name='int'), - doc='maximum number of mjvGeom supported by this state object', - ), - StructFieldDecl( - name='scratch', - type=ValueType(name='mjvScene'), - doc='scratch space for vis geoms inserted by the user and plugins', # pylint: disable=line-too-long - ), - StructFieldDecl( - name='model', - type=AnonymousStructDecl( - fields=( - StructFieldDecl( - name='nv', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nu', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='na', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nbody', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nbvh', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nbvhstatic', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='njnt', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='ngeom', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nsite', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='ncam', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nlight', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nmesh', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nskin', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nflex', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nflexvert', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nflextexcoord', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nskinvert', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nskinface', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nskinbone', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nskinbonevert', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nmat', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='neq', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='ntendon', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='ntree', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nwrap', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nsensor', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nnames', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='npaths', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nsensordata', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='narena', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='opt', - type=ValueType(name='mjOption'), - doc='', - ), - StructFieldDecl( - name='vis', - type=ValueType(name='mjVisual'), - doc='', - ), - StructFieldDecl( - name='stat', - type=ValueType(name='mjStatistic'), - doc='', - ), - StructFieldDecl( - name='body_parentid', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='body_rootid', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='body_weldid', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='body_mocapid', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='body_jntnum', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='body_jntadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='body_dofnum', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='body_dofadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='body_geomnum', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='body_geomadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='body_iquat', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='body_mass', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='body_inertia', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='body_bvhadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='body_bvhnum', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='bvh_depth', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='bvh_child', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='bvh_nodeid', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='bvh_aabb', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='jnt_type', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='jnt_bodyid', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='jnt_group', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='geom_type', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='geom_bodyid', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='geom_contype', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='geom_conaffinity', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='geom_dataid', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='geom_matid', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='geom_group', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='geom_size', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='geom_aabb', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='geom_rbound', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='geom_rgba', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='site_type', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='site_bodyid', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='site_matid', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='site_group', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='site_size', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='site_rgba', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='cam_orthographic', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='cam_fovy', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='cam_ipd', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='cam_resolution', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='cam_sensorsize', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='cam_intrinsic', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='light_directional', - type=PointerType( - inner_type=ValueType(name='mjtByte'), - ), - doc='', - ), - StructFieldDecl( - name='light_castshadow', - type=PointerType( - inner_type=ValueType(name='mjtByte'), - ), - doc='', - ), - StructFieldDecl( - name='light_bulbradius', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='light_intensity', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='light_range', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='light_active', - type=PointerType( - inner_type=ValueType(name='mjtByte'), - ), - doc='', - ), - StructFieldDecl( - name='light_attenuation', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='light_cutoff', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='light_exponent', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='light_ambient', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='light_diffuse', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='light_specular', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='flex_flatskin', - type=PointerType( - inner_type=ValueType(name='mjtByte'), - ), - doc='', - ), - StructFieldDecl( - name='flex_dim', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_matid', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_group', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_interp', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_nodeadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_nodenum', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_nodebodyid', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_vertadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_vertnum', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_elem', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_elemtexcoord', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_elemlayer', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_elemadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_elemnum', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_elemdataadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_shell', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_shellnum', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_shelldataadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_texcoordadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_bvhadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_bvhnum', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flex_centered', - type=PointerType( - inner_type=ValueType(name='mjtByte'), - ), - doc='', - ), - StructFieldDecl( - name='flex_node', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='flex_radius', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='flex_rgba', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='flex_texcoord', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='hfield_pathadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='mesh_bvhadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='mesh_bvhnum', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='mesh_texcoordadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='mesh_graphadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='mesh_pathadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='skin_matid', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='skin_group', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='skin_rgba', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='skin_inflate', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='skin_vertadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='skin_vertnum', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='skin_texcoordadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='skin_faceadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='skin_facenum', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='skin_boneadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='skin_bonenum', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='skin_vert', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='skin_face', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='skin_bonevertadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='skin_bonevertnum', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='skin_bonebindpos', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='skin_bonebindquat', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='skin_bonebodyid', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='skin_bonevertid', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='skin_bonevertweight', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='skin_pathadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='tex_pathadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='mat_texid', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='mat_texuniform', - type=PointerType( - inner_type=ValueType(name='mjtByte'), - ), - doc='', - ), - StructFieldDecl( - name='mat_texrepeat', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='mat_emission', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='mat_specular', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='mat_shininess', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='mat_reflectance', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='mat_metallic', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='mat_roughness', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='mat_rgba', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='eq_type', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='eq_obj1id', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='eq_obj2id', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='eq_objtype', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='eq_data', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='tendon_num', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='tendon_matid', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='tendon_group', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='tendon_limited', - type=PointerType( - inner_type=ValueType(name='mjtByte'), - ), - doc='', - ), - StructFieldDecl( - name='tendon_actfrclimited', - type=PointerType( - inner_type=ValueType(name='mjtByte'), - ), - doc='', - ), - StructFieldDecl( - name='tendon_width', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='tendon_range', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='tendon_actfrcrange', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='tendon_stiffness', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='tendon_damping', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='tendon_frictionloss', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='tendon_lengthspring', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='tendon_rgba', - type=PointerType( - inner_type=ValueType(name='float'), - ), - doc='', - ), - StructFieldDecl( - name='actuator_trntype', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='actuator_dyntype', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='actuator_trnid', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='actuator_actadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='actuator_actnum', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='actuator_group', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='actuator_ctrllimited', - type=PointerType( - inner_type=ValueType(name='mjtByte'), - ), - doc='', - ), - StructFieldDecl( - name='actuator_actlimited', - type=PointerType( - inner_type=ValueType(name='mjtByte'), - ), - doc='', - ), - StructFieldDecl( - name='actuator_ctrlrange', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='actuator_actrange', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='actuator_cranklength', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='sensor_type', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='sensor_objid', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='sensor_adr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='name_bodyadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='name_jntadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='name_geomadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='name_siteadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='name_camadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='name_lightadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='name_eqadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='name_tendonadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='name_actuatoradr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='names', - type=PointerType( - inner_type=ValueType(name='char'), - ), - doc='', - ), - StructFieldDecl( - name='paths', - type=PointerType( - inner_type=ValueType(name='char'), - ), - doc='', - ), - ), - ), - doc='', - ), - StructFieldDecl( - name='data', - type=AnonymousStructDecl( - fields=( - StructFieldDecl( - name='warning', - type=ArrayType( - inner_type=ValueType(name='mjWarningStat'), - extents=(8,), - ), - doc='', - ), - StructFieldDecl( - name='nefc', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='ncon', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='nisland', - type=ValueType(name='int'), - doc='', - ), - StructFieldDecl( - name='time', - type=ValueType(name='mjtNum'), - doc='', - ), - StructFieldDecl( - name='act', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='ctrl', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='xfrc_applied', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='eq_active', - type=PointerType( - inner_type=ValueType(name='mjtByte'), - ), - doc='', - ), - StructFieldDecl( - name='sensordata', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='xpos', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='xquat', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='xmat', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='xipos', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='ximat', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='xanchor', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='xaxis', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='geom_xpos', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='geom_xmat', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='site_xpos', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='site_xmat', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='cam_xpos', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='cam_xmat', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='light_xpos', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='light_xdir', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='subtree_com', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='ten_wrapadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='ten_wrapnum', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='wrap_obj', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='ten_length', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='wrap_xpos', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='bvh_aabb_dyn', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='bvh_active', - type=PointerType( - inner_type=ValueType(name='mjtByte'), - ), - doc='', - ), - StructFieldDecl( - name='island_dofadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='dof_island', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='efc_island', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='tendon_efcadr', - type=PointerType( - inner_type=ValueType(name='int'), - ), - doc='', - ), - StructFieldDecl( - name='flexvert_xpos', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='contact', - type=PointerType( - inner_type=ValueType(name='mjContact'), - ), - doc='', - ), - StructFieldDecl( - name='efc_force', - type=PointerType( - inner_type=ValueType(name='mjtNum'), - ), - doc='', - ), - StructFieldDecl( - name='arena', - type=PointerType( - inner_type=ValueType(name='void'), - ), - doc='', - ), - ), - ), - doc='', - ), - ), - )), ('mjSDF', StructDecl( name='mjSDF', diff --git a/simulate/simulate.cc b/simulate/simulate.cc index bd255ce3..77db1a04 100644 --- a/simulate/simulate.cc +++ b/simulate/simulate.cc @@ -689,7 +689,7 @@ void UpdateWatch(mj::Simulate* sim, const mjModel* m, const mjData* d) { // make physics section of UI void MakePhysicsSection(mj::Simulate* sim) { - mjOption* opt = sim->is_passive_ ? &sim->scnstate_.model.opt : &sim->m_->opt; + mjOption* opt = sim->is_passive_ ? &sim->m_passive_->opt : &sim->m_->opt; mjuiDef defPhysics[] = { {mjITEM_SECTION, "Physics", mjPRESERVE, nullptr, "AP"}, {mjITEM_SELECT, "Integrator", 2, &(opt->integrator), "Euler\nRK4\nimplicit\nimplicitfast"}, @@ -873,8 +873,8 @@ void MakeRenderingSection(mj::Simulate* sim, const mjModel* m) { // make visualization section of UI void MakeVisualizationSection(mj::Simulate* sim, const mjModel* m) { - mjStatistic* stat = sim->is_passive_ ? &sim->scnstate_.model.stat : &sim->m_->stat; - mjVisual* vis = sim->is_passive_ ? &sim->scnstate_.model.vis : &sim->m_->vis; + mjStatistic* stat = sim->is_passive_ ? &sim->m_passive_->stat : &sim->m_->stat; + mjVisual* vis = sim->is_passive_ ? &sim->m_passive_->vis : &sim->m_->vis; mjuiDef defVisualization[] = { {mjITEM_SECTION, "Visualization", mjPRESERVE, nullptr, "AV"}, @@ -1503,7 +1503,7 @@ void UiEvent(mjuiState* state) { // physics section else if (it && it->sectionid==SECT_PHYSICS && sim->m_) { - mjOption* opt = sim->is_passive_ ? &sim->scnstate_.model.opt : &sim->m_->opt; + mjOption* opt = sim->is_passive_ ? &sim->m_passive_->opt : &sim->m_->opt; // update disable flags in mjOption opt->disableflags = 0; @@ -1765,15 +1765,14 @@ void UiEvent(mjuiState* state) { return; } + // local pointers used below + mjModel* model = sim->is_passive_ ? sim->m_passive_ : sim->m_; + mjData* data = sim->is_passive_ ? sim->d_passive_ : sim->d_; + // 3D scroll - if (state->type==mjEVENT_SCROLL && state->mouserect==3) { + if (state->type==mjEVENT_SCROLL && state->mouserect==3 && model) { // emulate vertical mouse motion = 2% of window height - if (sim->m_ && !sim->is_passive_) { - mjv_moveCamera(sim->m_, mjMOUSE_ZOOM, 0, -zoom_increment*state->sy, &sim->scn, &sim->cam); - } else { - mjv_moveCameraFromState( - &sim->scnstate_, mjMOUSE_ZOOM, 0, -zoom_increment*state->sy, &sim->scn, &sim->cam); - } + mjv_moveCamera(model, mjMOUSE_ZOOM, 0, -zoom_increment*state->sy, &sim->scn, &sim->cam); return; } @@ -1829,25 +1828,11 @@ void UiEvent(mjuiState* state) { // move perturb or camera mjrRect r = state->rect[3]; if (sim->pert.active) { - if (!sim->is_passive_) { - mjv_movePerturb( - sim->m_, sim->d_, action, state->dx / r.height, -state->dy / r.height, - &sim->scn, &sim->pert); - } else { - mjv_movePerturbFromState( - &sim->scnstate_, action, state->dx / r.height, -state->dy / r.height, - &sim->scn, &sim->pert); - } + mjv_movePerturb(model, data, action, state->dx / r.height, -state->dy / r.height, + &sim->scn, &sim->pert); } else { - if (!sim->is_passive_) { - mjv_moveCamera( - sim->m_, action, state->dx / r.height, -state->dy / r.height, - &sim->scn, &sim->cam); - } else { - mjv_moveCameraFromState( - &sim->scnstate_, action, state->dx / r.height, -state->dy / r.height, - &sim->scn, &sim->cam); - } + mjv_moveCamera(model, action, state->dx / r.height, -state->dy / r.height, + &sim->scn, &sim->cam); } return; } @@ -1881,10 +1866,11 @@ Simulate::Simulate(std::unique_ptr platform_ui, platform_ui(std::move(platform_ui)), uistate(this->platform_ui->state()) { mjv_defaultScene(&scn); - mjv_defaultSceneState(&scnstate_); } -// synchronize model and data + +//------------------------- Synchronize render and physics threads --------------------------------- + // operations which require holding the mutex, prevents racing with physics thread void Simulate::Sync() { MutexLock lock(this->mtx); @@ -1947,10 +1933,10 @@ void Simulate::Sync() { if (is_passive_) { // synchronize m_->opt with changes made via the UI -#define X(name) \ - if (IsDifferent(scnstate_.model.opt.name, mjopt_prev_.name)) { \ - pending_.ui_update_physics = true; \ - Copy(m_->opt.name, scnstate_.model.opt.name); \ +#define X(name) \ + if (IsDifferent(m_passive_->opt.name, mjopt_prev_.name)) { \ + pending_.ui_update_physics = true; \ + Copy(m_->opt.name, m_passive_->opt.name); \ } X(timestep); @@ -1984,9 +1970,9 @@ void Simulate::Sync() { #undef X // synchronize number of mjWARN_VGEOMFULL warnings - if (scnstate_.data.warning[mjWARN_VGEOMFULL].number > warn_vgeomfull_prev_) { + if (d_passive_->warning[mjWARN_VGEOMFULL].number > warn_vgeomfull_prev_) { d_->warning[mjWARN_VGEOMFULL].number += - scnstate_.data.warning[mjWARN_VGEOMFULL].number - warn_vgeomfull_prev_; + d_passive_->warning[mjWARN_VGEOMFULL].number - warn_vgeomfull_prev_; } } @@ -2135,25 +2121,19 @@ void Simulate::Sync() { pending_.select = false; } - // update scene + // update scene or sync data from user in passive mode if (!is_passive_) { mjv_updateScene(m_, d_, &this->opt, &this->pert, &this->cam, mjCAT_ALL, &this->scn); } else { - mjv_updateSceneState(m_, d_, &this->opt, &scnstate_); + mjv_copyModel(m_passive_, m_); + mjv_copyData(d_passive_, m_passive_, d_); - // append geoms from user_scn to scnstate_ scratch space + // append geoms from user_scn to scratch space if (user_scn) { - int ngeom = user_scn->ngeom; - int maxgeom = scnstate_.scratch.maxgeom - scnstate_.scratch.ngeom; - if (ngeom > maxgeom) { - mj_warning(d_, mjWARN_VGEOMFULL, scnstate_.scratch.maxgeom); - ngeom = maxgeom; - } - if (ngeom > 0) { - std::memcpy(scnstate_.scratch.geoms + scnstate_.scratch.ngeom, - user_scn->geoms, - sizeof(mjvGeom) * ngeom); - scnstate_.scratch.ngeom += ngeom; + user_scn_geoms_.clear(); + user_scn_geoms_.reserve(user_scn->ngeom); + for (int i = 0; i < user_scn->ngeom; ++i) { + user_scn_geoms_.push_back(user_scn->geoms[i]); } } @@ -2169,8 +2149,8 @@ void Simulate::Sync() { Copy(user_scn_flags_prev_, user_scn->flags); } - mjopt_prev_ = scnstate_.model.opt; - warn_vgeomfull_prev_ = scnstate_.data.warning[mjWARN_VGEOMFULL].number; + mjopt_prev_ = m_passive_->opt; + warn_vgeomfull_prev_ = d_passive_->warning[mjWARN_VGEOMFULL].number; } // update settings @@ -2329,15 +2309,8 @@ void Simulate::LoadOnRenderThread() { } } - // re-create scene and context + // re-create scene mjv_makeScene(this->m_, &this->scn, kMaxGeom); - if (this->is_passive_) { - mjopt_prev_ = m_->opt; - opt_prev_ = opt; - cam_prev_ = cam; - warn_vgeomfull_prev_ = d_->warning[mjWARN_VGEOMFULL].number; - mjv_makeSceneState(this->m_, this->d_, &this->scnstate_, kMaxGeom); - } this->platform_ui->RefreshMjrContext(this->m_, 50*(this->font+1)); UiModify(&this->ui0, &this->uistate, &this->platform_ui->mjr_context()); @@ -2366,12 +2339,18 @@ void Simulate::LoadOnRenderThread() { mju::strcpy_arr(this->previous_filename, this->filename); } - // update scene + // update scene in managed mode, in passive mode copy data from user (update in RenderLoop) if (!is_passive_) { - mjv_updateScene(this->m_, this->d_, - &this->opt, &this->pert, &this->cam, mjCAT_ALL, &this->scn); + mjv_updateScene(this->m_, this->d_, &this->opt, &this->pert, &this->cam, mjCAT_ALL, &this->scn); } else { - mjv_updateSceneState(this->m_, this->d_, &this->opt, &this->scnstate_); + mjopt_prev_ = m_->opt; + opt_prev_ = opt; + cam_prev_ = cam; + warn_vgeomfull_prev_ = d_->warning[mjWARN_VGEOMFULL].number; + + // full copy on init + m_passive_ = mj_copyModel(nullptr, m_); + d_passive_ = mj_copyData(nullptr, m_passive_, d_); } // set window title to model name @@ -2793,11 +2772,22 @@ void Simulate::RenderLoop() { } // update scene, doing a full sync if in fully managed mode - if (!this->is_passive_) { + if (!is_passive_) { Sync(); - } else { - scnstate_.data.warning[mjWARN_VGEOMFULL].number += mjv_updateSceneFromState( - &scnstate_, &this->opt, &this->pert, &this->cam, mjCAT_ALL, &this->scn); + } else if (m_passive_ && d_passive_) { + // the user has called Sync() in their code + mjv_updateScene(m_passive_, d_passive_, + &this->opt, &this->pert, &this->cam, mjCAT_ALL, &this->scn); + + // add user geoms to scene + int nusergeom = user_scn_geoms_.size(); + int ngeom = std::min(nusergeom, this->scn.maxgeom - this->scn.ngeom); + if (ngeom < nusergeom) { + mj_warning(d_passive_, mjWARN_VGEOMFULL, this->scn.maxgeom); + } + std::memcpy(this->scn.geoms + this->scn.ngeom, user_scn_geoms_.data(), + ngeom * sizeof(mjvGeom)); + this->scn.ngeom += ngeom; } } // MutexLock (unblocks simulation thread) @@ -2818,7 +2808,8 @@ void Simulate::RenderLoop() { const MutexLock lock(this->mtx); mjv_freeScene(&this->scn); if (is_passive_) { - mjv_freeSceneState(&scnstate_); + mj_deleteData(d_passive_); + mj_deleteModel(m_passive_); } this->exitrequest.store(2); diff --git a/simulate/simulate.h b/simulate/simulate.h index 717d1871..7e20d3c5 100644 --- a/simulate/simulate.h +++ b/simulate/simulate.h @@ -130,7 +130,12 @@ class Simulate { std::vector ctrl_; std::vector ctrl_prev_; - mjvSceneState scnstate_; + // in passive mode the user owns m_ and d_, these "passive" instances are + // owned by Simulate, updated from the user by the Sync() method + mjModel* m_passive_; + mjData* d_passive_; + std::vector user_scn_geoms_; + mjOption mjopt_prev_; mjvOption opt_prev_; mjvCamera cam_prev_; diff --git a/src/engine/CMakeLists.txt b/src/engine/CMakeLists.txt index 8957c4c6..ea4b2dd6 100644 --- a/src/engine/CMakeLists.txt +++ b/src/engine/CMakeLists.txt @@ -83,8 +83,6 @@ set(MUJOCO_ENGINE_SRCS engine_vis_init.h engine_vis_interact.c engine_vis_interact.h - engine_vis_state.c - engine_vis_state.h engine_vis_visualize.c engine_vis_visualize.h ) diff --git a/src/engine/engine_vis_state.c b/src/engine/engine_vis_state.c deleted file mode 100644 index ff305f05..00000000 --- a/src/engine/engine_vis_state.c +++ /dev/null @@ -1,409 +0,0 @@ -// Copyright 2023 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. - -#include "engine/engine_vis_state.h" - -#include - -#include -#include -#include -#include -#include -#include "engine/engine_core_constraint.h" -#include "engine/engine_plugin.h" -#include "engine/engine_support.h" -#include "engine/engine_util_errmem.h" -#include "engine/engine_vis_init.h" -#include "engine/engine_vis_interact.h" -#include "engine/engine_vis_visualize.h" - -#ifdef MEMORY_SANITIZER - #include -#endif - -// this source file needs to treat XMJV differently from other X macros -#undef XMJV - - - -// round size up to multiples of 64-byte cache lines -static inline size_t roundUpToCacheLine(size_t n) { - return 64 * ((n / 64) + (n % 64 ? 1 : 0)); -} - - - -// set default scene -void mjv_defaultSceneState(mjvSceneState* scnstate) { - memset(scnstate, 0, sizeof(mjvSceneState)); - mjv_defaultScene(&scnstate->scratch); -} - - - -// allocate and init scene state -void mjv_makeSceneState(const mjModel* m, const mjData* d, mjvSceneState* scnstate, int maxgeom) { - mjv_freeScene(&scnstate->scratch); - mju_free(scnstate->buffer); - -#ifdef MEMORY_SANITIZER - __msan_allocated_memory(scnstate, sizeof(mjvSceneState)); - mjv_defaultScene(&scnstate->scratch); -#endif - - scnstate->nbuffer = 0; - scnstate->maxgeom = maxgeom; - -#define X(var) -#define XMJV(var) scnstate->model.var = m->var; - MJMODEL_INTS -#undef XMJV -#undef X - -#define X(dtype, var, dim0, dim1) -#define XMJV(dtype, var, dim0, dim1) \ - scnstate->nbuffer += roundUpToCacheLine(sizeof(dtype) * m->dim0 * dim1); - MJMODEL_POINTERS -#undef XMJV -#undef X - -#define X(dtype, var, dim0, dim1) -#define XMJV(dtype, var, dim0, dim1) \ - scnstate->nbuffer += roundUpToCacheLine(sizeof(dtype) * m->dim0 * dim1); - MJDATA_POINTERS -#undef XMJV -#undef X - - // create an arena in the scnstate, to allow visualization code to use the stack. - // TODO: Consider allocating way less than narena, since stack allocations in - // visualization code are much smaller than the arena space required by the model, - // typically. - scnstate->nbuffer += roundUpToCacheLine(m->narena); - // buffer space required for contacts - int condimmax = mj_isPyramidal(m) ? 10 : 6; - scnstate->nbuffer += roundUpToCacheLine(sizeof(*d->contact) * maxgeom); - scnstate->nbuffer += roundUpToCacheLine(sizeof(*d->efc_force) * maxgeom * condimmax); - - // buffer space required for islands - scnstate->nbuffer += roundUpToCacheLine(sizeof(*d->island_dofadr) * m->ntree); - scnstate->nbuffer += roundUpToCacheLine(sizeof(*d->dof_island) * m->nv); - scnstate->nbuffer += roundUpToCacheLine(sizeof(*d->efc_island) * maxgeom * condimmax); - scnstate->nbuffer += roundUpToCacheLine(sizeof(*d->tendon_efcadr) * m->ntendon); - - scnstate->buffer = mju_malloc(scnstate->nbuffer); - - char* ptr = scnstate->buffer; - -#define X(dtype, var, dim0, dim1) -#define XMJV(dtype, var, dim0, dim1) \ - scnstate->model.var = (dtype*)ptr; \ - ptr += roundUpToCacheLine(sizeof(dtype) * m->dim0 * dim1); - MJMODEL_POINTERS -#undef XMJV -#undef X - -#define X(dtype, var, dim0, dim1) -#define XMJV(dtype, var, dim0, dim1) \ - scnstate->data.var = (dtype*)ptr; \ - ptr += roundUpToCacheLine(sizeof(dtype) * m->dim0 * dim1); - MJDATA_POINTERS -#undef XMJV -#undef X - - scnstate->model.narena = m->narena; - scnstate->data.arena = (void*)ptr; - ptr += roundUpToCacheLine(m->narena); - - scnstate->data.contact = (mjContact*)ptr; - ptr += roundUpToCacheLine(sizeof(*scnstate->data.contact) * scnstate->maxgeom); - - scnstate->data.efc_force = (mjtNum*)ptr; - ptr += roundUpToCacheLine(sizeof(*scnstate->data.efc_force) * scnstate->maxgeom * condimmax); - - scnstate->data.island_dofadr = (int*)ptr; - ptr += roundUpToCacheLine(sizeof(*scnstate->data.island_dofadr) * scnstate->model.ntree); - - scnstate->data.dof_island = (int*)ptr; - ptr += roundUpToCacheLine(sizeof(*scnstate->data.dof_island) * scnstate->model.nv); - - scnstate->data.efc_island = (int*)ptr; - ptr += roundUpToCacheLine(sizeof(*scnstate->data.efc_island) * scnstate->maxgeom * condimmax); - - scnstate->data.tendon_efcadr = (int*)ptr; - ptr += roundUpToCacheLine(sizeof(*scnstate->data.tendon_efcadr) * m->ntendon); - - // should not occur - if (ptr - (char*)scnstate->buffer != scnstate->nbuffer) { - mjERROR("mjvSceneState buffer is not fully used"); - } - - mjv_makeScene(m, &scnstate->scratch, maxgeom); -} - - - -// free scene state -void mjv_freeSceneState(mjvSceneState* scnstate) { - mjv_freeScene(&scnstate->scratch); - mju_free(scnstate->buffer); - mjv_defaultSceneState(scnstate); -} - - - -// shallow copy scene state into model and data for use with mjv functions -void mjv_assignFromSceneState(const mjvSceneState* scnstate, mjModel* m, mjData* d) { - if (m) { - memset(m, 0, sizeof(mjModel)); - -#ifdef MEMORY_SANITIZER - // Tell msan to treat the entire buffer as uninitialized - __msan_allocated_memory(m, sizeof(mjModel)); -#endif - -#define X(var) -#define XMJV(var) m->var = scnstate->model.var; - MJMODEL_INTS -#undef XMJV -#undef X - - m->opt = scnstate->model.opt; - m->vis = scnstate->model.vis; - m->stat = scnstate->model.stat; - m->narena = scnstate->model.narena; - -#define X(dtype, var, dim0, dim1) -#define XMJV(dtype, var, dim0, dim1) m->var = scnstate->model.var; - MJMODEL_POINTERS -#undef XMJV -#undef X - } - - if (d) { - memset(d, 0, sizeof(mjData)); - -#ifdef MEMORY_SANITIZER - // Tell msan to treat the entire buffer as uninitialized - __msan_allocated_memory(d, sizeof(mjData)); -#endif - - memcpy(d->warning, scnstate->data.warning, sizeof(d->warning)); - d->threadpool = 0; - d->nefc = scnstate->data.nefc; - d->ncon = scnstate->data.ncon; - d->nisland = scnstate->data.nisland; - d->time = scnstate->data.time; - d->narena = scnstate->model.narena; - d->arena = scnstate->data.arena; - d->parena = 0; - d->pbase = 0; - d->pstack = 0; - - #define X(dtype, var, dim0, dim1) - #define XMJV(dtype, var, dim0, dim1) d->var = scnstate->data.var; - MJDATA_POINTERS - #undef XMJV - #undef X - - d->contact = scnstate->data.contact; - d->efc_force = scnstate->data.efc_force; - d->island_dofadr = scnstate->data.island_dofadr; - d->dof_island = scnstate->data.dof_island; - d->efc_island = scnstate->data.efc_island; - d->tendon_efcadr = scnstate->data.tendon_efcadr; - } -} - - - -// update entire scene from a scene state, return the number of new mjWARN_VGEOMFULL warnings -int mjv_updateSceneFromState(const mjvSceneState* scnstate, const mjvOption* opt, - const mjvPerturb* pert, mjvCamera* cam, int catmask, mjvScene* scn) { - // shallow-copy scnstate pointers into mjModel and mjData - mjModel m; - mjData d; - mjv_assignFromSceneState(scnstate, &m, &d); - - // save the number of mjWARN_VGEOMFULL warnings before the scene update - int warning_start = d.warning[mjWARN_VGEOMFULL].number; - - // copy mjvGeoms added by plugins - int nplugingeom = scnstate->scratch.ngeom; - if (nplugingeom > scn->maxgeom) { - mj_warning(&d, mjWARN_VGEOMFULL, scn->maxgeom); - scn->ngeom = scn->maxgeom; - } else { - scn->ngeom = nplugingeom; - } - memcpy(scn->geoms, scnstate->scratch.geoms, sizeof(mjvGeom) * scn->ngeom); - - // add all categories - mjv_addGeoms(&m, &d, opt, pert, catmask, scn); - - // update camera - mjv_updateCamera(&m, &d, cam, scn); - - // add lights - mjv_makeLights(&m, &d, scn); - - // update flexes - if (opt->flags[mjVIS_FLEXVERT] || opt->flags[mjVIS_FLEXEDGE] || - opt->flags[mjVIS_FLEXFACE] || opt->flags[mjVIS_FLEXSKIN]) { - mjv_updateActiveFlex(&m, &d, scn, opt); - } - - // update skins - if (opt->flags[mjVIS_SKIN]) { - mjv_updateActiveSkin(&m, &d, scn, opt); - } - - // return the number of new mjWARN_VGEOMFULL warnings generated - return d.warning[mjWARN_VGEOMFULL].number - warning_start; -} - - - -// update a scene state from model and data -void mjv_updateSceneState(const mjModel* m, mjData* d, const mjvOption* opt, - mjvSceneState* scnstate) { - // Check that mjModel sizes haven't changed. -#define X(var) -#define XMJV(var) \ - if (scnstate->model.var != m->var) { \ - mjERROR("m->%s changed: %d vs %d", #var, scnstate->model.var, m->var); \ - } - MJMODEL_INTS -#undef XMJV -#undef X - - // Update plugin visualization cache. - scnstate->scratch.ngeom = 0; - if (m->nplugin) { - const int nslot = mjp_pluginCount(); - // iterate over plugins, call visualize if defined - for (int i=0; i < m->nplugin; i++) { - const int slot = m->plugin[i]; - const mjpPlugin* plugin = mjp_getPluginAtSlotUnsafe(slot, nslot); - if (!plugin) { - mjERROR("invalid plugin slot: %d", slot); - } - if (plugin->visualize) { - plugin->visualize(m, d, opt, &scnstate->scratch, i); - } - } - } - - // Copy variable-sized arrays in mjModel. -#define X(dtype, var, dim0, dim1) -#define XMJV(dtype, var, dim0, dim1) \ - memcpy(scnstate->model.var, m->var, sizeof(dtype) * m->dim0 * dim1); - MJMODEL_POINTERS -#undef XMJV -#undef X - - scnstate->model.opt = m->opt; - scnstate->model.vis = m->vis; - scnstate->model.stat = m->stat; - - // Copy mjData variables. - memcpy(scnstate->data.warning, d->warning, sizeof(d->warning)); - scnstate->data.time = d->time; - - // Copy variable-sized arrays in mjData. -#define X(dtype, var, dim0, dim1) -#define XMJV(dtype, var, dim0, dim1) \ - memcpy(scnstate->data.var, d->var, sizeof(dtype) * m->dim0 * dim1); - MJDATA_POINTERS -#undef XMJV -#undef X - - // Copy contacts. - { - if (d->ncon > scnstate->maxgeom) { - mj_warning(d, mjWARN_VGEOMFULL, scnstate->maxgeom); - scnstate->data.ncon = scnstate->maxgeom; - } else { - scnstate->data.ncon = d->ncon; - } - memcpy(scnstate->data.contact, d->contact, sizeof(*d->contact) * scnstate->data.ncon); - } - - // Copy only the entries in efc_force and efc_island that correspond to contacts. - { - scnstate->data.nefc = 0; - for (int i = 0; i < scnstate->data.ncon; ++i) { - const mjContact* con = &d->contact[i]; - scnstate->data.nefc += con->dim; - } - scnstate->data.nefc += scnstate->model.ntendon; - - int efc_address = 0; - int ispyramid = mj_isPyramidal(m); - for (int i = 0; i < scnstate->data.ncon; ++i) { - mjContact* con = &scnstate->data.contact[i]; - int dim = con->dim; - if (ispyramid && dim > 1){ - dim = 2*(dim - 1); - } - for (int j = 0; j < dim; ++j) { - scnstate->data.efc_force[efc_address + j] = d->efc_force[con->efc_address + j]; - if (d->nisland) { - scnstate->data.efc_island[efc_address + j] = d->efc_island[con->efc_address + j]; - } - } - con->efc_address = efc_address; - efc_address += dim; - } - if (d->nisland) { - for (int i = 0; i < scnstate->model.ntendon; ++i) { - int efcadr = d->tendon_efcadr[i]; - if (efcadr != -1) { - scnstate->data.efc_island[efcadr] = d->efc_island[efcadr]; - } - } - } - } - - // Copy island data. - scnstate->data.nisland = d->nisland; - if (d->nisland) { - memcpy(scnstate->data.island_dofadr, d->island_dofadr, sizeof(*d->island_dofadr) * d->nisland); - memcpy(scnstate->data.dof_island, d->dof_island, sizeof(*d->dof_island) * m->nv); - memcpy(scnstate->data.tendon_efcadr, d->tendon_efcadr, sizeof(*d->tendon_efcadr) * m->ntendon); - } -} - - - -// move camera with mouse given a scene state; action is mjtMouse -void mjv_moveCameraFromState(const mjvSceneState* scnstate, int action, - mjtNum reldx, mjtNum reldy, - const mjvScene* scn, mjvCamera* cam) { - mjModel m; - mjv_assignFromSceneState(scnstate, &m, NULL); - mjv_moveCamera(&m, action, reldx, reldy, scn, cam); -} - - - -// move perturb object with mouse given a scene state; action is mjtMouse -void mjv_movePerturbFromState(const mjvSceneState* scnstate, int action, - mjtNum reldx, mjtNum reldy, - const mjvScene* scn, mjvPerturb* pert) { - mjModel m; - mjData d; - mjv_assignFromSceneState(scnstate, &m, &d); - mjv_movePerturb(&m, &d, action, reldx, reldy, scn, pert); -} diff --git a/src/engine/engine_vis_state.h b/src/engine/engine_vis_state.h deleted file mode 100644 index 7e1abfbe..00000000 --- a/src/engine/engine_vis_state.h +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2023 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. - -#ifndef MUJOCO_SRC_ENGINE_ENGINE_VIS_STATE_H_ -#define MUJOCO_SRC_ENGINE_ENGINE_VIS_STATE_H_ - -#include -#include -#include -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif -// set default scene state -MJAPI void mjv_defaultSceneState(mjvSceneState* scnstate); - -// allocate and init scene state -MJAPI void mjv_makeSceneState(const mjModel* m, const mjData* d, - mjvSceneState* scnstate, int maxgeom); - -// free scene state -MJAPI void mjv_freeSceneState(mjvSceneState* scnstate); - -// shallow copy scene state into model and data for use with mjv functions -void mjv_assignFromSceneState(const mjvSceneState* scnstate, mjModel* m, mjData* d); - -// update entire scene from a scene state, return the number of new mjWARN_VGEOMFULL warnings -MJAPI int mjv_updateSceneFromState(const mjvSceneState* scnstate, const mjvOption* opt, - const mjvPerturb* pert, mjvCamera* cam, int catmask, - mjvScene* scn); - -// update a scene state from model and data -MJAPI void mjv_updateSceneState(const mjModel* m, mjData* d, const mjvOption* opt, - mjvSceneState* scnstate); - -// move camera with mouse given a scene state; action is mjtMouse -MJAPI void mjv_moveCameraFromState(const mjvSceneState* scnstate, int action, - mjtNum reldx, mjtNum reldy, - const mjvScene* scn, mjvCamera* cam); - -// move perturb object with mouse given a scene state; action is mjtMouse -MJAPI void mjv_movePerturbFromState(const mjvSceneState* scnstate, int action, - mjtNum reldx, mjtNum reldy, - const mjvScene* scn, mjvPerturb* pert); - -#ifdef __cplusplus -} -#endif - -#endif // MUJOCO_SRC_ENGINE_ENGINE_VIS_STATE_H_ diff --git a/test/engine/CMakeLists.txt b/test/engine/CMakeLists.txt index 2acdc4b9..1884ae05 100644 --- a/test/engine/CMakeLists.txt +++ b/test/engine/CMakeLists.txt @@ -67,11 +67,4 @@ mujoco_test(engine_util_solve_test) mujoco_test(engine_util_spatial_test) -mujoco_test( - engine_vis_state_test - PROPERTIES - ENVIRONMENT - "MUJOCO_PLUGIN_DIR=$" -) - mujoco_test(engine_vis_visualize_test) diff --git a/test/engine/engine_vis_state_test.cc b/test/engine/engine_vis_state_test.cc deleted file mode 100644 index 6fefde47..00000000 --- a/test/engine/engine_vis_state_test.cc +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2023 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. - -#include -#include - -#include -#include -#include -#include -#include "test/fixture.h" - -namespace mujoco { -namespace { - -using ::testing::NotNull; -using MjvSceneStateTest = MujocoTest; - -constexpr int kMaxGeom = 10000; - -static const char* const kHammockPath = - "engine/testdata/hammock/hammock.xml"; -static const char* const kTendonPath = - "engine/testdata/island/tendon_wrap.xml"; -static const char* const kFrustumPath = - "engine/testdata/vis_visualize/frustum.xml"; -static const char* const kFlex = "testdata/flex.xml"; -static const char* const kModelPath = "testdata/model.xml"; - -#define EXPECT_ZERO(exp) EXPECT_EQ(0, exp); - -TEST_F(MjvSceneStateTest, CanUpdateFromState) { - for (const char* path : - {kHammockPath, kTendonPath, kModelPath, kFrustumPath, kFlex}) { - const std::string xml_path = GetTestDataFilePath(path); - mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, 0, 0); - ASSERT_THAT(model, NotNull()) << "Failed to load model from " << path; - mjData* data = mj_makeData(model); - - while (data->time < 2) { - mj_step(model, data); - } - - mjvScene scn1; - mjv_defaultScene(&scn1); - mjv_makeScene(model, &scn1, kMaxGeom); - - mjvOption opt; - mjv_defaultOption(&opt); - - mjvPerturb pert; - mjv_defaultPerturb(&pert); - - mjvCamera cam; - mjv_defaultFreeCamera(model, &cam); - - // Enable all flags to exercise all code paths - for (int i = 0; i < mjNVISFLAG; ++i) { - opt.flags[i] = 1; - } - - mjv_updateScene(model, data, &opt, &pert, &cam, mjCAT_ALL, &scn1); - EXPECT_GT(scn1.ngeom, 0); - if (model->nskin) EXPECT_GT(scn1.nskin, 0); - EXPECT_GT(scn1.nlight, 0); - - mjvSceneState scnstate; - mjv_defaultSceneState(&scnstate); - mjv_makeSceneState(model, data, &scnstate, kMaxGeom); - mjv_updateSceneState(model, data, &opt, &scnstate); - - mjvScene scn2; - mjv_defaultScene(&scn2); - mjv_makeScene(model, &scn2, kMaxGeom); - mjv_updateSceneFromState(&scnstate, &opt, &pert, &cam, mjCAT_ALL, &scn2); - - EXPECT_EQ(scn1.ngeom, scn2.ngeom); - for (int i = 0; i < scn1.ngeom; ++i) { - EXPECT_ZERO(std::memcmp(&scn1.geoms[i], &scn2.geoms[i], sizeof(mjvGeom))); - } - // NB: scn->geomorder is a scratch space for use by mjr_render, so we don't - // need to compare them here. - - EXPECT_LE(scn1.nskin, scn2.nskin); - EXPECT_ZERO(std::memcmp(scn1.skinfacenum, scn2.skinfacenum, - sizeof(*scn2.skinfacenum) * scn2.nskin)); - EXPECT_ZERO(std::memcmp(scn1.skinvertadr, scn2.skinvertadr, - sizeof(*scn2.skinvertadr) * scn2.nskin)); - EXPECT_ZERO(std::memcmp(scn1.skinvertnum, scn2.skinvertnum, - sizeof(*scn2.skinvertnum) * scn2.nskin)); - EXPECT_ZERO(std::memcmp(scn1.skinvert, scn2.skinvert, - sizeof(*scn2.skinvert) * scn2.nskin)); - EXPECT_ZERO(std::memcmp(scn1.skinnormal, scn2.skinnormal, - sizeof(*scn2.skinnormal) * scn2.nskin)); - - auto scn1_cmp_begin = reinterpret_cast(&scn1.nlight); - auto scn2_cmp_begin = reinterpret_cast(&scn2.nlight); - auto cmp_bytes = - sizeof(mjvScene) - (scn2_cmp_begin - reinterpret_cast(&scn2)); - EXPECT_ZERO(std::memcmp(scn1_cmp_begin, scn2_cmp_begin, cmp_bytes)); - - mjv_freeScene(&scn1); - mjv_freeScene(&scn2); - mjv_freeSceneState(&scnstate); - - mj_deleteData(data); - mj_deleteModel(model); - } -} - -} // namespace -} // namespace mujoco diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index cdeeca54..dc7b8078 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -6276,270 +6276,6 @@ public unsafe struct mjvFigure_ { public fixed int yaxispixel[2]; public fixed float xaxisdata[2]; public fixed float yaxisdata[2]; -} - -[StructLayout(LayoutKind.Sequential)] -public unsafe struct model { - public int nv; - public int nu; - public int na; - public int nbody; - public int nbvh; - public int nbvhstatic; - public int njnt; - public int ngeom; - public int nsite; - public int ncam; - public int nlight; - public int nmesh; - public int nskin; - public int nflex; - public int nflexvert; - public int nflextexcoord; - public int nskinvert; - public int nskinface; - public int nskinbone; - public int nskinbonevert; - public int nmat; - public int neq; - public int ntendon; - public int ntree; - public int nwrap; - public int nsensor; - public int nnames; - public int npaths; - public int nsensordata; - public int narena; - public mjOption_ opt; - public mjVisual_ vis; - public mjStatistic_ stat; - public int* body_parentid; - public int* body_rootid; - public int* body_weldid; - public int* body_mocapid; - public int* body_jntnum; - public int* body_jntadr; - public int* body_dofnum; - public int* body_dofadr; - public int* body_geomnum; - public int* body_geomadr; - public double* body_iquat; - public double* body_mass; - public double* body_inertia; - public int* body_bvhadr; - public int* body_bvhnum; - public int* bvh_depth; - public int* bvh_child; - public int* bvh_nodeid; - public double* bvh_aabb; - public int* jnt_type; - public int* jnt_bodyid; - public int* jnt_group; - public int* geom_type; - public int* geom_bodyid; - public int* geom_contype; - public int* geom_conaffinity; - public int* geom_dataid; - public int* geom_matid; - public int* geom_group; - public double* geom_size; - public double* geom_aabb; - public double* geom_rbound; - public float* geom_rgba; - public int* site_type; - public int* site_bodyid; - public int* site_matid; - public int* site_group; - public double* site_size; - public float* site_rgba; - public int* cam_orthographic; - public double* cam_fovy; - public double* cam_ipd; - public int* cam_resolution; - public float* cam_sensorsize; - public float* cam_intrinsic; - public byte* light_directional; - public byte* light_castshadow; - public float* light_bulbradius; - public float* light_intensity; - public float* light_range; - public byte* light_active; - public float* light_attenuation; - public float* light_cutoff; - public float* light_exponent; - public float* light_ambient; - public float* light_diffuse; - public float* light_specular; - public byte* flex_flatskin; - public int* flex_dim; - public int* flex_matid; - public int* flex_group; - public int* flex_interp; - public int* flex_nodeadr; - public int* flex_nodenum; - public int* flex_nodebodyid; - public int* flex_vertadr; - public int* flex_vertnum; - public int* flex_elem; - public int* flex_elemtexcoord; - public int* flex_elemlayer; - public int* flex_elemadr; - public int* flex_elemnum; - public int* flex_elemdataadr; - public int* flex_shell; - public int* flex_shellnum; - public int* flex_shelldataadr; - public int* flex_texcoordadr; - public int* flex_bvhadr; - public int* flex_bvhnum; - public byte* flex_centered; - public double* flex_node; - public double* flex_radius; - public float* flex_rgba; - public float* flex_texcoord; - public int* hfield_pathadr; - public int* mesh_bvhadr; - public int* mesh_bvhnum; - public int* mesh_texcoordadr; - public int* mesh_graphadr; - public int* mesh_pathadr; - public int* skin_matid; - public int* skin_group; - public float* skin_rgba; - public float* skin_inflate; - public int* skin_vertadr; - public int* skin_vertnum; - public int* skin_texcoordadr; - public int* skin_faceadr; - public int* skin_facenum; - public int* skin_boneadr; - public int* skin_bonenum; - public float* skin_vert; - public int* skin_face; - public int* skin_bonevertadr; - public int* skin_bonevertnum; - public float* skin_bonebindpos; - public float* skin_bonebindquat; - public int* skin_bonebodyid; - public int* skin_bonevertid; - public float* skin_bonevertweight; - public int* skin_pathadr; - public int* tex_pathadr; - public int* mat_texid; - public byte* mat_texuniform; - public float* mat_texrepeat; - public float* mat_emission; - public float* mat_specular; - public float* mat_shininess; - public float* mat_reflectance; - public float* mat_metallic; - public float* mat_roughness; - public float* mat_rgba; - public int* eq_type; - public int* eq_obj1id; - public int* eq_obj2id; - public int* eq_objtype; - public double* eq_data; - public int* tendon_num; - public int* tendon_matid; - public int* tendon_group; - public byte* tendon_limited; - public byte* tendon_actfrclimited; - public double* tendon_width; - public double* tendon_range; - public double* tendon_actfrcrange; - public double* tendon_stiffness; - public double* tendon_damping; - public double* tendon_frictionloss; - public double* tendon_lengthspring; - public float* tendon_rgba; - public int* actuator_trntype; - public int* actuator_dyntype; - public int* actuator_trnid; - public int* actuator_actadr; - public int* actuator_actnum; - public int* actuator_group; - public byte* actuator_ctrllimited; - public byte* actuator_actlimited; - public double* actuator_ctrlrange; - public double* actuator_actrange; - public double* actuator_cranklength; - public int* sensor_type; - public int* sensor_objid; - public int* sensor_adr; - public int* name_bodyadr; - public int* name_jntadr; - public int* name_geomadr; - public int* name_siteadr; - public int* name_camadr; - public int* name_lightadr; - public int* name_eqadr; - public int* name_tendonadr; - public int* name_actuatoradr; - public char* names; - public char* paths; -} - -[StructLayout(LayoutKind.Sequential)] -public unsafe struct data { - public mjWarningStat_ warning0; - public mjWarningStat_ warning1; - public mjWarningStat_ warning2; - public mjWarningStat_ warning3; - public mjWarningStat_ warning4; - public mjWarningStat_ warning5; - public mjWarningStat_ warning6; - public mjWarningStat_ warning7; - public int nefc; - public int ncon; - public int nisland; - public double time; - public double* act; - public double* ctrl; - public double* xfrc_applied; - public byte* eq_active; - public double* sensordata; - public double* xpos; - public double* xquat; - public double* xmat; - public double* xipos; - public double* ximat; - public double* xanchor; - public double* xaxis; - public double* geom_xpos; - public double* geom_xmat; - public double* site_xpos; - public double* site_xmat; - public double* cam_xpos; - public double* cam_xmat; - public double* light_xpos; - public double* light_xdir; - public double* subtree_com; - public int* ten_wrapadr; - public int* ten_wrapnum; - public int* wrap_obj; - public double* ten_length; - public double* wrap_xpos; - public double* bvh_aabb_dyn; - public byte* bvh_active; - public int* island_dofadr; - public int* dof_island; - public int* efc_island; - public int* tendon_efcadr; - public double* flexvert_xpos; - public mjContact_* contact; - public double* efc_force; - public void* arena; -} - -[StructLayout(LayoutKind.Sequential)] -public unsafe struct mjvSceneState_ { - public int nbuffer; - public void* buffer; - public int maxgeom; - public mjvScene_ scratch; - public model model; - public data data; }public struct mjuiItem_ {}public struct mjfItemEnable {} // ----------------------------Function declarations---------------------------- @@ -6979,15 +6715,9 @@ public static unsafe extern void mjv_alignToCamera(double* res, double* vec, dou [DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] public static unsafe extern void mjv_moveCamera(mjModel_* m, int action, double reldx, double reldy, mjvScene_* scn, mjvCamera_* cam); -[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] -public static unsafe extern void mjv_moveCameraFromState(mjvSceneState_* scnstate, int action, double reldx, double reldy, mjvScene_* scn, mjvCamera_* cam); - [DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] public static unsafe extern void mjv_movePerturb(mjModel_* m, mjData_* d, int action, double reldx, double reldy, mjvScene_* scn, mjvPerturb_* pert); -[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] -public static unsafe extern void mjv_movePerturbFromState(mjvSceneState_* scnstate, int action, double reldx, double reldy, mjvScene_* scn, mjvPerturb_* pert); - [DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] public static unsafe extern void mjv_moveModel(mjModel_* m, int action, double reldx, double reldy, double* roomup, mjvScene_* scn); @@ -7030,24 +6760,9 @@ public static unsafe extern void mjv_freeScene(mjvScene_* scn); [DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] public static unsafe extern void mjv_updateScene(mjModel_* m, mjData_* d, mjvOption_* opt, mjvPerturb_* pert, mjvCamera_* cam, int catmask, mjvScene_* scn); -[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] -public static unsafe extern int mjv_updateSceneFromState(mjvSceneState_* scnstate, mjvOption_* opt, mjvPerturb_* pert, mjvCamera_* cam, int catmask, mjvScene_* scn); - [DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] public static unsafe extern void mjv_copyModel(mjModel_* dest, mjModel_* src); -[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] -public static unsafe extern void mjv_defaultSceneState(mjvSceneState_* scnstate); - -[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] -public static unsafe extern void mjv_makeSceneState(mjModel_* m, mjData_* d, mjvSceneState_* scnstate, int maxgeom); - -[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] -public static unsafe extern void mjv_freeSceneState(mjvSceneState_* scnstate); - -[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] -public static unsafe extern void mjv_updateSceneState(mjModel_* m, mjData_* d, mjvOption_* opt, mjvSceneState_* scnstate); - [DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] public static unsafe extern void mjv_addGeoms(mjModel_* m, mjData_* d, mjvOption_* opt, mjvPerturb_* pert, int catmask, mjvScene_* scn); From 45fc15b8447b56d3e6f12d3a158e2bc0d4d30dc8 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 19 May 2025 10:13:20 -0700 Subject: [PATCH 149/191] Speed up sparse supernode detection by combining it with transposition. PiperOrigin-RevId: 760673008 Change-Id: I6d66580e675fd86e5b974859383f93f87482ca16 --- src/engine/engine_core_constraint.c | 8 +-- src/engine/engine_solver.c | 2 +- src/engine/engine_util_sparse.c | 40 +++++++++++--- src/engine/engine_util_sparse.h | 4 +- .../engine_util_sparse_benchmark_test.cc | 44 ++++++++++++--- test/engine/engine_util_sparse_test.cc | 53 ++++++++++++++----- 6 files changed, 115 insertions(+), 36 deletions(-) diff --git a/src/engine/engine_core_constraint.c b/src/engine/engine_core_constraint.c index 1a60b6e6..3e625f49 100644 --- a/src/engine/engine_core_constraint.c +++ b/src/engine/engine_core_constraint.c @@ -1996,7 +1996,7 @@ void mj_makeConstraint(const mjModel* m, mjData* d) { if (mj_isSparse(m)) { // transpose mju_transposeSparse(d->efc_JT, d->efc_J, d->nefc, m->nv, - d->efc_JT_rownnz, d->efc_JT_rowadr, d->efc_JT_colind, + d->efc_JT_rownnz, d->efc_JT_rowadr, d->efc_JT_colind, d->efc_JT_rowsuper, d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind); @@ -2010,10 +2010,6 @@ void mj_makeConstraint(const mjModel* m, mjData* d) { __msan_allocated_memory(d->efc_J_rowsuper, d->nefc); #endif // MEMORY_SANITIZER #endif // mjUSEAVX - - // supernodes of JT - mju_superSparse(m->nv, d->efc_JT_rowsuper, - d->efc_JT_rownnz, d->efc_JT_rowadr, d->efc_JT_colind); } // compute diagApprox @@ -2182,7 +2178,7 @@ void mj_projectConstraint(const mjModel* m, mjData* d) { int* BT_colind = mjSTACKALLOC(d, nB, int); mjtNum* BT = mjSTACKALLOC(d, nB, mjtNum); mju_transposeSparse(BT, B, nefc, nv, - BT_rownnz, BT_rowadr, BT_colind, + BT_rownnz, BT_rowadr, BT_colind, NULL, B_rownnz, B_rowadr, B_colind); // allocate AR row nonzeros and addresses on arena diff --git a/src/engine/engine_solver.c b/src/engine/engine_solver.c index 37b83024..635fa6af 100644 --- a/src/engine/engine_solver.c +++ b/src/engine/engine_solver.c @@ -1572,7 +1572,7 @@ static void MakeHessian(mjData* d, mjCGContext* ctx) { int* HT_rowadr = mjSTACKALLOC(d, nv, int); int* HT_colind = mjSTACKALLOC(d, ctx->nH, int); mju_transposeSparse(NULL, NULL, nv, nv, - HT_rownnz, HT_rowadr, HT_colind, + HT_rownnz, HT_rowadr, HT_colind, NULL, ctx->H_rownnz, ctx->H_rowadr, ctx->H_colind); // count total and row non-zeros of reverse-Cholesky factor L diff --git a/src/engine/engine_util_sparse.c b/src/engine/engine_util_sparse.c index 44dddb92..c9cf3bab 100644 --- a/src/engine/engine_util_sparse.c +++ b/src/engine/engine_util_sparse.c @@ -531,9 +531,9 @@ int mju_compressSparse(mjtNum* mat, int nr, int nc, int* rownnz, int* rowadr, in -// transpose sparse matrix +// transpose sparse matrix, optionally compute row supernodes void mju_transposeSparse(mjtNum* res, const mjtNum* mat, int nr, int nc, - int* res_rownnz, int* res_rowadr, int* res_colind, + int* res_rownnz, int* res_rowadr, int* res_colind, int* res_rowsuper, const int* rownnz, const int* rowadr, const int* colind) { // clear number of non-zeros for each row of transposed mju_zeroInt(res_rownnz, nc); @@ -547,22 +547,40 @@ void mju_transposeSparse(mjtNum* res, const mjtNum* mat, int nr, int nc, } } + // init res_rowsuper + if (res_rowsuper) { + for (int i = 0; i < nc - 1; i++) { + res_rowsuper[i] = (res_rownnz[i] == res_rownnz[i + 1]); + } + res_rowsuper[nc - 1] = 0; + } + // compute the row addresses for the transposed matrix res_rowadr[0] = 0; for (int i = 1; i < nc; i++) { res_rowadr[i] = res_rowadr[i-1] + res_rownnz[i-1]; } - // iterate through each non-zero entry of mat + // iterate through each row (column) of mat (res) for (int r = 0; r < nr; r++) { + int c_prev = -1; int start = rowadr[r]; int end = start + rownnz[r]; for (int i = start; i < end; i++) { // swap rows with columns and increment res_rowadr - int c = res_rowadr[colind[i]]++; - res_colind[c] = r; + int c = colind[i]; + int adr = res_rowadr[c]++; + res_colind[adr] = r; if (res) { - res[c] = mat[i]; + res[adr] = mat[i]; + } + + // mark non-supernodes + if (res_rowsuper) { + if (c > 0 && c != c_prev + 1 && res_rowsuper[c - 1]) { + res_rowsuper[c - 1] = 0; + } + c_prev = c; } } } @@ -571,8 +589,16 @@ void mju_transposeSparse(mjtNum* res, const mjtNum* mat, int nr, int nc, for (int i = nc-1; i > 0; i--) { res_rowadr[i] = res_rowadr[i-1]; } - res_rowadr[0] = 0; + + // accumulate supernodes + if (res_rowsuper) { + for (int i = nc - 2; i >= 0; i--) { + if (res_rowsuper[i]) { + res_rowsuper[i] += res_rowsuper[i + 1]; + } + } + } } diff --git a/src/engine/engine_util_sparse.h b/src/engine/engine_util_sparse.h index 33f65513..f6c2f7c2 100644 --- a/src/engine/engine_util_sparse.h +++ b/src/engine/engine_util_sparse.h @@ -91,9 +91,9 @@ int mju_addToSparseMat(mjtNum* dst, const mjtNum* src, int n, int nrow, mjtNum s int mju_addChains(int* res, int n, int NV1, int NV2, const int* chain1, const int* chain2); -// transpose sparse matrix +// transpose sparse matrix, optionally compute row supernodes MJAPI void mju_transposeSparse(mjtNum* res, const mjtNum* mat, int nr, int nc, - int* res_rownnz, int* res_rowadr, int* res_colind, + int* res_rownnz, int* res_rowadr, int* res_colind, int* res_rowsuper, const int* rownnz, const int* rowadr, const int* colind); // construct row supernodes diff --git a/test/benchmark/engine_util_sparse_benchmark_test.cc b/test/benchmark/engine_util_sparse_benchmark_test.cc index 6a1b3835..82a04a3f 100644 --- a/test/benchmark/engine_util_sparse_benchmark_test.cc +++ b/test/benchmark/engine_util_sparse_benchmark_test.cc @@ -152,8 +152,8 @@ void ABSL_ATTRIBUTE_NOINLINE mju_sqrMatTDSparse_baseline( // transpose sparse matrix (uncompressed) void ABSL_ATTRIBUTE_NOINLINE transposeSparse_baseline( mjtNum* res, const mjtNum* mat, int nr, int nc, int* res_rownnz, - int* res_rowadr, int* res_colind, const int* rownnz, const int* rowadr, - const int* colind) { + int* res_rowadr, int* res_colind, int* res_rowsuper, + const int* rownnz, const int* rowadr, const int* colind) { memset(res_rownnz, 0, nc * sizeof(int)); for (int rt = 0; rt < nc; rt++) { res_rowadr[rt] = rt * nr; @@ -497,7 +497,14 @@ void ABSL_ATTRIBUTE_NO_TAIL_CALL BM_combineSparse_old( } BENCHMARK(BM_combineSparse_old); -static void BM_transposeSparse(benchmark::State& state, TransposeFuncPtr func) { +enum class Supernode { + None, + PostProcess, + Inline +}; + +static void BM_transposeSparse(benchmark::State& state, TransposeFuncPtr func, + Supernode super) { static mjModel* m = LoadModelFromPath("humanoid/humanoid100.xml"); // force use of sparse matrices @@ -516,12 +523,19 @@ static void BM_transposeSparse(benchmark::State& state, TransposeFuncPtr func) { mjtNum* res = mj_stackAllocNum(d, m->nv * d->nefc); int* res_rownnz = mj_stackAllocInt(d, m->nv); int* res_rowadr = mj_stackAllocInt(d, m->nv); + int* res_rowsuper = mj_stackAllocInt(d, m->nv); int* res_colind = mj_stackAllocInt(d, m->nv * d->nefc); // time benchmark for (auto s : state) { - func(res, d->efc_J, d->nefc, m->nv, res_rownnz, res_rowadr, res_colind, + int* rowsuper = (super == Supernode::Inline) ? res_rowsuper : nullptr; + func(res, d->efc_J, d->nefc, m->nv, + res_rownnz, res_rowadr, res_colind, rowsuper, d->efc_J_rownnz, d->efc_J_rowadr, d->efc_J_colind); + if (super == Supernode::PostProcess) { + mju_superSparse(m->nv, res_rowsuper, + res_rownnz, res_rowadr, res_colind); + } } mj_freeStack(d); @@ -529,19 +543,33 @@ static void BM_transposeSparse(benchmark::State& state, TransposeFuncPtr func) { state.SetItemsProcessed(state.iterations()); } +void ABSL_ATTRIBUTE_NO_TAIL_CALL +BM_transposeSparse_old(benchmark::State& state) { + MujocoErrorTestGuard guard; + BM_transposeSparse(state, &transposeSparse_baseline, Supernode::None); +} +BENCHMARK(BM_transposeSparse_old); + void ABSL_ATTRIBUTE_NO_TAIL_CALL BM_transposeSparse_new(benchmark::State& state) { MujocoErrorTestGuard guard; - BM_transposeSparse(state, &mju_transposeSparse); + BM_transposeSparse(state, &mju_transposeSparse, Supernode::None); } BENCHMARK(BM_transposeSparse_new); void ABSL_ATTRIBUTE_NO_TAIL_CALL -BM_transposeSparse_old(benchmark::State& state) { +BM_transposeSparse_superpost(benchmark::State& state) { MujocoErrorTestGuard guard; - BM_transposeSparse(state, &transposeSparse_baseline); + BM_transposeSparse(state, &mju_transposeSparse, Supernode::PostProcess); } -BENCHMARK(BM_transposeSparse_old); +BENCHMARK(BM_transposeSparse_superpost); + +void ABSL_ATTRIBUTE_NO_TAIL_CALL +BM_transposeSparse_superinline(benchmark::State& state) { + MujocoErrorTestGuard guard; + BM_transposeSparse(state, &mju_transposeSparse, Supernode::Inline); +} +BENCHMARK(BM_transposeSparse_superinline); static void BM_sqrMatTDSparse(benchmark::State& state, SqrMatTDFuncPtr func) { static mjModel* m = diff --git a/test/engine/engine_util_sparse_test.cc b/test/engine/engine_util_sparse_test.cc index edbd2558..36cbda57 100644 --- a/test/engine/engine_util_sparse_test.cc +++ b/test/engine/engine_util_sparse_test.cc @@ -173,8 +173,8 @@ TEST_F(EngineUtilSparseTest, MjuTranspose3by3) { int rownnzT[] = {0, 0, 0}; int rowadrT[] = {0, 0, 0}; - mju_transposeSparse(matT, mat, 3, 3, rownnzT, rowadrT, colindT, rownnz, - rowadr, colind); + mju_transposeSparse(matT, mat, 3, 3, rownnzT, rowadrT, colindT, nullptr, + rownnz, rowadr, colind); EXPECT_THAT(matT, ElementsAre(1, 2, 1, 3)); EXPECT_THAT(colindT, ElementsAre(0, 0, 1, 2)); @@ -197,8 +197,8 @@ TEST_F(EngineUtilSparseTest, MjuTranspose1by3) { int rownnzT[] = {0, 0, 0}; int rowadrT[] = {0, 0, 0}; - mju_transposeSparse(matT, mat, 1, 3, rownnzT, rowadrT, colindT, rownnz, - rowadr, colind); + mju_transposeSparse(matT, mat, 1, 3, rownnzT, rowadrT, colindT, nullptr, + rownnz, rowadr, colind); EXPECT_THAT(matT, ElementsAre(1, 3)); EXPECT_THAT(colindT, ElementsAre(0, 0)); @@ -221,8 +221,8 @@ TEST_F(EngineUtilSparseTest, MjuTranspose3by1) { int rownnzT[] = {0}; int rowadrT[] = {0}; - mju_transposeSparse(matT, mat, 3, 1, rownnzT, rowadrT, colindT, rownnz, - rowadr, colind); + mju_transposeSparse(matT, mat, 3, 1, rownnzT, rowadrT, colindT, nullptr, + rownnz, rowadr, colind); EXPECT_THAT(matT, ElementsAre(1, 3)); EXPECT_THAT(colindT, ElementsAre(0, 2)); @@ -244,14 +244,43 @@ TEST_F(EngineUtilSparseTest, MjuTransposeDense) { int colindT[] = {0, 0, 0, 0, 0, 0, 0, 0, 0}; int rownnzT[] = {0, 0, 0}; int rowadrT[] = {0, 0, 0}; + int rowsuperT[] = {0, 0, 0}; - mju_transposeSparse(matT, mat, 3, 3, rownnzT, rowadrT, colindT, rownnz, - rowadr, colind); + mju_transposeSparse(matT, mat, 3, 3, rownnzT, rowadrT, colindT, rowsuperT, + rownnz, rowadr, colind); EXPECT_THAT(matT, ElementsAre(1, 4, 7, 2, 5, 8, 3, 6, 9)); EXPECT_THAT(colindT, ElementsAre(0, 1, 2, 0, 1, 2, 0, 1, 2)); EXPECT_THAT(rownnzT, ElementsAre(3, 3, 3)); EXPECT_THAT(rowadrT, ElementsAre(0, 3, 6)); + EXPECT_THAT(rowsuperT, ElementsAre(2, 1, 0)); +} + +TEST_F(EngineUtilSparseTest, MjuTransposeSuper) { + // mat: 0, 1, 2, 0, 3, 4, 5, 0, 0, 0, 0, 0, 0 + // 0, 0, 0, 6, 7, 8, 9, 10, 11, 12, 0, 0, 0 + // + // super: 0, 1, 0, 0, 2, 1, 0, 2, 1, 0, 2, 1, 0 + + mjtNum mat[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; + int colind[] = {1, 2, 4, 5, 6, 3, 4, 5, 6, 7, 8, 9}; + int rownnz[] = {5, 7}; + int rowadr[] = {0, 5}; + + mjtNum matT[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + int colindT[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + int rownnzT[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + int rowadrT[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + int rowsuperT[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + + mju_transposeSparse(matT, mat, 2, 13, rownnzT, rowadrT, colindT, rowsuperT, + rownnz, rowadr, colind); + + EXPECT_THAT(matT, ElementsAre(1, 2, 6, 3, 7, 4, 8, 5, 9, 10, 11, 12)); + EXPECT_THAT(colindT, ElementsAre(0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1)); + EXPECT_THAT(rownnzT, ElementsAre(0, 1, 1, 1, 2, 2, 2, 1, 1, 1, 0, 0, 0)); + EXPECT_THAT(rowadrT, ElementsAre(0, 0, 1, 2, 3, 5, 7, 9, 10, 11, 12, 12, 12)); + EXPECT_THAT(rowsuperT, ElementsAre(0, 1, 0, 0, 2, 1, 0, 2, 1, 0, 2, 1, 0)); } TEST_F(EngineUtilSparseTest, MjuTranspose1by1) { @@ -267,8 +296,8 @@ TEST_F(EngineUtilSparseTest, MjuTranspose1by1) { int rownnzT[] = {0}; int rowadrT[] = {0}; - mju_transposeSparse(matT, mat, 1, 1, rownnzT, rowadrT, colindT, rownnz, - rowadr, colind); + mju_transposeSparse(matT, mat, 1, 1, rownnzT, rowadrT, colindT, nullptr, + rownnz, rowadr, colind); EXPECT_THAT(matT, ElementsAre(1)); EXPECT_THAT(colindT, ElementsAre(0)); @@ -289,8 +318,8 @@ TEST_F(EngineUtilSparseTest, MjuTransposeNullMatrix) { int rownnzT[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; int rowadrT[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; - mju_transposeSparse(matT, mat, 10, 10, rownnzT, rowadrT, colindT, rownnz, - rowadr, colind); + mju_transposeSparse(matT, mat, 10, 10, rownnzT, rowadrT, colindT, nullptr, + rownnz, rowadr, colind); EXPECT_THAT(rownnzT, ElementsAre(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)); EXPECT_THAT(rowadrT, ElementsAre(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)); From 7edbdd0ad6a6e9fd99d6bc906037b2334f5f9da1 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Mon, 19 May 2025 10:50:27 -0700 Subject: [PATCH 150/191] Remove the Shell plugin and integrate it into the engine. PiperOrigin-RevId: 760688502 Change-Id: Ia70988d42b7edf571d7cb4a4f48f8fc50b51667d --- doc/XMLreference.rst | 6 +- doc/changelog.rst | 2 + model/flex/bunny.xml | 2 +- model/flex/bunny_with_uv.xml | 2 +- .../flag_flex.xml => flex/flag.xml} | 7 +- .../{plugin/elasticity => flex}/mannequin.xml | 0 .../pancake_flex.xml => flex/pancake.xml} | 6 +- .../plate_flex.xml => flex/plate.xml} | 7 +- .../poncho_flex.xml => flex/poncho.xml} | 7 +- .../poncho_vertcollide.xml | 7 +- model/flex/trampoline.xml | 2 +- plugin/elasticity/CMakeLists.txt | 2 - plugin/elasticity/README.md | 12 -- plugin/elasticity/register.cc | 2 - plugin/elasticity/shell.cc | 154 ------------------ plugin/elasticity/shell.h | 61 ------- src/engine/engine_passive.c | 43 ++++- src/user/user_init.c | 1 - src/user/user_mesh.cc | 17 +- src/user/user_model.cc | 2 +- src/xml/xml_base.h | 1 + src/xml/xml_native_reader.cc | 13 +- src/xml/xml_native_writer.cc | 2 +- .../engine_util_sparse_benchmark_test.cc | 2 +- test/benchmark/parse_benchmark_test.cc | 2 +- test/benchmark/step_benchmark_test.cc | 2 +- test/engine/engine_plugin_test.cc | 2 +- test/plugin/elasticity/elasticity_test.cc | 15 +- 28 files changed, 90 insertions(+), 291 deletions(-) rename model/{plugin/elasticity/flag_flex.xml => flex/flag.xml} (88%) rename model/{plugin/elasticity => flex}/mannequin.xml (100%) rename model/{plugin/elasticity/pancake_flex.xml => flex/pancake.xml} (90%) rename model/{plugin/elasticity/plate_flex.xml => flex/plate.xml} (87%) rename model/{plugin/elasticity/poncho_flex.xml => flex/poncho.xml} (99%) rename model/{plugin/elasticity => flex}/poncho_vertcollide.xml (99%) delete mode 100644 plugin/elasticity/shell.cc delete mode 100644 plugin/elasticity/shell.h diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 1f479369..a57f1a1f 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -4073,9 +4073,9 @@ stress-strain relationship.. See also :ref:`deformable ` objects. .. _flex-elasticity-elastic2d: -:at:`elastic2d`: :at-val:`int, "1"` - Elastic contribution to passive forces of 2D flexes. 0: none, 1: bending only, 2: stretching only, 3: bending and - stretching +:at:`elastic2d`: :at-val:`[none, bend, stretch, both], "none"` + Elastic contribution to passive forces of 2D flexes. "none": none, "bend": bending only, "stretch": stretching only, + "both": bending and stretching. .. _flex-contact: diff --git a/doc/changelog.rst b/doc/changelog.rst index 4255c8f0..9e0329f4 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -9,6 +9,8 @@ General ^^^^^^^ - Refactored island implementation so that island data is memory-contiguous. This speeds up island processing in the solver and clears the way for the addition of the Newton and PGS solvers (currently only CG is supported). +- Removed the :at:`shell` plugin. This is now supported by :ref:`flexcomp` and is active depending on + the :ref:`elastic2d` attribute (on by default). simulate ^^^^^^^^ diff --git a/model/flex/bunny.xml b/model/flex/bunny.xml index 64e081ef..ec31684e 100644 --- a/model/flex/bunny.xml +++ b/model/flex/bunny.xml @@ -31,7 +31,7 @@ - + diff --git a/model/flex/bunny_with_uv.xml b/model/flex/bunny_with_uv.xml index 5b9e42e4..c3cde418 100644 --- a/model/flex/bunny_with_uv.xml +++ b/model/flex/bunny_with_uv.xml @@ -37,7 +37,7 @@ - + diff --git a/model/plugin/elasticity/flag_flex.xml b/model/flex/flag.xml similarity index 88% rename from model/plugin/elasticity/flag_flex.xml rename to model/flex/flag.xml index 9e37edab..c7bfa371 100644 --- a/model/plugin/elasticity/flag_flex.xml +++ b/model/flex/flag.xml @@ -26,10 +26,6 @@ - - - - @@ -37,8 +33,7 @@ - - + diff --git a/model/plugin/elasticity/mannequin.xml b/model/flex/mannequin.xml similarity index 100% rename from model/plugin/elasticity/mannequin.xml rename to model/flex/mannequin.xml diff --git a/model/plugin/elasticity/pancake_flex.xml b/model/flex/pancake.xml similarity index 90% rename from model/plugin/elasticity/pancake_flex.xml rename to model/flex/pancake.xml index fb390e34..8e3d1ec1 100644 --- a/model/plugin/elasticity/pancake_flex.xml +++ b/model/flex/pancake.xml @@ -16,10 +16,6 @@ - - - - diff --git a/model/plugin/elasticity/plate_flex.xml b/model/flex/plate.xml similarity index 87% rename from model/plugin/elasticity/plate_flex.xml rename to model/flex/plate.xml index e39ec427..9278c7fe 100644 --- a/model/plugin/elasticity/plate_flex.xml +++ b/model/flex/plate.xml @@ -16,10 +16,6 @@ - - - - diff --git a/model/plugin/elasticity/poncho_flex.xml b/model/flex/poncho.xml similarity index 99% rename from model/plugin/elasticity/poncho_flex.xml rename to model/flex/poncho.xml index 9400d106..c2462e56 100644 --- a/model/plugin/elasticity/poncho_flex.xml +++ b/model/flex/poncho.xml @@ -19,10 +19,6 @@ - - - - @@ -1418,9 +1414,8 @@ 398 399 418 398 376 378"> - + - diff --git a/model/plugin/elasticity/poncho_vertcollide.xml b/model/flex/poncho_vertcollide.xml similarity index 99% rename from model/plugin/elasticity/poncho_vertcollide.xml rename to model/flex/poncho_vertcollide.xml index 16e572bb..cb75a7cd 100644 --- a/model/plugin/elasticity/poncho_vertcollide.xml +++ b/model/flex/poncho_vertcollide.xml @@ -19,10 +19,6 @@ - - - - @@ -1418,9 +1414,8 @@ 398 399 418 398 376 378"> - + - diff --git a/model/flex/trampoline.xml b/model/flex/trampoline.xml index 16775a82..bb538201 100644 --- a/model/flex/trampoline.xml +++ b/model/flex/trampoline.xml @@ -39,7 +39,7 @@ radius=".001" mass="10" name="plate" dim="2"> - + diff --git a/plugin/elasticity/CMakeLists.txt b/plugin/elasticity/CMakeLists.txt index f6186261..97b2f412 100644 --- a/plugin/elasticity/CMakeLists.txt +++ b/plugin/elasticity/CMakeLists.txt @@ -22,8 +22,6 @@ set(MUJOCO_ELASTICITY_SRCS elasticity.cc elasticity.h register.cc - shell.cc - shell.h ) add_library(elasticity SHARED) diff --git a/plugin/elasticity/README.md b/plugin/elasticity/README.md index 2e86c1a7..7ccf5712 100644 --- a/plugin/elasticity/README.md +++ b/plugin/elasticity/README.md @@ -18,15 +18,3 @@ Parameters: - `bend` [Pa]: bending stiffness. - `flat` [bool]: if true, the stress-equilibrium configuration is that of a straight cable; if false or unspecified, it is the configuration defined in the XML. - `vmax` [N/m^2]: If greater than zero, the cable is colored using mechanical stresses; the value represent the maximum stress in the color scale. - -### Shell - -Implemented in [shell.cc](shell.cc). - -The shell plugin discretizes an inextensible 2D continuum. It is intended to simulate the bending of plates where the stretching is negligible compared to other deformation modes. - -Parameters: - - - `young` [Pa]: Young's modulus. - - `poisson` [Pa]: Poisson's ratio; if 0, then the material only opposed shear deformations; if near 0.5, then the material is nearly incompressible (rubber-like). - - `thickness` [m]: shell thickness, used to scale the bending stiffness. diff --git a/plugin/elasticity/register.cc b/plugin/elasticity/register.cc index edde5252..ab8d283a 100644 --- a/plugin/elasticity/register.cc +++ b/plugin/elasticity/register.cc @@ -14,13 +14,11 @@ #include #include "cable.h" -#include "shell.h" namespace mujoco::plugin::elasticity { mjPLUGIN_LIB_INIT { Cable::RegisterPlugin(); - Shell::RegisterPlugin(); } } // namespace mujoco::plugin::elasticity diff --git a/plugin/elasticity/shell.cc b/plugin/elasticity/shell.cc deleted file mode 100644 index 699040c6..00000000 --- a/plugin/elasticity/shell.cc +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright 2023 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. - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include "elasticity.h" -#include "shell.h" - - -namespace mujoco::plugin::elasticity { -namespace { - -// local tetrahedron numbering -constexpr int kNumVerts = Stencil2D::kNumVerts; - - - -} // namespace - -// factory function -std::optional Shell::Create(const mjModel* m, mjData* d, int instance) { - return Shell(m, d, instance); -} - -// plugin constructor -Shell::Shell(const mjModel* m, mjData* d, int instance) - : f0(-1) { - // count plugin bodies - nv = 0; - for (int i = 1; i < m->nbody; i++) { - if (m->body_plugin[i] == instance) { - if (!nv++) { - i0 = i; - } - } - } - - // count flexes - for (int i = 0; i < m->nflex; i++) { - for (int j = 0; j < m->flex_vertnum[i]; j++) { - if (m->flex_vertbodyid[m->flex_vertadr[i]+j] == i0) { - f0 = i; - nv = m->flex_vertnum[f0]; - if (m->flex_dim[i] != 2) { // SHOULD NOT OCCUR - mju_error("mujoco.elasticity.shell requires a 2D mesh"); - } - } - } - } - - // loop over all triangles - for (int t = 0; t < m->flex_elemnum[f0]; t++) { - int* v = m->flex_elem + 3*(t+m->flex_elemadr[f0]); - for (int i = 0; i < kNumVerts; i++) { - if (m->body_plugin[i0+v[i]] != instance) { - mju_error("This body does not have the requested plugin instance"); - } - } - } - - // allocate array - position.assign(nv*3, 0); - - // store previous positions - mju_copy(position.data(), m->body_pos+3*i0, 3*nv); -} - -void Shell::Compute(const mjModel* m, mjData* d, int instance) { - for (int e = 0; e < m->flex_edgenum[f0]; e++) { - int* edge = m->flex_edge + 2*(e+m->flex_edgeadr[f0]); - int* flap = m->flex_edgeflap + 2*(e+m->flex_edgeadr[f0]); - int v[4] = {edge[0], edge[1], flap[0], flap[1]}; - mjtNum force[12] = {0}; - if (v[3] == -1) { - // skip boundary edges - continue; - } - mjtNum* k = m->flex_bending + 16*m->flex_edgeadr[f0]; - for (int i = 0; i < StencilFlap::kNumVerts; i++) { - for (int j = 0; j < StencilFlap::kNumVerts; j++) { - for (int x = 0; x < 3; x++) { - force[3*i+x] += k[16*e+4*i+j] * d->xpos[3*(i0+v[j])+x]; - } - } - } - - // update stored positions - mju_copy(position.data(), d->xpos+3*i0, 3*nv); - - // insert into global force - for (int i = 0; i < StencilFlap::kNumVerts; i++) { - for (int x = 0; x < 3; x++) { - d->qfrc_passive[m->body_dofadr[i0]+3*v[i]+x] -= force[3*i+x]; - } - } - } -} - - - -void Shell::RegisterPlugin() { - mjpPlugin plugin; - mjp_defaultPlugin(&plugin); - - plugin.name = "mujoco.elasticity.shell"; - plugin.capabilityflags |= mjPLUGIN_PASSIVE; - - const char* attributes[] = {"damping"}; - plugin.nattribute = sizeof(attributes) / sizeof(attributes[0]); - plugin.attributes = attributes; - plugin.nstate = +[](const mjModel* m, int instance) { return 0; }; - - plugin.init = +[](const mjModel* m, mjData* d, int instance) { - auto elasticity_or_null = Shell::Create(m, d, instance); - if (!elasticity_or_null.has_value()) { - return -1; - } - d->plugin_data[instance] = reinterpret_cast( - new Shell(std::move(*elasticity_or_null))); - return 0; - }; - plugin.destroy = +[](mjData* d, int instance) { - delete reinterpret_cast(d->plugin_data[instance]); - d->plugin_data[instance] = 0; - }; - plugin.compute = +[](const mjModel* m, mjData* d, int instance, int type) { - auto* elasticity = reinterpret_cast(d->plugin_data[instance]); - elasticity->Compute(m, d, instance); - }; - - mjp_registerPlugin(&plugin); -} - -} // namespace mujoco::plugin::elasticity diff --git a/plugin/elasticity/shell.h b/plugin/elasticity/shell.h deleted file mode 100644 index aa1e0191..00000000 --- a/plugin/elasticity/shell.h +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2023 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. - -#ifndef MUJOCO_PLUGIN_ELASTICITY_SHELL_H_ -#define MUJOCO_PLUGIN_ELASTICITY_SHELL_H_ - -#include -#include - -#include -#include -#include -#include "elasticity.h" - - -namespace mujoco::plugin::elasticity { - -struct StencilFlap { - static constexpr int kNumVerts = 4; - int vertices[kNumVerts]; -}; - -class Shell { - public: - // Returns a new Shell instance or nullopt on failure. - static std::optional Create(const mjModel* m, mjData* d, - int instance); - Shell(Shell&&) = default; - - Shell& operator=(Shell&& other) = default; - - void Compute(const mjModel* m, mjData* d, int instance); - - static void RegisterPlugin(); - - int i0; // index of first body - int f0; // index of corresponding flex - int nc; // number of quads in the grid - int nv; // number of vertices (bodies) in the Shell - - // precomputed quantities - std::vector position; // previous-step positions (nv x 3) - - private: - Shell(const mjModel* m, mjData* d, int instance); -}; - -} // namespace mujoco::plugin::elasticity - -#endif // MUJOCO_PLUGIN_ELASTICITY_SHELL_H_ diff --git a/src/engine/engine_passive.c b/src/engine/engine_passive.c index 3c025804..5b0d1b5b 100644 --- a/src/engine/engine_passive.c +++ b/src/engine/engine_passive.c @@ -116,9 +116,50 @@ static void mj_springdamper(const mjModel* m, mjData* d) { // flex elasticity for (int f=0; f < m->nflex; f++) { mjtNum* k = m->flex_stiffness + 21*m->flex_elemadr[f]; + mjtNum* b = m->flex_bending + 16*m->flex_edgeadr[f]; int dim = m->flex_dim[f]; - if (dim == 1 || m->flex_rigid[f] || k[0] == 0) { + if (dim == 1 || m->flex_rigid[f]) { + continue; + } + + // add bending forces to qfrc_spring + if (dim == 2) { + mjtNum* xpos = d->flexvert_xpos + 3*m->flex_vertadr[f]; + int* bodyid = m->flex_vertbodyid + m->flex_vertadr[f]; + + for (int e = 0; e < m->flex_edgenum[f]; e++) { + const int* edge = m->flex_edge + 2*(e+m->flex_edgeadr[f]); + const int* flap = m->flex_edgeflap + 2*(e+m->flex_edgeadr[f]); + int v[4] = {edge[0], edge[1], flap[0], flap[1]}; + if (v[3] == -1) { + // skip boundary edges + continue; + } + mjtNum force[12] = {0}; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + for (int x = 0; x < 3; x++) { + force[3*i+x] += b[16*e+4*i+j] * xpos[3*v[j]+x]; + } + } + } + + // TODO: add damping + + // insert into global force + for (int i = 0; i < 4; i++) { + int bid = bodyid[v[i]]; + int body_dofnum = m->body_dofnum[bid]; + int body_dofadr = m->body_dofadr[bid]; + for (int x = 0; x < body_dofnum; x++) { + d->qfrc_spring[body_dofadr+x] -= force[3*i+x]; + } + } + } + } + + if (k[0] == 0) { continue; } diff --git a/src/user/user_init.c b/src/user/user_init.c index 3a55af25..01f4a8b9 100644 --- a/src/user/user_init.c +++ b/src/user/user_init.c @@ -238,7 +238,6 @@ void mjs_defaultFlex(mjsFlex* flex) { flex->rgba[0] = flex->rgba[1] = flex->rgba[2] = 0.5f; flex->rgba[3] = 1.0f; flex->thickness = -1; - flex->elastic2d = 1; } diff --git a/src/user/user_mesh.cc b/src/user/user_mesh.cc index 49230914..03ad956c 100644 --- a/src/user/user_mesh.cc +++ b/src/user/user_mesh.cc @@ -3382,9 +3382,13 @@ void mjCFlex::ResolveReferences(const mjCModel* m) { vertbodyid.clear(); nodebodyid.clear(); for (const auto& vertbody : vertbody_) { - mjCBase* pbody = m->FindObject(mjOBJ_BODY, vertbody); + mjCBody* pbody = static_cast(m->FindObject(mjOBJ_BODY, vertbody)); if (pbody) { vertbodyid.push_back(pbody->id); + if (pbody->joints.size() != 3 && dim == 2 && (elastic2d == 1 || elastic2d == 3)) { + // TODO(quaglino): add support for pins + throw mjCError(this, "pins are not supported for bending"); + } } else { throw mjCError(this, "unknown body '%s' in flex", vertbody.c_str()); } @@ -3658,7 +3662,10 @@ void mjCFlex::Compile(const mjVFS* vfs) { } // bending stiffness (2D only) - if (dim == 2 && (elastic2d == 1 || elastic2d == 3) && thickness > 0) { + if (dim == 2 && (elastic2d == 1 || elastic2d == 3)) { + if (thickness < 0) { + throw mjCError(this, "thickness must be positive for bending stiffness"); + } bending.assign(nedge*16, 0); for (unsigned int e = 0; e < nedge; e++) { @@ -3668,7 +3675,7 @@ void mjCFlex::Compile(const mjVFS* vfs) { } } - // add plugins + // placeholder for setting plugins parameters, currently not used for (const auto& vbodyid : vertbodyid) { if (vbodyid < 0) { continue; @@ -3676,8 +3683,8 @@ void mjCFlex::Compile(const mjVFS* vfs) { if (model->Bodies()[vbodyid]->plugin.element) { mjCPlugin* plugin_instance = static_cast(model->Bodies()[vbodyid]->plugin.element); - if (damping > 0) { - plugin_instance->config_attribs["damping"] = std::to_string(damping); + if (!plugin_instance) { + throw mjCError(this, "plugin instance not found"); } } } diff --git a/src/user/user_model.cc b/src/user/user_model.cc index 9e516380..49f79ead 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -3165,7 +3165,7 @@ void mjCModel::CopyObjects(mjModel* m) { for (int k=0; k < pfl->nedge; k++) { m->flex_edge[2*(edge_adr+k)] = pfl->edge[k].first; m->flex_edge[2*(edge_adr+k)+1] = pfl->edge[k].second; - if (pfl->dim == 2) { + if (pfl->dim == 2 && (pfl->elastic2d == 1 || pfl->elastic2d == 3)) { m->flex_edgeflap[2*(edge_adr+k)+0] = pfl->flaps[k].vertices[2]; m->flex_edgeflap[2*(edge_adr+k)+1] = pfl->flaps[k].vertices[3]; } else { diff --git a/src/xml/xml_base.h b/src/xml/xml_base.h index ba914229..3651d31c 100644 --- a/src/xml/xml_base.h +++ b/src/xml/xml_base.h @@ -71,6 +71,7 @@ extern const mjMap datatype_map[]; extern const mjMap meshtype_map[]; extern const mjMap meshinertia_map[]; extern const mjMap flexself_map[]; +extern const mjMap elastic2d_map[]; //---------------------------------- Base XML class ------------------------------------------------ diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 8e2874aa..f2b5f1e9 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -807,6 +807,15 @@ const mjMap flexself_map[5] = { }; +// flex elastic 2d type +const mjMap elastic2d_map[5] = { + {"none", 0}, + {"bend", 1}, + {"stretch", 2}, + {"both", 3}, +}; + + //---------------------------------- class mjXReader implementation -------------------------------- @@ -1401,7 +1410,7 @@ void mjXReader::OneFlex(XMLElement* elem, mjsFlex* flex) { ReadAttr(elasticity, "poisson", 1, &flex->poisson, text); ReadAttr(elasticity, "thickness", 1, &flex->thickness, text); ReadAttr(elasticity, "damping", 1, &flex->damping, text); - ReadAttr(elasticity, "elastic2d", 1, &flex->elastic2d, text); + MapValue(elasticity, "elastic2d", &flex->elastic2d, elastic2d_map, 4); } // write error info @@ -2665,7 +2674,7 @@ void mjXReader::OneFlexcomp(XMLElement* elem, mjsBody* body, const mjVFS* vfs) { ReadAttr(elasticity, "poisson", 1, &dflex.poisson, text); ReadAttr(elasticity, "damping", 1, &dflex.damping, text); ReadAttr(elasticity, "thickness", 1, &dflex.thickness, text); - ReadAttr(elasticity, "elastic2d", 1, &dflex.elastic2d, text); + MapValue(elasticity, "elastic2d", &dflex.elastic2d, elastic2d_map, 4); } // check errors diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index 14905144..b6464cd1 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -194,7 +194,7 @@ void mjXWriter::OneFlex(XMLElement* elem, const mjCFlex* flex) { WriteAttr(elastic, "poisson", 1, &flex->poisson, &defflex.poisson); WriteAttr(elastic, "thickness", 1, &flex->thickness, &defflex.thickness); WriteAttr(elastic, "damping", 1, &flex->damping, &defflex.damping); - WriteAttr(elastic, "elastic2d", 1, &flex->elastic2d, &defflex.elastic2d); + WriteAttrKey(elastic, "elastic2d", elastic2d_map, 2, flex->elastic2d, defflex.elastic2d); // edge subelement XMLElement* edge = InsertEnd(elem, "edge"); diff --git a/test/benchmark/engine_util_sparse_benchmark_test.cc b/test/benchmark/engine_util_sparse_benchmark_test.cc index 82a04a3f..fdeadb31 100644 --- a/test/benchmark/engine_util_sparse_benchmark_test.cc +++ b/test/benchmark/engine_util_sparse_benchmark_test.cc @@ -343,7 +343,7 @@ void ABSL_ATTRIBUTE_NOINLINE mulMatVecSparse_8(mjtNum* res, // ----------------------------- benchmark ------------------------------------ static void BM_MatVecSparse(benchmark::State& state, int unroll) { - static mjModel* m = LoadModelFromPath("plugin/elasticity/flag_flex.xml"); + static mjModel* m = LoadModelFromPath("flex/flag.xml"); mjData* d = mj_makeData(m); // warm-up rollout to get a typical state diff --git a/test/benchmark/parse_benchmark_test.cc b/test/benchmark/parse_benchmark_test.cc index 059f0c58..95471c01 100644 --- a/test/benchmark/parse_benchmark_test.cc +++ b/test/benchmark/parse_benchmark_test.cc @@ -73,7 +73,7 @@ static void run_parse_benchmark(const std::string xml_path, // run_parse_benchmark). void ABSL_ATTRIBUTE_NO_TAIL_CALL BM_ParseFlagPlugin(benchmark::State& state) { - run_parse_benchmark(GetModelPath("plugin/elasticity/flag_flex.xml"), state); + run_parse_benchmark(GetModelPath("flex/flag.xml"), state); } BENCHMARK(BM_ParseFlagPlugin); diff --git a/test/benchmark/step_benchmark_test.cc b/test/benchmark/step_benchmark_test.cc index 551e312c..1d34f2c9 100644 --- a/test/benchmark/step_benchmark_test.cc +++ b/test/benchmark/step_benchmark_test.cc @@ -73,7 +73,7 @@ static void run_step_benchmark(const mjModel* model, benchmark::State& state) { void ABSL_ATTRIBUTE_NO_TAIL_CALL BM_StepFlagPlugin(benchmark::State& state) { MujocoErrorTestGuard guard; - static mjModel* model = LoadModelFromPath("plugin/elasticity/flag_flex.xml"); + static mjModel* model = LoadModelFromPath("flex/flag.xml"); run_step_benchmark(model, state); } BENCHMARK(BM_StepFlagPlugin); diff --git a/test/engine/engine_plugin_test.cc b/test/engine/engine_plugin_test.cc index a1b5f959..5463f1e2 100644 --- a/test/engine/engine_plugin_test.cc +++ b/test/engine/engine_plugin_test.cc @@ -37,7 +37,7 @@ using ::testing::DoubleNear; using ::testing::HasSubstr; using ::testing::NotNull; -constexpr int kNumTruePlugins = 11; +constexpr int kNumTruePlugins = 10; constexpr int kNumFakePlugins = 30; constexpr int kNumTestPlugins = 4; diff --git a/test/plugin/elasticity/elasticity_test.cc b/test/plugin/elasticity/elasticity_test.cc index 754fa46e..ea4df2e3 100644 --- a/test/plugin/elasticity/elasticity_test.cc +++ b/test/plugin/elasticity/elasticity_test.cc @@ -22,7 +22,6 @@ #include #include #include "test/fixture.h" -#include "plugin/elasticity/shell.h" namespace mujoco { namespace { @@ -58,15 +57,10 @@ TEST_F(ElasticityTest, FlexCompatibility) { TEST_F(ElasticityTest, ElasticEnergyShell) { static constexpr char cantilever_xml[] = R"( - - - - - @@ -76,7 +70,8 @@ TEST_F(ElasticityTest, ElasticEnergyShell) { mjModel* m = LoadModelFromString(cantilever_xml, error, sizeof(error)); ASSERT_THAT(m, testing::NotNull()) << error; mjData* d = mj_makeData(m); - auto* shell = reinterpret_cast(d->plugin_data[0]); + mj_kinematics(m, d); + mj_flex(m, d); // check that a plane is in the kernel of the energy for (mjtNum scale = 1; scale < 4; scale++) { @@ -92,8 +87,8 @@ TEST_F(ElasticityTest, ElasticEnergyShell) { for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { for (int x = 0; x < 3; x++) { - mjtNum elongation1 = scale * shell->position[3*v[i]+x]; - mjtNum elongation2 = scale * shell->position[3*v[j]+x]; + mjtNum elongation1 = scale * d->flexvert_xpos[3*v[i]+x]; + mjtNum elongation2 = scale * d->flexvert_xpos[3*v[j]+x]; energy += m->flex_bending[16*e+4*i+j] * elongation1 * elongation2; } } @@ -114,7 +109,7 @@ TEST_F(PluginTest, ElasticEnergyMembrane) { - + From 94803d772c02ff0ee0a47806749e98b97cae0c96 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 19 May 2025 12:17:22 -0700 Subject: [PATCH 151/191] Enable multithreading of Newton solver. PiperOrigin-RevId: 760723081 Change-Id: Ib7d3c9cec2fdf7dd0e46a570f37c0a6fc5424c7b --- src/engine/engine_forward.c | 48 +++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/engine/engine_forward.c b/src/engine/engine_forward.c index 6115e77f..16d33cae 100644 --- a/src/engine/engine_forward.c +++ b/src/engine/engine_forward.c @@ -659,32 +659,35 @@ struct mjSolIslandArgs_ { }; typedef struct mjSolIslandArgs_ mjSolIslandArgs; -// extract arguments, pass to solver -void* mj_solCG_island_wrapper(void* args) { +// extract arguments, pass to CG solver +static void* CG_wrapper(void* args) { mjSolIslandArgs* solargs = (mjSolIslandArgs*) args; mj_solCG_island(solargs->m, solargs->d, solargs->island, solargs->m->opt.iterations); return NULL; } - - +// extract arguments, pass to Newton solver +static void* Newton_wrapper(void* args) { + mjSolIslandArgs* solargs = (mjSolIslandArgs*) args; + mj_solNewton_island(solargs->m, solargs->d, solargs->island, solargs->m->opt.iterations); + return NULL; +} // CG solver, multi-threaded over islands -void mj_solCG_island_multithreaded(const mjModel* m, mjData* d) { +static void solve_threaded(const mjModel* m, mjData* d, int flg_Newton) { mj_markStack(d); // allocate array of arguments to be passed to threads - mjSolIslandArgs* sol_cg_island_args = mjSTACKALLOC(d, d->nisland, mjSolIslandArgs); + mjSolIslandArgs* sol_island_args = mjSTACKALLOC(d, d->nisland, mjSolIslandArgs); mjTask* tasks = mjSTACKALLOC(d, d->nisland, mjTask); - for (int island = 0; island < d->nisland; ++island) { - sol_cg_island_args[island].m = m; - sol_cg_island_args[island].d = d; - sol_cg_island_args[island].island = island; + sol_island_args[island].m = m; + sol_island_args[island].d = d; + sol_island_args[island].island = island; mju_defaultTask(&tasks[island]); - tasks[island].func = mj_solCG_island_wrapper; - tasks[island].args = &sol_cg_island_args[island]; + tasks[island].func = flg_Newton ? Newton_wrapper : CG_wrapper; + tasks[island].args = &sol_island_args[island]; mju_threadPoolEnqueue((mjThreadPool*)d->threadpool, &tasks[island]); } @@ -740,23 +743,22 @@ void mj_fwdConstraint(const mjModel* m, mjData* d) { mju_gather(d->iefc_force, d->efc_force, d->map_iefc2efc, nefc); mju_gather(d->iefc_aref, d->efc_aref, d->map_iefc2efc, nefc); - // solve per island - if (m->opt.solver == mjSOL_CG) { - if (!d->threadpool) { - // no threadpool, loop over islands - for (int island=0; island < nisland; island++) { + // solve per island, with or without threads + if (!d->threadpool) { + // no threadpool, loop over islands + for (int island=0; island < nisland; island++) { + if (m->opt.solver == mjSOL_NEWTON) { + mj_solNewton_island(m, d, island, m->opt.iterations); + } else { mj_solCG_island(m, d, island, m->opt.iterations); } - } else { - // have threadpool, solve using threads - mj_solCG_island_multithreaded(m, d); } } else { - for (int island=0; island < nisland; island++) { - mj_solNewton_island(m, d, island, m->opt.iterations); - } + // have threadpool, solve using threads + solve_threaded(m, d, m->opt.solver == mjSOL_NEWTON); } + // copy back solver outputs (scatter dofs since ni <= nv) mju_scatter(d->qacc, d->iacc, d->map_idof2dof, nidof); mju_scatter(d->qfrc_constraint, d->ifrc_constraint, d->map_idof2dof, nidof); From bf722098f0083ccd2b354062ff8f2cb771e2a6b6 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 19 May 2025 12:39:33 -0700 Subject: [PATCH 152/191] Fix broken link in changelog. PiperOrigin-RevId: 760730799 Change-Id: I65e2d06f9162266b2929cc4d26af621fc3535197 --- doc/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index 9e0329f4..4ebebbb7 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -10,7 +10,7 @@ General - Refactored island implementation so that island data is memory-contiguous. This speeds up island processing in the solver and clears the way for the addition of the Newton and PGS solvers (currently only CG is supported). - Removed the :at:`shell` plugin. This is now supported by :ref:`flexcomp` and is active depending on - the :ref:`elastic2d` attribute (on by default). + the :ref:`elastic2d` attribute (on by default). simulate ^^^^^^^^ From cada50434a493a518357d111786df2ce0fd3279e Mon Sep 17 00:00:00 2001 From: Erik Frey Date: Mon, 19 May 2025 12:40:56 -0700 Subject: [PATCH 153/191] Raise an error if MJX scan functions are called on models with zero DoFs. In the future, MJX will support correctly simulating models with zero dofs. PiperOrigin-RevId: 760731314 Change-Id: Iadddf80a959c64cddb31610e60927ae0437ebcaf --- mjx/mujoco/mjx/_src/scan.py | 2 ++ mjx/mujoco/mjx/_src/scan_test.py | 5 +---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/mjx/mujoco/mjx/_src/scan.py b/mjx/mujoco/mjx/_src/scan.py index 496ba4cd..0bcfc195 100644 --- a/mjx/mujoco/mjx/_src/scan.py +++ b/mjx/mujoco/mjx/_src/scan.py @@ -132,6 +132,8 @@ def _nvmap(f: Callable[..., Y], *args) -> Y: def _check_input(m: Model, args: Any, in_types: str) -> None: """Checks that scan input has the right shape.""" + if m.nv == 0: + raise ValueError('Scan across Model with zero DoFs unsupported.') size = { 'b': m.nbody, 'j': m.njnt, diff --git a/mjx/mujoco/mjx/_src/scan_test.py b/mjx/mujoco/mjx/_src/scan_test.py index 74bf3224..758ebe89 100644 --- a/mjx/mujoco/mjx/_src/scan_test.py +++ b/mjx/mujoco/mjx/_src/scan_test.py @@ -61,10 +61,7 @@ class ScanTest(absltest.TestCase): return body_id + 1 b_in = jp.array([1]) - b_expect = jp.array([2]) - b_out = scan.flat(m, fn, 'b', 'b', b_in) - - np.testing.assert_equal(np.array(b_out), np.array(b_expect)) + self.assertRaises(ValueError, scan.flat, m, fn, 'b', 'b', b_in) def test_flat_joints(self): """Tests scanning over bodies with joints of different types.""" From 06e4937ed775833e6b11e9559f9c8549b0112ae7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 21:16:37 +0000 Subject: [PATCH 154/191] Bump setuptools from 75.5.0 to 78.1.1 in /mjx Bumps [setuptools](https://github.com/pypa/setuptools) from 75.5.0 to 78.1.1. - [Release notes](https://github.com/pypa/setuptools/releases) - [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst) - [Commits](https://github.com/pypa/setuptools/compare/v75.5.0...v78.1.1) --- updated-dependencies: - dependency-name: setuptools dependency-version: 78.1.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- mjx/requirements.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mjx/requirements.txt b/mjx/requirements.txt index 3747653a..9b8cf8c1 100644 --- a/mjx/requirements.txt +++ b/mjx/requirements.txt @@ -76,8 +76,9 @@ scipy==1.13.1; python_version == '3.9' \ --hash=sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004 \ --hash=sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24 \ --hash=sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5 -setuptools==75.5.0 \ - --hash=sha256:87cb777c3b96d638ca02031192d40390e0ad97737e27b6b4fa831bea86f2f829 +setuptools==78.1.1 \ + --hash=sha256:c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561 \ + --hash=sha256:fcc17fd9cd898242f6b4adfaca46137a9edef687f43e6f78469692a5e70d851d trimesh==4.5.2 \ --hash=sha256:2e50f3a7fd135c3045da887a1b9f91230528f3ce11d2ec1ba44750d82d6b4f73 wheel==0.45.0 \ From 1663867cba58af77019bbdd1b43815aa2223a181 Mon Sep 17 00:00:00 2001 From: Saran Tunyasuvunakool Date: Tue, 20 May 2025 04:37:32 -0700 Subject: [PATCH 155/191] Add `GL_EXT_texture_sRGB` to MuJoCo's GLAD stubs. PiperOrigin-RevId: 761025068 Change-Id: I9197f206666adf547aa5fcd0e06b54d7d4c415fa --- src/render/glad/glad.c | 7 +++++-- src/render/glad/glad.h | 25 +++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/render/glad/glad.c b/src/render/glad/glad.c index fa1642c4..72bd6f78 100644 --- a/src/render/glad/glad.c +++ b/src/render/glad/glad.c @@ -27,6 +27,7 @@ // GL_ARB_framebuffer_object, // GL_ARB_seamless_cube_map, // GL_ARB_vertex_buffer_object, +// GL_EXT_texture_sRGB, // GL_KHR_debug // Loader: True // Local files: False @@ -34,9 +35,9 @@ // Reproducible: False // // Commandline: -// --profile="compatibility" --api="gl=1.5" --generator="c" --spec="gl" --extensions="GL_ARB_clip_control,GL_ARB_depth_buffer_float,GL_ARB_framebuffer_object,GL_ARB_seamless_cube_map,GL_ARB_vertex_buffer_object,GL_KHR_debug" +// --profile="compatibility" --api="gl=1.5" --generator="c" --spec="gl" --extensions="GL_ARB_clip_control,GL_ARB_depth_buffer_float,GL_ARB_framebuffer_object,GL_ARB_seamless_cube_map,GL_ARB_vertex_buffer_object,GL_EXT_texture_sRGB,GL_KHR_debug" // Online: -// https://glad.dav1d.de/#profile=compatibility&language=c&specification=gl&loader=on&api=gl%3D1.5&extensions=GL_ARB_clip_control&extensions=GL_ARB_depth_buffer_float&extensions=GL_ARB_framebuffer_object&extensions=GL_ARB_seamless_cube_map&extensions=GL_ARB_vertex_buffer_object&extensions=GL_KHR_debug +// https://glad.dav1d.de/#profile=compatibility&language=c&specification=gl&loader=on&api=gl%3D1.5&extensions=GL_ARB_clip_control&extensions=GL_ARB_depth_buffer_float&extensions=GL_ARB_framebuffer_object&extensions=GL_ARB_seamless_cube_map&extensions=GL_ARB_vertex_buffer_object&extensions=GL_EXT_texture_sRGB&extensions=GL_KHR_debug #if defined(__GNUC__) && !defined(__clang__) #pragma GCC diagnostic push @@ -836,6 +837,7 @@ int mjGLAD_GL_ARB_depth_buffer_float = 0; int mjGLAD_GL_ARB_framebuffer_object = 0; int mjGLAD_GL_ARB_seamless_cube_map = 0; int mjGLAD_GL_ARB_vertex_buffer_object = 0; +int mjGLAD_GL_EXT_texture_sRGB = 0; int mjGLAD_GL_KHR_debug = 0; PFNGLCLIPCONTROLPROC mjGlad_glClipControl = NULL; PFNGLISRENDERBUFFERPROC mjGlad_glIsRenderbuffer = NULL; @@ -1433,6 +1435,7 @@ static int mjGlad_find_extensionsGL(void) { mjGLAD_GL_ARB_framebuffer_object = mjGlad_has_ext("GL_ARB_framebuffer_object"); mjGLAD_GL_ARB_seamless_cube_map = mjGlad_has_ext("GL_ARB_seamless_cube_map"); mjGLAD_GL_ARB_vertex_buffer_object = mjGlad_has_ext("GL_ARB_vertex_buffer_object"); + mjGLAD_GL_EXT_texture_sRGB = mjGlad_has_ext("GL_EXT_texture_sRGB"); mjGLAD_GL_KHR_debug = mjGlad_has_ext("GL_KHR_debug"); mjGlad_free_exts(); return 1; diff --git a/src/render/glad/glad.h b/src/render/glad/glad.h index 1b1fd449..620a7cbb 100644 --- a/src/render/glad/glad.h +++ b/src/render/glad/glad.h @@ -27,6 +27,7 @@ // GL_ARB_framebuffer_object, // GL_ARB_seamless_cube_map, // GL_ARB_vertex_buffer_object, +// GL_EXT_texture_sRGB, // GL_KHR_debug // Loader: True // Local files: False @@ -34,9 +35,9 @@ // Reproducible: False // // Commandline: -// --profile="compatibility" --api="gl=1.5" --generator="c" --spec="gl" --extensions="GL_ARB_clip_control,GL_ARB_depth_buffer_float,GL_ARB_framebuffer_object,GL_ARB_seamless_cube_map,GL_ARB_vertex_buffer_object,GL_KHR_debug" +// --profile="compatibility" --api="gl=1.5" --generator="c" --spec="gl" --extensions="GL_ARB_clip_control,GL_ARB_depth_buffer_float,GL_ARB_framebuffer_object,GL_ARB_seamless_cube_map,GL_ARB_vertex_buffer_object,GL_EXT_texture_sRGB,GL_KHR_debug" // Online: -// https://glad.dav1d.de/#profile=compatibility&language=c&specification=gl&loader=on&api=gl%3D1.5&extensions=GL_ARB_clip_control&extensions=GL_ARB_depth_buffer_float&extensions=GL_ARB_framebuffer_object&extensions=GL_ARB_seamless_cube_map&extensions=GL_ARB_vertex_buffer_object&extensions=GL_KHR_debug +// https://glad.dav1d.de/#profile=compatibility&language=c&specification=gl&loader=on&api=gl%3D1.5&extensions=GL_ARB_clip_control&extensions=GL_ARB_depth_buffer_float&extensions=GL_ARB_framebuffer_object&extensions=GL_ARB_seamless_cube_map&extensions=GL_ARB_vertex_buffer_object&extensions=GL_EXT_texture_sRGB&extensions=GL_KHR_debug #ifndef MUJOCO_SRC_RENDER_GLAD_GLAD_H_ #define MUJOCO_SRC_RENDER_GLAD_GLAD_H_ @@ -2420,6 +2421,22 @@ GLAPI PFNGLGETBUFFERPOINTERVPROC mjGlad_glGetBufferPointerv; #define GL_DYNAMIC_DRAW_ARB 0x88E8 #define GL_DYNAMIC_READ_ARB 0x88E9 #define GL_DYNAMIC_COPY_ARB 0x88EA +#define GL_SRGB_EXT 0x8C40 +#define GL_SRGB8_EXT 0x8C41 +#define GL_SRGB_ALPHA_EXT 0x8C42 +#define GL_SRGB8_ALPHA8_EXT 0x8C43 +#define GL_SLUMINANCE_ALPHA_EXT 0x8C44 +#define GL_SLUMINANCE8_ALPHA8_EXT 0x8C45 +#define GL_SLUMINANCE_EXT 0x8C46 +#define GL_SLUMINANCE8_EXT 0x8C47 +#define GL_COMPRESSED_SRGB_EXT 0x8C48 +#define GL_COMPRESSED_SRGB_ALPHA_EXT 0x8C49 +#define GL_COMPRESSED_SLUMINANCE_EXT 0x8C4A +#define GL_COMPRESSED_SLUMINANCE_ALPHA_EXT 0x8C4B +#define GL_COMPRESSED_SRGB_S3TC_DXT1_EXT 0x8C4C +#define GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT 0x8C4D +#define GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT 0x8C4E +#define GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT 0x8C4F #define GL_DEBUG_OUTPUT_SYNCHRONOUS 0x8242 #define GL_DEBUG_NEXT_LOGGED_MESSAGE_LENGTH 0x8243 #define GL_DEBUG_CALLBACK_FUNCTION 0x8244 @@ -2614,6 +2631,10 @@ typedef void (APIENTRYP PFNGLGETBUFFERPOINTERVARBPROC)(GLenum target, GLenum pna GLAPI PFNGLGETBUFFERPOINTERVARBPROC mjGlad_glGetBufferPointervARB; #define glGetBufferPointervARB mjGlad_glGetBufferPointervARB #endif +#ifndef GL_EXT_texture_sRGB +#define GL_EXT_texture_sRGB 1 +GLAPI int mjGLAD_GL_EXT_texture_sRGB; +#endif #ifndef GL_KHR_debug #define GL_KHR_debug 1 GLAPI int mjGLAD_GL_KHR_debug; From b728a50eb20e4b0d4f1b1db51e0ad63133aa9fbc Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Tue, 20 May 2025 09:47:06 -0700 Subject: [PATCH 156/191] Correct elastic2d default information in the changelog. PiperOrigin-RevId: 761120899 Change-Id: Id7093f9baa775fb71cf1dd7aaf74f6bd8d4d5e83 --- doc/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index 4ebebbb7..3fc979bd 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -10,9 +10,9 @@ General - Refactored island implementation so that island data is memory-contiguous. This speeds up island processing in the solver and clears the way for the addition of the Newton and PGS solvers (currently only CG is supported). - Removed the :at:`shell` plugin. This is now supported by :ref:`flexcomp` and is active depending on - the :ref:`elastic2d` attribute (on by default). + the :ref:`elastic2d` attribute (off by default). -simulate +Simulate ^^^^^^^^ - The struct ``mjv_sceneState`` has been removed. This struct was used for partial synchronization of ``mjModel`` and ``mjData`` when the Python viewer is used in passive mode. This functionality is now provided by :ref:`mjv_copyModel` From 85c84c1eceec697391332e6e3b9df85e03086e57 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 20 May 2025 12:18:14 -0700 Subject: [PATCH 157/191] Initialize Passive Viewer's internal `mjModel` and `mjData` to `nullptr`. Prevents the render thread from calling `mjv_updateScene` before these have been initialized. PiperOrigin-RevId: 761181568 Change-Id: I4f7f61fdcf7f87dcd6317883e5082d9ce2266e20 --- simulate/simulate.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/simulate/simulate.h b/simulate/simulate.h index 7e20d3c5..312b5db7 100644 --- a/simulate/simulate.h +++ b/simulate/simulate.h @@ -132,8 +132,8 @@ class Simulate { // in passive mode the user owns m_ and d_, these "passive" instances are // owned by Simulate, updated from the user by the Sync() method - mjModel* m_passive_; - mjData* d_passive_; + mjModel* m_passive_ = nullptr; + mjData* d_passive_ = nullptr; std::vector user_scn_geoms_; mjOption mjopt_prev_; From 505152073386bdbe6dd2df465ad4d37937b347ca Mon Sep 17 00:00:00 2001 From: Baruch Tabanpour Date: Tue, 20 May 2025 14:36:59 -0700 Subject: [PATCH 158/191] Delete commented code. PiperOrigin-RevId: 761234852 Change-Id: Ibea32eee47448073fff1ed2df307b1e44c78423f --- mjx/mujoco/mjx/_src/dataclasses.py | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/mjx/mujoco/mjx/_src/dataclasses.py b/mjx/mujoco/mjx/_src/dataclasses.py index c2d8fb63..b8bdb758 100644 --- a/mjx/mujoco/mjx/_src/dataclasses.py +++ b/mjx/mujoco/mjx/_src/dataclasses.py @@ -50,36 +50,6 @@ def dataclass(clz: _T, register_as_pytree: bool) -> _T: """ data_clz = dataclasses.dataclass(frozen=True)(clz) data_clz.replace = dataclasses.replace - # def replace(self, **updates): - # """Returns a new object replacing the specified fields with new values.""" - # if not hasattr(self, '_impl'): - # return dataclasses.replace(self, **updates) - - # # Private fields under `_impl` are allowed to be replaced directly as if - # # they were on the base class. This logic will be removed in a future - # # release. - # impl_updates = {} - # for k in tuple(updates.keys()): - # # Recall that getattr is overridden for '_impl' fields. - # hasattr_ = k in self.__annotations__ - # if not hasattr_ and hasattr(self._impl, k): # pylint: disable=protected-access - # impl_updates[k] = updates[k] - # del updates[k] - - # if impl_updates: - # updates['_impl'] = self._impl.replace(**impl_updates) - # warnings.warn( - # f'Accessing/replacing fields `{tuple(impl_updates.keys())}` directly' - # f' from `{self.__class__.__name__}` will be deprecated. Refrain from' - # ' using private fields that were moved to' - # f' `{self.__class__.__name__}`._impl.', - # DeprecationWarning, - # stacklevel=2, - # ) - - # return dataclasses.replace(self, **updates) - - # data_clz.replace = replace if register_as_pytree: meta_fields, data_fields = [], [] From 49c33716a1cced63d5fc35ef514d8cb1c6c95521 Mon Sep 17 00:00:00 2001 From: Erik Frey Date: Tue, 20 May 2025 15:25:10 -0700 Subject: [PATCH 159/191] Allow _realloc_con_efc to also allocate new nJ arrays. PiperOrigin-RevId: 761252071 Change-Id: Idce822c6f2b8f2d81a5d43228f5dc35281c5ad07 --- python/mujoco/bindings_test.py | 4 +++- python/mujoco/functions.cc | 14 ++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/python/mujoco/bindings_test.py b/python/mujoco/bindings_test.py index 3896b8ce..99ca73ab 100644 --- a/python/mujoco/bindings_test.py +++ b/python/mujoco/bindings_test.py @@ -623,10 +623,12 @@ class MuJoCoBindingsTest(parameterized.TestCase): ncon = 13 nefc = 17 - mujoco._functions._realloc_con_efc(self.data, ncon=ncon, nefc=nefc) + nj = 21 + mujoco._functions._realloc_con_efc(self.data, ncon=ncon, nefc=nefc, nJ=nj) self.assertLen(self.data.contact, ncon) self.assertEqual(self.data.efc_id.shape, (nefc,)) + self.assertEqual(self.data.efc_J.shape, (nj,)) self.assertEqual(self.data.efc_KBIP.shape, (nefc, 4)) expected_error = 'insufficient arena memory available' diff --git a/python/mujoco/functions.cc b/python/mujoco/functions.cc index 7fed04f7..d4708254 100644 --- a/python/mujoco/functions.cc +++ b/python/mujoco/functions.cc @@ -1467,10 +1467,10 @@ PYBIND11_MODULE(_functions, pymodule) { pymodule.def( "_realloc_con_efc", - [](MjDataWrapper& d, int ncon, int nefc) { + [](MjDataWrapper& d, int ncon, int nefc, int nJ) { raw::MjData* data = d.get(); - auto cleanup = [](raw::MjData* data) { + auto cleanup = [](raw::MjData* data, int nJ) { #ifdef ADDRESS_SANITIZER ASAN_POISON_MEMORY_REGION( static_cast(data->arena), @@ -1479,6 +1479,7 @@ PYBIND11_MODULE(_functions, pymodule) { data->parena = 0; data->ncon = 0; data->nefc = 0; + if (nJ > -1) data->nJ = 0; data->contact = static_cast(data->arena); #define X(type, name, nr, nc) data->name = nullptr; MJDATA_ARENA_POINTERS_SOLVER @@ -1486,14 +1487,15 @@ PYBIND11_MODULE(_functions, pymodule) { #undef X }; - cleanup(data); + cleanup(data, nJ); data->ncon = ncon; data->nefc = nefc; + if (nJ > -1) data->nJ = nJ; data->contact = static_cast(InterceptMjErrors(::mj_arenaAllocByte)( data, ncon * sizeof(raw::MjContact), alignof(raw::MjContact))); if (!data->contact) { - cleanup(data); + cleanup(data, nJ); throw FatalError("insufficient arena memory available"); } @@ -1505,7 +1507,7 @@ PYBIND11_MODULE(_functions, pymodule) { data->name = static_cast(InterceptMjErrors(::mj_arenaAllocByte)( \ data, sizeof(type) * (nr) * (nc), alignof(type))); \ if (!data->name) { \ - cleanup(data); \ + cleanup(data, nJ); \ throw FatalError("insufficient arena memory available"); \ } @@ -1519,7 +1521,7 @@ PYBIND11_MODULE(_functions, pymodule) { #undef MJ_M #define MJ_M(x) x }, - py::arg("d"), py::arg("ncon"), py::arg("nefc"), + py::arg("d"), py::arg("ncon"), py::arg("nefc"), py::arg("nJ") = -1, py::call_guard()); } // PYBIND11_MODULE NOLINT(readability/fn_size) } // namespace From 7da6b5b9d959d2001a004ad52a7bc10fc39d7615 Mon Sep 17 00:00:00 2001 From: Erik Frey Date: Tue, 20 May 2025 16:02:13 -0700 Subject: [PATCH 160/191] Ensure `mjx.get_data_into` returns correct efc_J, qLD, and qLDiagInv. PiperOrigin-RevId: 761264885 Change-Id: Ia3e669e24b2cc05f1506f0508c552c48233298cb --- mjx/mujoco/mjx/_src/io.py | 39 +++++++++++++++++------ mjx/mujoco/mjx/_src/io_test.py | 56 +++++++++++++++++++++++++++++++--- 2 files changed, 80 insertions(+), 15 deletions(-) diff --git a/mjx/mujoco/mjx/_src/io.py b/mjx/mujoco/mjx/_src/io.py index c919f7f4..c5b94513 100644 --- a/mjx/mujoco/mjx/_src/io.py +++ b/mjx/mujoco/mjx/_src/io.py @@ -1110,12 +1110,10 @@ def _get_data_into( ncon = (d_i._impl.contact.dist <= 0).sum() efc_active = (d_i._impl.efc_J != 0).any(axis=1) nefc = int(efc_active.sum()) - result_i.nJ = nefc * m.nv - if ncon != result_i.ncon or nefc != result_i.nefc: - mujoco._functions._realloc_con_efc(result_i, ncon=ncon, nefc=nefc) # pylint: disable=protected-access - result_i.efc_J_rownnz[:] = np.repeat(m.nv, nefc) - result_i.efc_J_rowadr[:] = np.arange(0, nefc * m.nv, m.nv) - result_i.efc_J_colind[:] = np.tile(np.arange(m.nv), nefc) + nj = (d_i._impl.efc_J != 0).sum() if support.is_sparse(m) else nefc * m.nv + + if ncon != result_i.ncon or nefc != result_i.nefc or nj != result_i.nJ: + mujoco._functions._realloc_con_efc(result_i, ncon=ncon, nefc=nefc, nJ=nj) # pylint: disable=protected-access if d.backend_impl == types.BackendImpl.JAX: all_fields = types.Data.fields() + types.DataJAX.fields() @@ -1167,16 +1165,33 @@ def _get_data_into( value = {'nefc': nefc, 'ncon': ncon}[field.name] elif field.name.endswith('xmat') or field.name == 'ximat': value = value.reshape((-1, 9)) + elif field.name == 'efc_J': + value = value[efc_active] + if support.is_sparse(m): + efc_J_rownnz = np.zeros(nefc, dtype=np.int32) + efc_J_rowadr = np.zeros(nefc, dtype=np.int32) + efc_J_colind = np.zeros(nj, dtype=np.int32) + efc_J = np.zeros(nj) + mujoco.mju_dense2sparse( + efc_J, + value, + efc_J_rownnz, + efc_J_rowadr, + efc_J_colind, + ) + result_i.efc_J_rownnz[:] = efc_J_rownnz + result_i.efc_J_rowadr[:] = efc_J_rowadr + result_i.efc_J_colind[:] = efc_J_colind + value = efc_J + else: + value = value.reshape(-1) elif field.name.startswith('efc_'): value = value[efc_active] - if field.name == 'efc_J': - value = value.reshape(-1) if d.backend_impl == types.BackendImpl.JAX: if field.name == 'qM' and not support.is_sparse(m): value = value[dof_i, dof_j] elif field.name == 'qLD' and not support.is_sparse(m): - # TODO(erikfrey): provide correct qLDs - value = np.zeros(m.nM) + value = np.zeros(m.nC) elif field.name == 'qLDiagInv' and not support.is_sparse(m): value = np.ones(m.nv) @@ -1191,6 +1206,10 @@ def _get_data_into( else: setattr(result_i, field.name, value) + # recalculate qLD and qLDiagInv as MJX and MuJoCo have different + # representations of the Cholesky decomposition. + mujoco.mj_factorM(m, result_i) + def get_data_into( result: Union[mujoco.MjData, List[mujoco.MjData]], diff --git a/mjx/mujoco/mjx/_src/io_test.py b/mjx/mujoco/mjx/_src/io_test.py index 78f81e3f..33565999 100644 --- a/mjx/mujoco/mjx/_src/io_test.py +++ b/mjx/mujoco/mjx/_src/io_test.py @@ -96,6 +96,18 @@ _MULTIPLE_CONSTRAINTS = """ """ +_SIMPLE_BODY = """ + + + + + + + + + +""" + class ModelIOTest(parameterized.TestCase): """IO tests for mjx.Model.""" @@ -427,11 +439,14 @@ class DataIOTest(parameterized.TestCase): elif backend_impl == 'c': np.testing.assert_allclose(dx_from_dense._impl.qM, d.qM, atol=1e-8) - @parameterized.parameters('jax', 'c') - def test_get_data(self, backend_impl: str): + @parameterized.parameters( + ('jax', False), ('jax', True), ('c', False), ('c', True) + ) + def test_get_data(self, backend_impl: str, sparse: bool): """Test that get_data makes correct MjData.""" - m = mujoco.MjModel.from_xml_string(_MULTIPLE_CONSTRAINTS) + if sparse: + m.opt.jacobian = mujoco.mjtJacobian.mjJAC_SPARSE d = mujoco.MjData(m) mujoco.mj_step(m, d, 2) dx = mjx.put_data(m, d, backend_impl=backend_impl) @@ -443,6 +458,8 @@ class DataIOTest(parameterized.TestCase): np.testing.assert_allclose(d_2.cvel, d.cvel) np.testing.assert_allclose(d_2.cdof_dot, d.cdof_dot) np.testing.assert_allclose(d_2.qM, d.qM) + np.testing.assert_allclose(d_2.qLD, d.qLD, atol=1e-6) + np.testing.assert_allclose(d_2.qLDiagInv, d.qLDiagInv, atol=1e-6) # only 1 contact active self.assertEqual(d_2.contact.dist.shape, (1,)) @@ -463,8 +480,27 @@ class DataIOTest(parameterized.TestCase): # efc_* are also shape transformed and filtered self.assertEqual(d_2.nefc, 14) - self.assertEqual(d_2.efc_J.shape, (112,)) # nefc * nv - np.testing.assert_allclose(d_2.efc_J, d.efc_J) + if sparse: + efc_j = np.zeros((d.nefc, m.nv)) + mujoco.mju_sparse2dense( + efc_j, + d.efc_J, + d.efc_J_rownnz, + d.efc_J_rowadr, + d.efc_J_colind, + ) + efc_j2 = np.zeros((d_2.nefc, m.nv)) + mujoco.mju_sparse2dense( + efc_j2, + d_2.efc_J, + d_2.efc_J_rownnz, + d_2.efc_J_rowadr, + d_2.efc_J_colind, + ) + np.testing.assert_allclose(efc_j, efc_j2) + else: + self.assertEqual(d_2.efc_J.shape, (112,)) # nefc * nv + np.testing.assert_allclose(d_2.efc_J, d.efc_J) self.assertEqual(d_2.efc_aref.shape, (14,)) # nefc np.testing.assert_allclose(d_2.efc_aref, d.efc_aref) np.testing.assert_allclose(d_2.contact.efc_address, d.contact.efc_address) @@ -473,6 +509,16 @@ class DataIOTest(parameterized.TestCase): # check fields specific to the C implementation np.testing.assert_allclose(d_2.bvh_active, d.bvh_active) + def test_get_data_simplebody(self): + """Test that get_data works with simple bodies where nC < nM.""" + m = mujoco.MjModel.from_xml_string(_SIMPLE_BODY) + d = mujoco.MjData(m) + mujoco.mj_step(m, d, 2) + dx = mjx.put_data(m, d) + d_2: mujoco.MjData = mjx.get_data(m, dx) + np.testing.assert_allclose(d_2.qLD, d.qLD, atol=1e-6) + np.testing.assert_allclose(d_2.qLDiagInv, d.qLDiagInv, atol=1e-6) + def test_get_data_runs(self): xml = """ From bf1f8081c1b117225e855c6f397823f3a3521345 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Wed, 21 May 2025 07:51:59 -0700 Subject: [PATCH 161/191] Enable Visualization pane in Passive Viewer. Fixes #2158 PiperOrigin-RevId: 761522429 Change-Id: Ice4322ce413891893c89856e55f60a4f26010cf2 --- simulate/simulate.cc | 63 +++++++++++++++++++------------------------- simulate/simulate.h | 3 +++ 2 files changed, 30 insertions(+), 36 deletions(-) diff --git a/simulate/simulate.cc b/simulate/simulate.cc index 77db1a04..66087397 100644 --- a/simulate/simulate.cc +++ b/simulate/simulate.cc @@ -879,7 +879,7 @@ void MakeVisualizationSection(mj::Simulate* sim, const mjModel* m) { mjuiDef defVisualization[] = { {mjITEM_SECTION, "Visualization", mjPRESERVE, nullptr, "AV"}, {mjITEM_SEPARATOR, "Headlight", 1}, - {mjITEM_RADIO, "Active", 5, &(vis->headlight.active), "Off\nOn"}, + {mjITEM_RADIO, "Active", 2, &(vis->headlight.active), "Off\nOn"}, {mjITEM_EDITFLOAT, "Ambient", 2, &(vis->headlight.ambient), "3"}, {mjITEM_EDITFLOAT, "Diffuse", 2, &(vis->headlight.diffuse), "3"}, {mjITEM_EDITFLOAT, "Specular", 2, &(vis->headlight.specular), "3"}, @@ -892,7 +892,7 @@ void MakeVisualizationSection(mj::Simulate* sim, const mjModel* m) { {mjITEM_BUTTON, "Align", 2, nullptr, "CA"}, {mjITEM_SEPARATOR, "Global", 1}, {mjITEM_EDITNUM, "Extent", 2, &(stat->extent), "1"}, - {mjITEM_RADIO, "Inertia", 5, &(vis->global.ellipsoidinertia), "Box\nEllipsoid"}, + {mjITEM_RADIO, "Inertia", 2, &(vis->global.ellipsoidinertia), "Box\nEllipsoid"}, {mjITEM_RADIO, "BVH active", 5, &(vis->global.bvactive), "False\nTrue"}, {mjITEM_SEPARATOR, "Map", 1}, {mjITEM_EDITFLOAT, "Stiffness", 2, &(vis->map.stiffness), "1"}, @@ -1931,43 +1931,25 @@ void Simulate::Sync() { } } + // in passive mode, synchronize user's mjModel with changes made via the UI if (is_passive_) { - // synchronize m_->opt with changes made via the UI -#define X(name) \ - if (IsDifferent(m_passive_->opt.name, mjopt_prev_.name)) { \ - pending_.ui_update_physics = true; \ - Copy(m_->opt.name, m_passive_->opt.name); \ - } + // synchronize mjModel.opt + if (std::memcmp(&m_passive_->opt, &mjopt_prev_, sizeof(mjOption))) { + pending_.ui_update_physics = true; + m_->opt = m_passive_->opt; + } - X(timestep); - X(apirate); - X(impratio); - X(tolerance); - X(noslip_tolerance); - X(ccd_tolerance); - X(gravity); - X(wind); - X(magnetic); - X(density); - X(viscosity); - X(o_margin); - X(o_solref); - X(o_solimp); - X(o_friction); - X(integrator); - X(cone); - X(jacobian); - X(solver); - X(iterations); - X(noslip_iterations); - X(ccd_iterations); - X(disableflags); - X(enableflags); - X(disableactuator); - X(sdf_initpoints); - X(sdf_iterations); + // synchronize mjModel.vis + if (std::memcmp(&m_passive_->vis, &mjvis_prev_, sizeof(mjVisual))) { + pending_.ui_update_visualization = true; + m_->vis = m_passive_->vis; + } - #undef X + // synchronize mjModel.stat + if (std::memcmp(&m_passive_->stat, &mjstat_prev_, sizeof(mjStatistic))) { + pending_.ui_update_visualization = true; + m_->stat = m_passive_->stat; + } // synchronize number of mjWARN_VGEOMFULL warnings if (d_passive_->warning[mjWARN_VGEOMFULL].number > warn_vgeomfull_prev_) { @@ -2150,6 +2132,8 @@ void Simulate::Sync() { } mjopt_prev_ = m_passive_->opt; + mjvis_prev_ = m_passive_->vis; + mjstat_prev_ = m_passive_->stat; warn_vgeomfull_prev_ = d_passive_->warning[mjWARN_VGEOMFULL].number; } @@ -2471,6 +2455,13 @@ void Simulate::Render() { pending_.ui_update_physics = false; } + if (pending_.ui_update_visualization) { + if (this->ui0_enable && this->ui0.sect[SECT_VISUALIZATION].state) { + mjui0_update_section(this, SECT_VISUALIZATION); + } + pending_.ui_update_visualization = false; + } + if (is_passive_) { if (this->ui0_enable && this->ui0.sect[SECT_RENDERING].state && (cam_prev_.type != cam.type || diff --git a/simulate/simulate.h b/simulate/simulate.h index 312b5db7..70b4ddf4 100644 --- a/simulate/simulate.h +++ b/simulate/simulate.h @@ -137,6 +137,8 @@ class Simulate { std::vector user_scn_geoms_; mjOption mjopt_prev_; + mjVisual mjvis_prev_; + mjStatistic mjstat_prev_; mjvOption opt_prev_; mjvCamera cam_prev_; @@ -162,6 +164,7 @@ class Simulate { bool ui_update_simulation; bool ui_update_physics; bool ui_update_rendering; + bool ui_update_visualization; bool ui_update_joint; bool ui_update_ctrl; bool ui_remake_ctrl; From 62a9d5b98d80ad6cc8b7756e9284194d05b108ad Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Wed, 21 May 2025 13:02:32 -0700 Subject: [PATCH 162/191] Add MjcPhysicsSceneAPI support. Still missing compiler settings but will follow up in a future CL. Tried to add full coverage for testing setting of attributes to non-default values. PiperOrigin-RevId: 761636862 Change-Id: Id4b9486645b2600931556cdcb8dc71f28ec309cd --- .../usd/plugins/mjcf/mujoco_to_usd.cc | 234 ++++++++-- .../usd/mjcPhysics/mjc_physics_scene_test.cc | 190 ++++++++ .../usd/plugins/mjcf/mjcf_file_format_test.cc | 426 +++++++++++++++++- test/experimental/usd/test_utils.cc | 4 +- test/experimental/usd/test_utils.h | 10 +- 5 files changed, 829 insertions(+), 35 deletions(-) create mode 100644 test/experimental/usd/mjcPhysics/mjc_physics_scene_test.cc diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc index 45d59d66..289177b2 100644 --- a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -47,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -114,6 +116,8 @@ using pxr::TfCallContext; using pxr::Tf_PostErrorHelper; // clang-format on +using pxr::MjcPhysicsTokens; + using mujoco::usd::AddAttributeConnection; using mujoco::usd::AddPrimInherit; using mujoco::usd::AddPrimReference; @@ -175,6 +179,8 @@ class ModelWriter { SetLayerMetadata(data_, pxr::SdfFieldKeys->DefaultPrim, body_paths_[kWorldIndex].GetNameToken()); + WritePhysicsScene(); + // Author mesh scope + mesh prims to be referenced. WriteMeshes(); WriteMaterials(); @@ -304,6 +310,15 @@ class ModelWriter { SetAttributeDefault(data_, xform_op_order_path, order); } + template + void WriteUniformAttribute(const pxr::SdfPath &prim_path, + const pxr::SdfValueTypeName &value_type_name, + const pxr::TfToken &token, const T &value) { + pxr::SdfPath attr_path = CreateAttributeSpec( + data_, prim_path, token, value_type_name, pxr::SdfVariabilityUniform); + SetAttributeDefault(data_, attr_path, value); + } + void PrependToXformOpOrder(const pxr::SdfPath &prim_path, const pxr::VtArray &order) { auto xform_op_order_path = @@ -432,6 +447,185 @@ class ModelWriter { pxr::UsdGeomTokens->none); } + void WritePhysicsScene() { + pxr::SdfPath physics_scene_path = CreatePrimSpec( + data_, body_paths_[kWorldIndex], pxr::UsdPhysicsTokens->PhysicsScene, + pxr::UsdPhysicsTokens->PhysicsScene); + + ApplyApiSchema(data_, physics_scene_path, MjcPhysicsTokens->SceneAPI); + + const std::vector> + option_double_attributes = { + {MjcPhysicsTokens->mjcOptionTimestep, spec_->option.timestep}, + {MjcPhysicsTokens->mjcOptionTolerance, spec_->option.tolerance}, + {MjcPhysicsTokens->mjcOptionLs_tolerance, + spec_->option.ls_tolerance}, + {MjcPhysicsTokens->mjcOptionNoslip_tolerance, + spec_->option.noslip_tolerance}, + {MjcPhysicsTokens->mjcOptionCcd_tolerance, + spec_->option.ccd_tolerance}, + {MjcPhysicsTokens->mjcOptionApirate, spec_->option.apirate}, + {MjcPhysicsTokens->mjcOptionImpratio, spec_->option.impratio}, + {MjcPhysicsTokens->mjcOptionDensity, spec_->option.density}, + {MjcPhysicsTokens->mjcOptionViscosity, spec_->option.viscosity}, + {MjcPhysicsTokens->mjcOptionO_margin, spec_->option.o_margin}, + }; + for (const auto &[token, value] : option_double_attributes) { + WriteUniformAttribute(physics_scene_path, pxr::SdfValueTypeNames->Double, + token, value); + } + + const std::vector> option_int_attributes = { + {MjcPhysicsTokens->mjcOptionIterations, spec_->option.iterations}, + {MjcPhysicsTokens->mjcOptionLs_iterations, spec_->option.ls_iterations}, + {MjcPhysicsTokens->mjcOptionNoslip_iterations, + spec_->option.noslip_iterations}, + {MjcPhysicsTokens->mjcOptionCcd_iterations, + spec_->option.ccd_iterations}, + {MjcPhysicsTokens->mjcOptionSdf_iterations, + spec_->option.sdf_iterations}, + {MjcPhysicsTokens->mjcOptionSdf_initpoints, + spec_->option.sdf_initpoints}, + }; + for (const auto &[token, value] : option_int_attributes) { + WriteUniformAttribute(physics_scene_path, pxr::SdfValueTypeNames->Int, + token, value); + } + + pxr::SdfPath cone_attr = CreateAttributeSpec( + data_, physics_scene_path, MjcPhysicsTokens->mjcOptionCone, + pxr::SdfValueTypeNames->Token, pxr::SdfVariabilityUniform); + + switch (spec_->option.cone) { + case mjCONE_PYRAMIDAL: + SetAttributeDefault(data_, cone_attr, MjcPhysicsTokens->pyramidal); + break; + case mjCONE_ELLIPTIC: + SetAttributeDefault(data_, cone_attr, MjcPhysicsTokens->elliptic); + break; + default: + break; + } + + pxr::SdfPath jacobian_attr = CreateAttributeSpec( + data_, physics_scene_path, MjcPhysicsTokens->mjcOptionJacobian, + pxr::SdfValueTypeNames->Token, pxr::SdfVariabilityUniform); + + switch (spec_->option.jacobian) { + case mjJAC_AUTO: + SetAttributeDefault(data_, jacobian_attr, MjcPhysicsTokens->auto_); + break; + case mjJAC_DENSE: + SetAttributeDefault(data_, jacobian_attr, MjcPhysicsTokens->dense); + break; + case mjJAC_SPARSE: + SetAttributeDefault(data_, jacobian_attr, MjcPhysicsTokens->sparse); + break; + default: + break; + } + + pxr::SdfPath solver_attr = CreateAttributeSpec( + data_, physics_scene_path, MjcPhysicsTokens->mjcOptionSolver, + pxr::SdfValueTypeNames->Token, pxr::SdfVariabilityUniform); + switch (spec_->option.solver) { + case mjSOL_NEWTON: + SetAttributeDefault(data_, solver_attr, MjcPhysicsTokens->newton); + break; + case mjSOL_PGS: + SetAttributeDefault(data_, solver_attr, MjcPhysicsTokens->pgs); + break; + case mjSOL_CG: + SetAttributeDefault(data_, solver_attr, MjcPhysicsTokens->cg); + break; + default: + break; + } + + pxr::GfVec3d wind(spec_->option.wind[0], spec_->option.wind[1], + spec_->option.wind[2]); + WriteUniformAttribute(physics_scene_path, pxr::SdfValueTypeNames->Double3, + MjcPhysicsTokens->mjcOptionWind, wind); + + pxr::GfVec3d magnetic(spec_->option.magnetic[0], spec_->option.magnetic[1], + spec_->option.magnetic[2]); + WriteUniformAttribute(physics_scene_path, pxr::SdfValueTypeNames->Double3, + MjcPhysicsTokens->mjcOptionMagnetic, magnetic); + + pxr::VtArray o_solref(spec_->option.o_solref, + spec_->option.o_solref + 2); + WriteUniformAttribute(physics_scene_path, + pxr::SdfValueTypeNames->DoubleArray, + MjcPhysicsTokens->mjcOptionO_solref, o_solref); + + pxr::VtArray o_solimp(spec_->option.o_solimp, + spec_->option.o_solimp + 5); + WriteUniformAttribute(physics_scene_path, + pxr::SdfValueTypeNames->DoubleArray, + MjcPhysicsTokens->mjcOptionO_solimp, o_solimp); + + pxr::VtArray o_friction(spec_->option.o_friction, + spec_->option.o_friction + 5); + WriteUniformAttribute(physics_scene_path, + pxr::SdfValueTypeNames->DoubleArray, + MjcPhysicsTokens->mjcOptionO_friction, o_friction); + + pxr::SdfPath integrator_attr = CreateAttributeSpec( + data_, physics_scene_path, MjcPhysicsTokens->mjcOptionIntegrator, + pxr::SdfValueTypeNames->Token, pxr::SdfVariabilityUniform); + switch (spec_->option.integrator) { + case mjINT_EULER: + SetAttributeDefault(data_, integrator_attr, MjcPhysicsTokens->euler); + break; + case mjINT_RK4: + SetAttributeDefault(data_, integrator_attr, MjcPhysicsTokens->rk4); + break; + default: + break; + } + + auto create_flag_attr = [&](pxr::TfToken token, int flag, bool enable) { + int flags = + enable ? spec_->option.enableflags : spec_->option.disableflags; + bool value = enable ? (flags & flag) : !(flags & flag); + WriteUniformAttribute(physics_scene_path, pxr::SdfValueTypeNames->Bool, + token, value); + }; + + const std::vector> enable_flags = { + {MjcPhysicsTokens->mjcFlagMulticcd, mjENBL_MULTICCD}, + {MjcPhysicsTokens->mjcFlagIsland, mjENBL_ISLAND}, + {MjcPhysicsTokens->mjcFlagFwdinv, mjENBL_FWDINV}, + {MjcPhysicsTokens->mjcFlagEnergy, mjENBL_ENERGY}, + {MjcPhysicsTokens->mjcFlagOverride, mjENBL_OVERRIDE}, + {MjcPhysicsTokens->mjcFlagInvdiscrete, mjENBL_INVDISCRETE}}; + for (const auto &[token, flag] : enable_flags) { + create_flag_attr(token, flag, true); + } + + const std::vector> disable_flags = { + {MjcPhysicsTokens->mjcFlagConstraint, mjDSBL_CONSTRAINT}, + {MjcPhysicsTokens->mjcFlagEquality, mjDSBL_EQUALITY}, + {MjcPhysicsTokens->mjcFlagFrictionloss, mjDSBL_FRICTIONLOSS}, + {MjcPhysicsTokens->mjcFlagLimit, mjDSBL_LIMIT}, + {MjcPhysicsTokens->mjcFlagContact, mjDSBL_CONTACT}, + {MjcPhysicsTokens->mjcFlagPassive, mjDSBL_PASSIVE}, + {MjcPhysicsTokens->mjcFlagGravity, mjDSBL_GRAVITY}, + {MjcPhysicsTokens->mjcFlagClampctrl, mjDSBL_CLAMPCTRL}, + {MjcPhysicsTokens->mjcFlagWarmstart, mjDSBL_WARMSTART}, + {MjcPhysicsTokens->mjcFlagFilterparent, mjDSBL_FILTERPARENT}, + {MjcPhysicsTokens->mjcFlagActuation, mjDSBL_ACTUATION}, + {MjcPhysicsTokens->mjcFlagRefsafe, mjDSBL_REFSAFE}, + {MjcPhysicsTokens->mjcFlagSensor, mjDSBL_SENSOR}, + {MjcPhysicsTokens->mjcFlagMidphase, mjDSBL_MIDPHASE}, + {MjcPhysicsTokens->mjcFlagEulerdamp, mjDSBL_EULERDAMP}, + {MjcPhysicsTokens->mjcFlagAutoreset, mjDSBL_AUTORESET}, + {MjcPhysicsTokens->mjcFlagNativeccd, mjDSBL_NATIVECCD}}; + for (const auto &[token, flag] : disable_flags) { + create_flag_attr(token, flag, false); + } + } + void WriteMeshes() { // Create a scope for the meshes to keep things organized pxr::SdfPath scope_path = @@ -835,7 +1029,7 @@ class ModelWriter { pxr::SdfPath site_path = WriteSiteGeom(site, body_path); SetPrimPurpose(data_, site_path, pxr::UsdGeomTokens->guide); - ApplyApiSchema(data_, site_path, pxr::MjcPhysicsTokens->SiteAPI); + ApplyApiSchema(data_, site_path, MjcPhysicsTokens->SiteAPI); int site_id = mjs_getId(site->element); auto transform = MujocoPosQuatToTransform(&model_->site_pos[3 * site_id], @@ -1022,26 +1216,17 @@ class ModelWriter { (cam_sensorsize[0] / 2.f - cam_intrinsic[2]) : vertical_apperture * aspect_ratio; - pxr::SdfPath clipping_range_attr_path = CreateAttributeSpec( - data_, camera_path, pxr::UsdGeomTokens->clippingRange, - pxr::SdfValueTypeNames->Float2); - SetAttributeDefault(data_, clipping_range_attr_path, - pxr::GfVec2f(znear, zfar)); - pxr::SdfPath focal_length_attr_path = - CreateAttributeSpec(data_, camera_path, pxr::UsdGeomTokens->focalLength, - pxr::SdfValueTypeNames->Float); - SetAttributeDefault(data_, focal_length_attr_path, znear); - - pxr::SdfPath vertical_aperture_attr_path = CreateAttributeSpec( - data_, camera_path, pxr::UsdGeomTokens->verticalAperture, - pxr::SdfValueTypeNames->Float); - SetAttributeDefault(data_, vertical_aperture_attr_path, vertical_apperture); - - pxr::SdfPath horizontal_aperture_attr_path = CreateAttributeSpec( - data_, camera_path, pxr::UsdGeomTokens->horizontalAperture, - pxr::SdfValueTypeNames->Float); - SetAttributeDefault(data_, horizontal_aperture_attr_path, - horizontal_aperture); + WriteUniformAttribute(camera_path, pxr::SdfValueTypeNames->Float2, + pxr::UsdGeomTokens->clippingRange, + pxr::GfVec2f(znear, zfar)); + WriteUniformAttribute(camera_path, pxr::SdfValueTypeNames->Float, + pxr::UsdGeomTokens->focalLength, znear); + WriteUniformAttribute(camera_path, pxr::SdfValueTypeNames->Float, + pxr::UsdGeomTokens->verticalAperture, + vertical_apperture); + WriteUniformAttribute(camera_path, pxr::SdfValueTypeNames->Float, + pxr::UsdGeomTokens->horizontalAperture, + horizontal_aperture); } void WriteCameras(mjsBody *body) { @@ -1105,10 +1290,6 @@ class ModelWriter { } // Create XformOp attribute for body transform. - pxr::SdfPath xform_op_path = - CreateAttributeSpec(data_, body_path, kTokens->xformOpTransform, - pxr::SdfValueTypeNames->Matrix4d); - // Make sure to account for the parent since UsdPhysics doesn't support // nested bodies! auto parent_xform = body_xforms_[model_->body_parentid[body_id]]; @@ -1118,7 +1299,8 @@ class ModelWriter { MujocoPosQuatToTransform(&model_->body_pos[body_id * 3], &model_->body_quat[body_id * 4]) * parent_xform; - SetAttributeDefault(data_, xform_op_path, body_xforms_[body_id]); + WriteUniformAttribute(body_path, pxr::SdfValueTypeNames->Matrix4d, + kTokens->xformOpTransform, body_xforms_[body_id]); // Create XformOpOrder attribute for body transform order. // For us this is simply the transform we authored above. diff --git a/test/experimental/usd/mjcPhysics/mjc_physics_scene_test.cc b/test/experimental/usd/mjcPhysics/mjc_physics_scene_test.cc new file mode 100644 index 00000000..534dcffe --- /dev/null +++ b/test/experimental/usd/mjcPhysics/mjc_physics_scene_test.cc @@ -0,0 +1,190 @@ +// Copyright 2025 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. + +#include +#include +#include +#include "src/experimental/usd/mjcPhysics/sceneAPI.h" +#include "test/fixture.h" +#include +#include +#include +#include +#include +#include + +namespace mujoco { +namespace { + +using pxr::SdfPath; +using MjcPhysicsSceneTest = MujocoTest; +using testing::NotNull; + +// clang-format off +#define EXPECT_TYPE_USD_FALLBACK_EQ_MODEL_DEFAULT(type, usd_attr, mjc_attr) \ + { \ + type value; \ + mjc_phys_scene.Get##usd_attr##Attr().Get(&value); \ + EXPECT_EQ((mjtNum)value, default_model->opt.mjc_attr); \ + } + + +#define EXPECT_REAL_USD_FALLBACK_EQ_MODEL_DEFAULT(usd_attr, mjc_attr) \ + EXPECT_TYPE_USD_FALLBACK_EQ_MODEL_DEFAULT(double, usd_attr, mjc_attr) + +#define EXPECT_DISABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(usd_attr, mjc_flag) \ + { \ + bool flag; \ + mjc_phys_scene.Get##usd_attr##Attr().Get(&flag); \ + EXPECT_NE(flag, default_model->opt.disableflags & (mjc_flag)); \ + } + +#define EXPECT_ENABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(usd_attr, mjc_flag) \ + { \ + bool flag; \ + mjc_phys_scene.Get##usd_attr##Attr().Get(&flag); \ + EXPECT_EQ(flag, default_model->opt.enableflags & (mjc_flag)); \ + } + + +#define EXPECT_INT_USD_FALLBACK_EQ_MODEL_DEFAULT(usd_attr, mjc_attr) \ + EXPECT_TYPE_USD_FALLBACK_EQ_MODEL_DEFAULT(int, usd_attr, mjc_attr) + +#define EXPECT_VEC3_USD_FALLBACK_EQ_MODEL_DEFAULT(usd_attr, mjc_attr) \ + { \ + pxr::GfVec3d value; \ + mjc_phys_scene.Get##usd_attr##Attr().Get(&value); \ + EXPECT_EQ(value[0], default_model->opt.mjc_attr[0]); \ + EXPECT_EQ(value[1], default_model->opt.mjc_attr[1]); \ + EXPECT_EQ(value[2], default_model->opt.mjc_attr[2]); \ + } + +#define EXPECT_TYPE_ARR_USD_FALLBACK_EQ_MODEL_DEFAULT(type, usd_attr, mjc_attr)\ + { \ + pxr::Vt##type##Array mjc_attr; \ + mjc_phys_scene.Get##usd_attr##Attr().Get(&mjc_attr); \ + EXPECT_THAT( \ + mjc_attr, \ + testing::ElementsAreArray(default_model->opt.mjc_attr) \ + ); \ + } + +#define EXPECT_REAL_ARR_USD_FALLBACK_EQ_MODEL_DEFAULT(usd_attr, mjc_attr)\ + EXPECT_TYPE_ARR_USD_FALLBACK_EQ_MODEL_DEFAULT(Double, usd_attr, mjc_attr) + +#define EXPECT_INT_ARR_USD_FALLBACK_EQ_MODEL_DEFAULT(usd_attr, mjc_attr)\ + EXPECT_TYPE_ARR_USD_FALLBACK_EQ_MODEL_DEFAULT(Int, usd_attr, mjc_attr) +// clang-format on + +TEST_F(MjcPhysicsSceneTest, TestDefaults) { + auto stage = pxr::UsdStage::CreateInMemory(); + + auto physics_scene = + pxr::UsdPhysicsScene::Define(stage, SdfPath("/World/PhysicsScene")); + + auto mjc_phys_scene = pxr::MjcPhysicsSceneAPI::Apply(physics_scene.GetPrim()); + + mjSpec* empty_spec = mj_makeSpec(); + mjModel* default_model = mj_compile(empty_spec, nullptr); + EXPECT_THAT(default_model, NotNull()); + + // Check that all the USD schema fallback values are the same + // as the model defaults. + // If this test is failing due to an update of defaults in Mujoco you need to + // update mjcPhysics/schema.usda. + EXPECT_REAL_USD_FALLBACK_EQ_MODEL_DEFAULT(Timestep, timestep); + EXPECT_REAL_USD_FALLBACK_EQ_MODEL_DEFAULT(ApiRate, apirate); + EXPECT_REAL_USD_FALLBACK_EQ_MODEL_DEFAULT(ImpRatio, impratio); + + EXPECT_VEC3_USD_FALLBACK_EQ_MODEL_DEFAULT(Wind, wind); + EXPECT_VEC3_USD_FALLBACK_EQ_MODEL_DEFAULT(Magnetic, magnetic); + + EXPECT_REAL_USD_FALLBACK_EQ_MODEL_DEFAULT(Density, density); + EXPECT_REAL_USD_FALLBACK_EQ_MODEL_DEFAULT(Viscosity, viscosity); + + EXPECT_REAL_USD_FALLBACK_EQ_MODEL_DEFAULT(OMargin, o_margin); + EXPECT_REAL_ARR_USD_FALLBACK_EQ_MODEL_DEFAULT(OSolRef, o_solref); + EXPECT_REAL_ARR_USD_FALLBACK_EQ_MODEL_DEFAULT(OSolImp, o_solimp); + EXPECT_REAL_ARR_USD_FALLBACK_EQ_MODEL_DEFAULT(OFriction, o_friction); + + EXPECT_INT_USD_FALLBACK_EQ_MODEL_DEFAULT(Iterations, iterations); + EXPECT_REAL_USD_FALLBACK_EQ_MODEL_DEFAULT(Tolerance, tolerance); + EXPECT_INT_USD_FALLBACK_EQ_MODEL_DEFAULT(LSIterations, ls_iterations); + EXPECT_REAL_USD_FALLBACK_EQ_MODEL_DEFAULT(LSTolerance, ls_tolerance); + EXPECT_INT_USD_FALLBACK_EQ_MODEL_DEFAULT(NoslipIterations, noslip_iterations); + EXPECT_REAL_USD_FALLBACK_EQ_MODEL_DEFAULT(NoslipTolerance, noslip_tolerance); + EXPECT_INT_USD_FALLBACK_EQ_MODEL_DEFAULT(CCDIterations, ccd_iterations); + EXPECT_REAL_USD_FALLBACK_EQ_MODEL_DEFAULT(CCDTolerance, ccd_tolerance); + EXPECT_INT_USD_FALLBACK_EQ_MODEL_DEFAULT(SDFIterations, sdf_iterations); + EXPECT_INT_USD_FALLBACK_EQ_MODEL_DEFAULT(SDFInitPoints, sdf_initpoints); + + // We store the actuator disable groups as an array of integers, but the + // model stores it as a bitmask. + pxr::VtIntArray actuator_group_disable; + mjc_phys_scene.GetActuatorGroupDisableAttr().Get(&actuator_group_disable); + int bitmask = 0; + for (int ind : actuator_group_disable) { + bitmask |= 1 << ind; + } + EXPECT_EQ(default_model->opt.disableactuator, bitmask); + + EXPECT_DISABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(ConstraintFlag, + mjDSBL_CONSTRAINT); + EXPECT_DISABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(EqualityFlag, + mjDSBL_EQUALITY); + EXPECT_DISABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(FrictionLossFlag, + mjDSBL_FRICTIONLOSS); + EXPECT_DISABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(LimitFlag, mjDSBL_LIMIT); + EXPECT_DISABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(ContactFlag, + mjDSBL_CONTACT); + EXPECT_DISABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(PassiveFlag, + mjDSBL_PASSIVE); + EXPECT_DISABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(GravityFlag, + mjDSBL_GRAVITY); + EXPECT_DISABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(ClampCtrlFlag, + mjDSBL_CLAMPCTRL); + EXPECT_DISABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(WarmStartFlag, + mjDSBL_WARMSTART); + EXPECT_DISABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(FilterParentFlag, + mjDSBL_FILTERPARENT); + EXPECT_DISABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(ActuationFlag, + mjDSBL_ACTUATION); + EXPECT_DISABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(RefSafeFlag, + mjDSBL_REFSAFE); + EXPECT_DISABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(SensorFlag, mjDSBL_SENSOR); + EXPECT_DISABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(MidPhaseFlag, + mjDSBL_MIDPHASE); + EXPECT_DISABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(NativeCCDFlag, + mjDSBL_NATIVECCD); + EXPECT_DISABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(EulerDampFlag, + mjDSBL_EULERDAMP); + EXPECT_DISABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(AutoResetFlag, + mjDSBL_AUTORESET); + + EXPECT_ENABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(OverrideFlag, + mjENBL_OVERRIDE); + EXPECT_ENABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(EnergyFlag, mjENBL_ENERGY); + EXPECT_ENABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(FwdinvFlag, mjENBL_FWDINV); + EXPECT_ENABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(InvDiscreteFlag, + mjENBL_INVDISCRETE); + EXPECT_ENABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(MultiCCDFlag, + mjENBL_MULTICCD); + EXPECT_ENABLE_FLAG_USD_FALLBACK_EQ_MODEL_DEFAULT(IslandFlag, mjENBL_ISLAND); + + mj_deleteModel(default_model); + mj_deleteSpec(empty_spec); +} + +} // namespace +} // namespace mujoco diff --git a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc index 8186af02..b1359540 100644 --- a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc +++ b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc @@ -19,9 +19,11 @@ #include #include "src/experimental/usd/mjcPhysics/sceneAPI.h" #include "src/experimental/usd/mjcPhysics/siteAPI.h" +#include "src/experimental/usd/mjcPhysics/tokens.h" #include "test/experimental/usd/test_utils.h" #include "test/fixture.h" #include +#include #include #include #include @@ -49,6 +51,7 @@ #include #include #include +#include #include PXR_NAMESPACE_OPEN_SCOPE @@ -63,6 +66,8 @@ namespace mujoco { namespace usd { namespace { +using pxr::MjcPhysicsSiteAPI; +using pxr::MjcPhysicsTokens; using pxr::SdfPath; using MjcfSdfFileFormatPluginTest = MujocoTest; @@ -499,6 +504,417 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestGeomsPrims) { pxr::GfVec3f(10.0, 20.0, 30.0)); } +static const pxr::SdfPath kPhysicsScenePrimPath("/test/PhysicsScene"); + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimTimestep) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual( + stage, + kPhysicsScenePrimPath.AppendProperty(MjcPhysicsTokens->mjcOptionTimestep), + 0.005); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimCone) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual( + stage, + kPhysicsScenePrimPath.AppendProperty(MjcPhysicsTokens->mjcOptionCone), + MjcPhysicsTokens->elliptic); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimWind) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual( + stage, + kPhysicsScenePrimPath.AppendProperty(MjcPhysicsTokens->mjcOptionWind), + pxr::GfVec3d(1, 2, 3)); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimApirate) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual( + stage, + kPhysicsScenePrimPath.AppendProperty(MjcPhysicsTokens->mjcOptionApirate), + 1.2); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimImpratio) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual( + stage, + kPhysicsScenePrimPath.AppendProperty(MjcPhysicsTokens->mjcOptionImpratio), + 0.8); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimMagnetic) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual( + stage, + kPhysicsScenePrimPath.AppendProperty(MjcPhysicsTokens->mjcOptionMagnetic), + pxr::GfVec3d(1, 2, 3)); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimDensity) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual( + stage, + kPhysicsScenePrimPath.AppendProperty(MjcPhysicsTokens->mjcOptionDensity), + 1.2); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimViscosity) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual(stage, + kPhysicsScenePrimPath.AppendProperty( + MjcPhysicsTokens->mjcOptionViscosity), + 0.8); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimO_margin) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual( + stage, + kPhysicsScenePrimPath.AppendProperty(MjcPhysicsTokens->mjcOptionO_margin), + 0.001); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimO_solref) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual( + stage, + kPhysicsScenePrimPath.AppendProperty(MjcPhysicsTokens->mjcOptionO_solref), + pxr::VtArray({0.1, 0.2})); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimO_solimp) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual( + stage, + kPhysicsScenePrimPath.AppendProperty(MjcPhysicsTokens->mjcOptionO_solimp), + pxr::VtArray({0.1, 0.2, 0.3, 0.4, 0.5})); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimTolerance) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual(stage, + kPhysicsScenePrimPath.AppendProperty( + MjcPhysicsTokens->mjcOptionTolerance), + 0.0012); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimLSTolerance) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual(stage, + kPhysicsScenePrimPath.AppendProperty( + MjcPhysicsTokens->mjcOptionLs_tolerance), + 0.0034); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimNoslipTolerance) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual(stage, + kPhysicsScenePrimPath.AppendProperty( + MjcPhysicsTokens->mjcOptionNoslip_tolerance), + 0.0056); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimCCDTolerance) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual(stage, + kPhysicsScenePrimPath.AppendProperty( + MjcPhysicsTokens->mjcOptionCcd_tolerance), + 0.0078); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimOFriction) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual(stage, + kPhysicsScenePrimPath.AppendProperty( + MjcPhysicsTokens->mjcOptionO_friction), + pxr::VtArray({0.1, 0.2, 0.3, 0.4, 0.5})); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimIntegrator) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual(stage, + kPhysicsScenePrimPath.AppendProperty( + MjcPhysicsTokens->mjcOptionIntegrator), + MjcPhysicsTokens->rk4); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimJacobian) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual( + stage, + kPhysicsScenePrimPath.AppendProperty(MjcPhysicsTokens->mjcOptionJacobian), + MjcPhysicsTokens->sparse); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimSolver) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual( + stage, + kPhysicsScenePrimPath.AppendProperty(MjcPhysicsTokens->mjcOptionSolver), + MjcPhysicsTokens->cg); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimIterations) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual(stage, + kPhysicsScenePrimPath.AppendProperty( + MjcPhysicsTokens->mjcOptionIterations), + 10); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimLSIterations) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual(stage, + kPhysicsScenePrimPath.AppendProperty( + MjcPhysicsTokens->mjcOptionLs_iterations), + 20); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimNoslipIterations) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual(stage, + kPhysicsScenePrimPath.AppendProperty( + MjcPhysicsTokens->mjcOptionNoslip_iterations), + 30); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimCCDIterations) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual(stage, + kPhysicsScenePrimPath.AppendProperty( + MjcPhysicsTokens->mjcOptionCcd_iterations), + 40); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimSDFInitPoints) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual(stage, + kPhysicsScenePrimPath.AppendProperty( + MjcPhysicsTokens->mjcOptionSdf_initpoints), + 50); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimSDFIterations) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + ExpectAttributeEqual(stage, + kPhysicsScenePrimPath.AppendProperty( + MjcPhysicsTokens->mjcOptionSdf_iterations), + 60); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimDisableFlags) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + const std::vector kFlags = { + MjcPhysicsTokens->mjcFlagConstraint, + MjcPhysicsTokens->mjcFlagEquality, + MjcPhysicsTokens->mjcFlagFrictionloss, + MjcPhysicsTokens->mjcFlagLimit, + MjcPhysicsTokens->mjcFlagContact, + MjcPhysicsTokens->mjcFlagPassive, + MjcPhysicsTokens->mjcFlagGravity, + MjcPhysicsTokens->mjcFlagClampctrl, + MjcPhysicsTokens->mjcFlagWarmstart, + MjcPhysicsTokens->mjcFlagFilterparent, + MjcPhysicsTokens->mjcFlagActuation, + MjcPhysicsTokens->mjcFlagRefsafe, + MjcPhysicsTokens->mjcFlagSensor, + MjcPhysicsTokens->mjcFlagMidphase, + MjcPhysicsTokens->mjcFlagNativeccd, + MjcPhysicsTokens->mjcFlagEulerdamp, + MjcPhysicsTokens->mjcFlagAutoreset, + }; + for (const auto& flag : kFlags) { + ExpectAttributeEqual(stage, kPhysicsScenePrimPath.AppendProperty(flag), + false); + } +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsScenePrimEnableFlags) { + auto stage = pxr::UsdStage::Open(LoadLayer(R"( + + + + )")); + + // clang-format off + const std::vector kFlags = { + MjcPhysicsTokens->mjcFlagOverride, + MjcPhysicsTokens->mjcFlagEnergy, + MjcPhysicsTokens->mjcFlagFwdinv, + MjcPhysicsTokens->mjcFlagInvdiscrete, + MjcPhysicsTokens->mjcFlagMulticcd, + MjcPhysicsTokens->mjcFlagIsland, + }; + // clang-format on + for (const auto& flag : kFlags) { + ExpectAttributeEqual(stage, kPhysicsScenePrimPath.AppendProperty(flag), + true); + } +} + static constexpr char kSiteXml[] = R"( @@ -520,28 +936,28 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestSitePrimsAuthored) { auto stage = pxr::UsdStage::Open(layer); EXPECT_PRIM_VALID(stage, "/test/box_site"); EXPECT_PRIM_IS_A(stage, "/test/box_site", pxr::UsdGeomCube); - EXPECT_PRIM_API_APPLIED(stage, "/test/box_site", pxr::MjcPhysicsSiteAPI); + EXPECT_PRIM_API_APPLIED(stage, "/test/box_site", MjcPhysicsSiteAPI); EXPECT_PRIM_VALID(stage, "/test/ball/ball/sphere_site"); EXPECT_PRIM_IS_A(stage, "/test/ball/ball/sphere_site", pxr::UsdGeomSphere); EXPECT_PRIM_API_APPLIED(stage, "/test/ball/ball/sphere_site", - pxr::MjcPhysicsSiteAPI); + MjcPhysicsSiteAPI); EXPECT_PRIM_VALID(stage, "/test/ball/ball/capsule_site"); EXPECT_PRIM_IS_A(stage, "/test/ball/ball/capsule_site", pxr::UsdGeomCapsule); EXPECT_PRIM_API_APPLIED(stage, "/test/ball/ball/capsule_site", - pxr::MjcPhysicsSiteAPI); + MjcPhysicsSiteAPI); EXPECT_PRIM_VALID(stage, "/test/ball/ball/cylinder_site"); EXPECT_PRIM_IS_A(stage, "/test/ball/ball/cylinder_site", pxr::UsdGeomCylinder); EXPECT_PRIM_API_APPLIED(stage, "/test/ball/ball/cylinder_site", - pxr::MjcPhysicsSiteAPI); + MjcPhysicsSiteAPI); EXPECT_PRIM_VALID(stage, "/test/ball/ball/ellipsoid_site"); EXPECT_PRIM_IS_A(stage, "/test/ball/ball/ellipsoid_site", pxr::UsdGeomSphere); EXPECT_PRIM_API_APPLIED(stage, "/test/ball/ball/ellipsoid_site", - pxr::MjcPhysicsSiteAPI); + MjcPhysicsSiteAPI); } TEST_F(MjcfSdfFileFormatPluginTest, TestSitePrimsPurpose) { diff --git a/test/experimental/usd/test_utils.cc b/test/experimental/usd/test_utils.cc index 8bfc30e0..a4e93240 100644 --- a/test/experimental/usd/test_utils.cc +++ b/test/experimental/usd/test_utils.cc @@ -44,9 +44,9 @@ pxr::SdfLayerRefPtr LoadLayer( template <> void ExpectAttributeEqual(pxr::UsdStageRefPtr stage, - const char* path, + pxr::SdfPath path, const pxr::SdfAssetPath& value) { - auto attr = stage->GetAttributeAtPath(pxr::SdfPath(path)); + auto attr = stage->GetAttributeAtPath(path); EXPECT_TRUE(attr.IsValid()); pxr::SdfAssetPath attr_value; attr.Get(&attr_value); diff --git a/test/experimental/usd/test_utils.h b/test/experimental/usd/test_utils.h index ec5e3de7..69075ab3 100644 --- a/test/experimental/usd/test_utils.h +++ b/test/experimental/usd/test_utils.h @@ -74,7 +74,7 @@ pxr::SdfLayerRefPtr LoadLayer( const pxr::SdfFileFormat::FileFormatArguments& args = {}); template -void ExpectAttributeEqual(pxr::UsdStageRefPtr stage, const char* path, +void ExpectAttributeEqual(pxr::UsdStageRefPtr stage, pxr::SdfPath path, const T& value) { auto attr = stage->GetAttributeAtPath(pxr::SdfPath(path)); EXPECT_TRUE(attr.IsValid()) << "Attribute " << path << " is not valid"; @@ -84,13 +84,19 @@ void ExpectAttributeEqual(pxr::UsdStageRefPtr stage, const char* path, << attr_value << ". Expected: " << value; } +template +void ExpectAttributeEqual(pxr::UsdStageRefPtr stage, const char* path, + const T& value) { + ExpectAttributeEqual(stage, pxr::SdfPath(path), value); +} + // Specialization for SdfAssetPath, so that we can compare only the asset path // and not care about whatever the resolved path is. // Otherwise the default operator== would fail because it tests for equality of // the asset path AND the resolved path. template <> void ExpectAttributeEqual(pxr::UsdStageRefPtr stage, - const char* path, + pxr::SdfPath, const pxr::SdfAssetPath& value); void ExpectAttributeHasConnection(pxr::UsdStageRefPtr stage, const char* path, From 3e9bc79b54fab5644f7649aea38a083f101e34b0 Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Thu, 22 May 2025 04:32:04 -0700 Subject: [PATCH 163/191] Adds color space to textures. PiperOrigin-RevId: 761907261 Change-Id: Iec546d1d5907a15e57ef809ec31850640fc52915 --- doc/XMLreference.rst | 6 ++++ doc/XMLschema.rst | 12 ++++---- doc/includes/references.h | 7 +++++ include/mujoco/mjmodel.h | 8 +++++ include/mujoco/mjspec.h | 1 + include/mujoco/mjxmacro.h | 1 + python/mujoco/introspect/enums.py | 10 ++++++ python/mujoco/introspect/structs.py | 13 ++++++++ src/render/render_context.c | 9 ++++-- src/user/user_init.c | 1 + src/user/user_model.cc | 1 + src/user/user_objects.cc | 46 ++++++++++++++++++++++------ src/user/user_objects.h | 6 ++-- src/xml/xml_base.h | 2 ++ src/xml/xml_native_reader.cc | 16 ++++++++-- src/xml/xml_native_writer.cc | 1 + unity/Runtime/Bindings/MjBindings.cs | 6 ++++ 17 files changed, 123 insertions(+), 23 deletions(-) diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index a57f1a1f..b2ce9370 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -1526,6 +1526,12 @@ still be specified here but this functionality is now deprecated and will be rem with the texrepeat attribute of :ref:`material `. The data can be loaded from a single file or created procedurally. +.. _asset-texture-colorspace: + +:at:`colorspace`: :at-val:`[auto, linear, sRGB], "auto"` + This attribute determines the color space of the texture. The default value ``auto`` means that the color space will + be determined from the image file itself. If no color space is defined in the file, then ``linear`` is assumed. + .. _asset-texture-content_type: :at:`content_type`: :at-val:`string, optional` diff --git a/doc/XMLschema.rst b/doc/XMLschema.rst index 1306115e..bd71743b 100644 --- a/doc/XMLschema.rst +++ b/doc/XMLschema.rst @@ -153,17 +153,17 @@ | :ref:`texture | \* | :class: mjcf-attributes | | ` | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`name` | :ref:`type` | :ref:`content_type` | :ref:`file` | | +| | | | :ref:`name` | :ref:`type` | :ref:`colorspace` | :ref:`content_type` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`gridsize` | :ref:`gridlayout` | :ref:`fileright` | :ref:`fileleft` | | +| | | | :ref:`file` | :ref:`gridsize` | :ref:`gridlayout` | :ref:`fileright` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`fileup` | :ref:`filedown` | :ref:`filefront` | :ref:`fileback` | | +| | | | :ref:`fileleft` | :ref:`fileup` | :ref:`filedown` | :ref:`filefront` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`builtin` | :ref:`rgb1` | :ref:`rgb2` | :ref:`mark` | | +| | | | :ref:`fileback` | :ref:`builtin` | :ref:`rgb1` | :ref:`rgb2` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`markrgb` | :ref:`random` | :ref:`width` | :ref:`height` | | +| | | | :ref:`mark` | :ref:`markrgb` | :ref:`random` | :ref:`width` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`hflip` | :ref:`vflip` | :ref:`nchannel` | | | +| | | | :ref:`height` | :ref:`hflip` | :ref:`vflip` | :ref:`nchannel` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| asset |br| |_| |L| | | .. table:: | diff --git a/doc/includes/references.h b/doc/includes/references.h index 9a973092..b9adc621 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -544,6 +544,11 @@ typedef enum mjtTextureRole_ { // role of texture map in rendering mjTEXROLE_ORM, // occlusion, roughness, metallic mjNTEXROLE } mjtTextureRole; +typedef enum mjtColorSpace_ { // type of color space encoding + mjCOLORSPACE_AUTO = 0, // attempts to autodetect color space, defaults to linear + mjCOLORSPACE_LINEAR, // linear color space + mjCOLORSPACE_SRGB // standard RGB color space +} mjtColorSpace; typedef enum mjtIntegrator_ { // integrator mode mjINT_EULER = 0, // semi-implicit Euler mjINT_RK4, // 4th-order Runge Kutta @@ -1326,6 +1331,7 @@ struct mjModel_ { // textures int* tex_type; // texture type (mjtTexture) (ntex x 1) + int* tex_colorspace; // texture colorspace (mjtColorSpace) (ntex x 1) int* tex_height; // number of rows in texture image (ntex x 1) int* tex_width; // number of columns in texture image (ntex x 1) int* tex_nchannel; // number of channels in texture image (ntex x 1) @@ -2150,6 +2156,7 @@ typedef struct mjsTexture_ { // texture specification mjsElement* element; // element type mjString* name; // name mjtTexture type; // texture type + mjtColorSpace colorspace; // colorspace // method 1: builtin int builtin; // builtin type (mjtBuiltin) diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index 0c531664..05b3d832 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -151,6 +151,13 @@ typedef enum mjtTextureRole_ { // role of texture map in rendering } mjtTextureRole; +typedef enum mjtColorSpace_ { // type of color space encoding + mjCOLORSPACE_AUTO = 0, // attempts to autodetect color space, defaults to linear + mjCOLORSPACE_LINEAR, // linear color space + mjCOLORSPACE_SRGB // standard RGB color space +} mjtColorSpace; + + typedef enum mjtIntegrator_ { // integrator mode mjINT_EULER = 0, // semi-implicit Euler mjINT_RK4, // 4th-order Runge Kutta @@ -994,6 +1001,7 @@ struct mjModel_ { // textures int* tex_type; // texture type (mjtTexture) (ntex x 1) + int* tex_colorspace; // texture colorspace (mjtColorSpace) (ntex x 1) int* tex_height; // number of rows in texture image (ntex x 1) int* tex_width; // number of columns in texture image (ntex x 1) int* tex_nchannel; // number of channels in texture image (ntex x 1) diff --git a/include/mujoco/mjspec.h b/include/mujoco/mjspec.h index d1b89d8e..20568f2f 100644 --- a/include/mujoco/mjspec.h +++ b/include/mujoco/mjspec.h @@ -525,6 +525,7 @@ typedef struct mjsTexture_ { // texture specification mjsElement* element; // element type mjString* name; // name mjtTexture type; // texture type + mjtColorSpace colorspace; // colorspace // method 1: builtin int builtin; // builtin type (mjtBuiltin) diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index 5ffe5764..57673285 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -439,6 +439,7 @@ XNV ( float, hfield_data, nhfielddata, 1 ) \ X ( int, hfield_pathadr, nhfield, 1 ) \ X ( int, tex_type, ntex, 1 ) \ + X ( int, tex_colorspace, ntex, 1 ) \ X ( int, tex_height, ntex, 1 ) \ X ( int, tex_width, ntex, 1 ) \ X ( int, tex_nchannel, ntex, 1 ) \ diff --git a/python/mujoco/introspect/enums.py b/python/mujoco/introspect/enums.py index a1cab44f..dc828a57 100644 --- a/python/mujoco/introspect/enums.py +++ b/python/mujoco/introspect/enums.py @@ -139,6 +139,16 @@ ENUMS: Mapping[str, EnumDecl] = dict([ ('mjNTEXROLE', 10), ]), )), + ('mjtColorSpace', + EnumDecl( + name='mjtColorSpace', + declname='enum mjtColorSpace_', + values=dict([ + ('mjCOLORSPACE_AUTO', 0), + ('mjCOLORSPACE_LINEAR', 1), + ('mjCOLORSPACE_SRGB', 2), + ]), + )), ('mjtIntegrator', EnumDecl( name='mjtIntegrator', diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index 379cdc84..5685ac1c 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -3339,6 +3339,14 @@ STRUCTS: Mapping[str, StructDecl] = dict([ doc='texture type (mjtTexture)', array_extent=('ntex',), ), + StructFieldDecl( + name='tex_colorspace', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='texture colorspace (mjtColorSpace)', + array_extent=('ntex',), + ), StructFieldDecl( name='tex_height', type=PointerType( @@ -9613,6 +9621,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='mjtTexture'), doc='texture type', ), + StructFieldDecl( + name='colorspace', + type=ValueType(name='mjtColorSpace'), + doc='colorspace', + ), StructFieldDecl( name='builtin', type=ValueType(name='int'), diff --git a/src/render/render_context.c b/src/render/render_context.c index b305f2dd..b1f279bb 100644 --- a/src/render/render_context.c +++ b/src/render/render_context.c @@ -1387,15 +1387,20 @@ void mjr_uploadTexture(const mjModel* m, const mjrContext* con, int texid) { // assign data int type = 0; + int internaltype = 0; if (m->tex_nchannel[texid] == 3) { type = GL_RGB; + internaltype = (m->tex_colorspace[texid] == mjCOLORSPACE_SRGB) ? GL_SRGB8_EXT : GL_RGB; } else if (m->tex_nchannel[texid] == 4) { type = GL_RGBA; + internaltype = (m->tex_colorspace[texid] == mjCOLORSPACE_SRGB) ? GL_SRGB8_ALPHA8_EXT : GL_RGBA; } else { mju_error("Number of channels not supported: %d", m->tex_nchannel[texid]); } - glTexImage2D(GL_TEXTURE_2D, 0, type, m->tex_width[texid], m->tex_height[texid], 0, - type, GL_UNSIGNED_BYTE, m->tex_data + m->tex_adr[texid]); + + glTexImage2D(GL_TEXTURE_2D, 0, internaltype, m->tex_width[texid], + m->tex_height[texid], 0, type, GL_UNSIGNED_BYTE, + m->tex_data + m->tex_adr[texid]); // generate mipmaps glGenerateMipmap(GL_TEXTURE_2D); diff --git a/src/user/user_init.c b/src/user/user_init.c index 01f4a8b9..fd8a7132 100644 --- a/src/user/user_init.c +++ b/src/user/user_init.c @@ -273,6 +273,7 @@ void mjs_defaultSkin(mjsSkin* skin) { void mjs_defaultTexture(mjsTexture* texture) { memset(texture, 0, sizeof(mjsTexture)); texture->type = mjTEXTURE_CUBE; + texture->colorspace = mjCOLORSPACE_AUTO; texture->rgb1[0] = texture->rgb1[1] = texture->rgb1[2] = 0.8; texture->rgb2[0] = texture->rgb2[1] = texture->rgb2[2] = 0.5; texture->random = 0.01; diff --git a/src/user/user_model.cc b/src/user/user_model.cc index 49f79ead..bd3eb0ac 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -3285,6 +3285,7 @@ void mjCModel::CopyObjects(mjModel* m) { // set fields m->tex_type[i] = ptex->type; + m->tex_colorspace[i] = ptex->colorspace; m->tex_height[i] = ptex->height; m->tex_width[i] = ptex->width; m->tex_nchannel[i] = ptex->nchannel; diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 51c0f63a..2e2c63b2 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -55,6 +55,8 @@ class PNGImage { LodePNGColorType color_type); int Width() const { return width_; } int Height() const { return height_; } + bool IsSRGB() const { return is_srgb_; } + uint8_t operator[] (int i) const { return data_[i]; } std::vector& MoveData() { return data_; } @@ -65,6 +67,7 @@ class PNGImage { int width_; int height_; + bool is_srgb_; LodePNGColorType color_type_; std::vector data_; }; @@ -104,8 +107,11 @@ PNGImage PNGImage::Load(const mjCBase* obj, mjResource* resource, // decode PNG from buffer unsigned int w, h; - unsigned err = lodepng::decode(image.data_, w, h, - buffer, nbuffer, image.color_type_, 8); + + lodepng::State state; + state.info_raw.colortype = image.color_type_; + state.info_raw.bitdepth = 8; + unsigned err = lodepng::decode(image.data_, w, h, state, buffer, nbuffer); // check for errors if (err) { @@ -116,6 +122,7 @@ PNGImage PNGImage::Load(const mjCBase* obj, mjResource* resource, image.width_ = w; image.height_ = h; + image.is_srgb_ = (state.info_png.srgb_defined == 1); if (image.width_ <= 0 || image.height_ < 0) { std::stringstream ss; @@ -4257,7 +4264,7 @@ void mjCTexture::BuiltinCube(void) { // load PNG file void mjCTexture::LoadPNG(mjResource* resource, std::vector& image, - unsigned int& w, unsigned int& h) { + unsigned int& w, unsigned int& h, bool& is_srgb) { LodePNGColorType color_type; if (nchannel == 4) { color_type = LCT_RGBA; @@ -4272,13 +4279,14 @@ void mjCTexture::LoadPNG(mjResource* resource, PNGImage png_image = PNGImage::Load(this, resource, color_type); w = png_image.Width(); h = png_image.Height(); + is_srgb = png_image.IsSRGB(); image = png_image.MoveData(); } // load custom file void mjCTexture::LoadCustom(mjResource* resource, std::vector& image, - unsigned int& w, unsigned int& h) { + unsigned int& w, unsigned int& h, bool& is_srgb) { const void* buffer = 0; int buffer_sz = mju_readResource(resource, &buffer); @@ -4295,6 +4303,9 @@ void mjCTexture::LoadCustom(mjResource* resource, w = pint[0]; h = pint[1]; + // assume linear color space + is_srgb = false; + // check dimensions if (w < 1 || h < 1) { throw mjCError(this, "Non-PNG texture, assuming custom binary file format,\n" @@ -4317,7 +4328,7 @@ void mjCTexture::LoadCustom(mjResource* resource, // load from PNG or custom file, flip if specified void mjCTexture::LoadFlip(std::string filename, const mjVFS* vfs, std::vector& image, - unsigned int& w, unsigned int& h) { + unsigned int& w, unsigned int& h, bool& is_srgb) { std::string asset_type = GetAssetContentType(filename, content_type_); // fallback to custom @@ -4333,9 +4344,9 @@ void mjCTexture::LoadFlip(std::string filename, const mjVFS* vfs, try { if (asset_type == "image/png") { - LoadPNG(resource, image, w, h); + LoadPNG(resource, image, w, h, is_srgb); } else { - LoadCustom(resource, image, w, h); + LoadCustom(resource, image, w, h, is_srgb); } mju_closeResource(resource); } catch(mjCError err) { @@ -4402,12 +4413,16 @@ void mjCTexture::LoadFlip(std::string filename, const mjVFS* vfs, void mjCTexture::Load2D(std::string filename, const mjVFS* vfs) { // load PNG or custom unsigned int w, h; + bool is_srgb; std::vector image; - LoadFlip(filename, vfs, image, w, h); + LoadFlip(filename, vfs, image, w, h, is_srgb); // assign size width = w; height = h; + if (colorspace == mjCOLORSPACE_AUTO) { + colorspace = is_srgb ? mjCOLORSPACE_SRGB : mjCOLORSPACE_LINEAR; + } // allocate and copy data std::int64_t size = static_cast(width)*height; @@ -4435,8 +4450,13 @@ void mjCTexture::LoadCubeSingle(std::string filename, const mjVFS* vfs) { // load PNG or custom unsigned int w, h; + bool is_srgb; std::vector image; - LoadFlip(filename, vfs, image, w, h); + LoadFlip(filename, vfs, image, w, h, is_srgb); + + if (colorspace == mjCOLORSPACE_AUTO) { + colorspace = is_srgb ? mjCOLORSPACE_SRGB : mjCOLORSPACE_LINEAR; + } // check gridsize for compatibility if (w/gridsize[1] != h/gridsize[0] || (w%gridsize[1]) || (h%gridsize[0])) { @@ -4549,8 +4569,14 @@ void mjCTexture::LoadCubeSeparate(const mjVFS* vfs) { // load PNG or custom unsigned int w, h; + bool is_srgb; std::vector image; - LoadFlip(filename.Str(), vfs, image, w, h); + LoadFlip(filename.Str(), vfs, image, w, h, is_srgb); + + // assume all faces have the same colorspace + if (colorspace == mjCOLORSPACE_AUTO) { + colorspace = is_srgb ? mjCOLORSPACE_SRGB : mjCOLORSPACE_LINEAR; + } // PNG must be square if (w != h) { diff --git a/src/user/user_objects.h b/src/user/user_objects.h index 46df52e6..994cb542 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -1328,14 +1328,14 @@ class mjCTexture : public mjCTexture_, private mjsTexture { void LoadFlip(std::string filename, const mjVFS* vfs, // load and flip std::vector& image, - unsigned int& w, unsigned int& h); + unsigned int& w, unsigned int& h, bool& is_srgb); void LoadPNG(mjResource* resource, std::vector& image, - unsigned int& w, unsigned int& h); + unsigned int& w, unsigned int& h, bool& is_srgb); void LoadCustom(mjResource* resource, std::vector& image, - unsigned int& w, unsigned int& h); + unsigned int& w, unsigned int& h, bool& is_srgb); bool clear_data_; // if true, data_ is empty and should be filled by Compile }; diff --git a/src/xml/xml_base.h b/src/xml/xml_base.h index 3651d31c..4134f46f 100644 --- a/src/xml/xml_base.h +++ b/src/xml/xml_base.h @@ -35,6 +35,7 @@ extern const int jac_sz; extern const int solver_sz; extern const int equality_sz; extern const int texture_sz; +extern const int colorspace_sz; extern const int builtin_sz; extern const int mark_sz; extern const int dyn_sz; @@ -60,6 +61,7 @@ extern const mjMap jac_map[]; extern const mjMap solver_map[]; extern const mjMap equality_map[]; extern const mjMap texture_map[]; +extern const mjMap colorspace_map[]; extern const mjMap texrole_map[]; extern const mjMap builtin_map[]; extern const mjMap mark_map[]; diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index f2b5f1e9..52d3181a 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -241,8 +241,8 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { {"<"}, {"bone", "*", "5", "body", "bindpos", "bindquat", "vertid", "vertweight"}, {">"}, - {"texture", "*", "23", "name", "type", "content_type", "file", "gridsize", "gridlayout", - "fileright", "fileleft", "fileup", "filedown", "filefront", "fileback", + {"texture", "*", "24", "name", "type", "colorspace", "content_type", "file", "gridsize", + "gridlayout", "fileright", "fileleft", "fileup", "filedown", "filefront", "fileback", "builtin", "rgb1", "rgb2", "mark", "markrgb", "random", "width", "height", "hflip", "vflip", "nchannel"}, {"material", "*", "12", "name", "class", "texture", "texrepeat", "texuniform", @@ -650,6 +650,15 @@ const mjMap texture_map[texture_sz] = { }; +// colorspace for texture +const int colorspace_sz = 3; +const mjMap colorspace_map[colorspace_sz] = { + {"auto", mjCOLORSPACE_AUTO}, + {"linear", mjCOLORSPACE_LINEAR}, + {"sRGB", mjCOLORSPACE_SRGB} +}; + + // builtin type for texture const int builtin_sz = 4; const mjMap builtin_map[builtin_sz] = { @@ -3194,6 +3203,9 @@ void mjXReader::Asset(XMLElement* section, const mjVFS* vfs) { if (MapValue(elem, "type", &n, texture_map, texture_sz)) { texture->type = (mjtTexture)n; } + if (MapValue(elem, "colorspace", &n, colorspace_map, colorspace_sz)) { + texture->colorspace = (mjtColorSpace)n; + } if (ReadAttrTxt(elem, "name", texname)) { mjs_setString(texture->name, texname.c_str()); } diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index b6464cd1..f850c82b 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -1488,6 +1488,7 @@ void mjXWriter::Asset(XMLElement* root) { // write common attributes WriteAttrKey(elem, "type", texture_map, texture_sz, texture->type); + WriteAttrKey(elem, "colorspace", colorspace_map, colorspace_sz, texture->colorspace); WriteAttrTxt(elem, "name", texture->name); // write builtin diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index dc7b8078..1172e758 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -225,6 +225,11 @@ public enum mjtTextureRole : int{ mjTEXROLE_ORM = 9, mjNTEXROLE = 10, } +public enum mjtColorSpace : int{ + mjCOLORSPACE_AUTO = 0, + mjCOLORSPACE_LINEAR = 1, + mjCOLORSPACE_SRGB = 2, +} public enum mjtIntegrator : int{ mjINT_EULER = 0, mjINT_RK4 = 1, @@ -5569,6 +5574,7 @@ public unsafe struct mjModel_ { public float* hfield_data; public int* hfield_pathadr; public int* tex_type; + public int* tex_colorspace; public int* tex_height; public int* tex_width; public int* tex_nchannel; From 373b4c04378b1b6f502711ef1e5bf0e5af376d7d Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Thu, 22 May 2025 05:46:49 -0700 Subject: [PATCH 164/191] Support image/ktx format for textures. PiperOrigin-RevId: 761926074 Change-Id: I6a312634b5bdc706ea5166bc3b2be273e14c1c8a --- doc/XMLreference.rst | 37 ++++++++++++++++++++----------------- src/engine/engine_io.c | 2 +- src/user/user_objects.cc | 29 ++++++++++++++++++++++++++++- src/user/user_objects.h | 3 +++ 4 files changed, 52 insertions(+), 19 deletions(-) diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index b2ce9370..104a92ef 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -1464,16 +1464,19 @@ still be specified here but this functionality is now deprecated and will be rem :el-prefix:`asset/` |-| **texture** (*) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -| This element creates a texture asset, which is then referenced from a :ref:`material ` asset, which is - finally referenced from a model element that needs to be textured. MuJoCo provides access to the texture mapping - mechanism in OpenGL. Texture coordinates are generated automatically in GL_OBJECT_PLANE mode, using either 2D or cube - mapping. MIP maps are always enabled in GL_LINEAR_MIPMAP_LINEAR mode. The texture color is combined with the object - color in GL_MODULATE mode. The texture data can be loaded from PNG files, with provisions for loading cube and skybox - textures. Alternatively the data can be generated by the compiler as a procedural texture. Because different texture - types require different parameters, only a subset of the attributes below are used for any given texture. -| A second file format is supported for loading textures, in addition to PNG. If the file name extension is - different from .png or .PNG, or if the ``content_type`` attribute is set to ``image/vnd.mujoco.texture``, then MuJoCo - assumes that the texture is in this format. This is a custom binary file format, containing the following data: + This element creates a texture asset, which is then referenced from a :ref:`material ` asset, which + is finally referenced from a model element that needs to be textured. + + The texture data can be loaded from files or can be generated by the compiler as a procedural texture. Because + different texture types require different parameters, only a subset of the attributes below are used for any given + texture. Provisions are provided for loading cube and skybox textures from individual image files. + + Currently, three file formats are supported for loading textures: PNG, KTX, and a custom MuJoCo texture format. The + loader will use the extension of the file name to determine which format to use, defaulting to the custom format if + the extension is not recognized. Alternatively, the content_type attribute can be used to specify the format + explicitly. Only ``image/png``, ``image/ktx``, or ``image/vnd.mujoco.texture`` are supported. + + The custom MuJoCo format is assumed to be a binary file containing the following data: .. code:: Text @@ -1537,8 +1540,8 @@ still be specified here but this functionality is now deprecated and will be rem :at:`content_type`: :at-val:`string, optional` If the file attribute is specified, then this sets the `Media Type `_ (formerly known as MIME types) of the - file to be loaded. Any filename extensions will be ignored. Currently ``image/png`` and ``image/vnd.mujoco.texture`` - are supported. + file to be loaded. Any filename extensions will be ignored. Currently ``image/png``, ``image/ktx``, and + ``image/vnd.mujoco.texture`` are supported. .. _asset-texture-file: @@ -1786,11 +1789,11 @@ properties are grouped together. .. _asset-material-rgba: :at:`rgba`: :at-val:`real(4), "1 1 1 1"` - Color and transparency of the material. All components should be in the range [0 1]. Note that textures are applied - in GL_MODULATE mode, meaning that the texture color and the color specified here are multiplied component-wise. Thus - the default value of "1 1 1 1" has the effect of leaving the texture unchanged. When the material is applied to a - model element which defines its own local rgba attribute, the local definition has precedence. Note that this "local" - definition could in fact come from a defaults class. The remaining material properties always apply. + Color and transparency of the material. All components should be in the range [0 1]. Note that the texture color (if + assigned) and the color specified here are multiplied component-wise. Thus the default value of "1 1 1 1" has the + effect of leaving the texture unchanged. When the material is applied to a model element which defines its own local + rgba attribute, the local definition has precedence. Note that this "local" definition could in fact come from a + defaults class. The remaining material properties always apply. .. _material-layer: diff --git a/src/engine/engine_io.c b/src/engine/engine_io.c index 49325b30..9a9ad366 100644 --- a/src/engine/engine_io.c +++ b/src/engine/engine_io.c @@ -2406,7 +2406,7 @@ const char* mj_validateReferences(const mjModel* m) { } } for (int i=0; i < m->ntex; i++) { - int tex_adr = m->tex_adr[i] + 3*m->tex_height[i]*m->tex_width[i]; + int tex_adr = m->tex_adr[i] + m->tex_nchannel[i]*m->tex_height[i]*m->tex_width[i]; if (tex_adr > m->ntexdata || m->tex_adr[i] < 0) { return "Invalid model: tex_adr out of bounds."; } diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 2e2c63b2..688c9729 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -4283,6 +4283,28 @@ void mjCTexture::LoadPNG(mjResource* resource, image = png_image.MoveData(); } +// load KTX file +void mjCTexture::LoadKTX(mjResource* resource, + std::vector& image, unsigned int& w, + unsigned int& h, bool& is_srgb) { + const void* buffer = 0; + int buffer_sz = mju_readResource(resource, &buffer); + + // still not found + if (buffer_sz < 0) { + throw mjCError(this, "could not read texture file '%s'", resource->name); + } else if (!buffer_sz) { + throw mjCError(this, "texture file is empty: '%s'", resource->name); + } + + w = buffer_sz; + h = 1; + is_srgb = false; + + image.resize(buffer_sz); + memcpy(image.data(), buffer, buffer_sz); +} + // load custom file void mjCTexture::LoadCustom(mjResource* resource, std::vector& image, @@ -4336,7 +4358,7 @@ void mjCTexture::LoadFlip(std::string filename, const mjVFS* vfs, asset_type = "image/vnd.mujoco.texture"; } - if (asset_type != "image/png" && asset_type != "image/vnd.mujoco.texture") { + if (asset_type != "image/png" && asset_type != "image/ktx" && asset_type != "image/vnd.mujoco.texture") { throw mjCError(this, "unsupported content type: '%s'", asset_type.c_str()); } @@ -4345,6 +4367,11 @@ void mjCTexture::LoadFlip(std::string filename, const mjVFS* vfs, try { if (asset_type == "image/png") { LoadPNG(resource, image, w, h, is_srgb); + } else if (asset_type == "image/ktx") { + if (hflip || vflip) { + throw mjCError(this, "cannot flip KTX textures"); + } + LoadKTX(resource, image, w, h, is_srgb); } else { LoadCustom(resource, image, w, h, is_srgb); } diff --git a/src/user/user_objects.h b/src/user/user_objects.h index 994cb542..db01349a 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -1333,6 +1333,9 @@ class mjCTexture : public mjCTexture_, private mjsTexture { void LoadPNG(mjResource* resource, std::vector& image, unsigned int& w, unsigned int& h, bool& is_srgb); + void LoadKTX(mjResource* resource, + std::vector& image, + unsigned int& w, unsigned int& h, bool& is_srgb); void LoadCustom(mjResource* resource, std::vector& image, unsigned int& w, unsigned int& h, bool& is_srgb); From 74cc904edc0537ef35c890a1e2622708f340a641 Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Thu, 22 May 2025 08:50:23 -0700 Subject: [PATCH 165/191] Convert light `directional` boolean into a `type` enum. PiperOrigin-RevId: 761983728 Change-Id: Id5bf93c103e5358d9c5cd9f1176532a978e0b3a8 --- doc/APIreference/APItypes.rst | 11 +++++++++++ doc/XMLreference.rst | 11 ++++++++++- doc/XMLschema.rst | 16 ++++++++-------- doc/changelog.rst | 3 +++ doc/includes/references.h | 12 +++++++++--- include/mujoco/mjmodel.h | 10 +++++++++- include/mujoco/mjspec.h | 2 +- include/mujoco/mjvisualize.h | 2 +- include/mujoco/mjxmacro.h | 2 +- mjx/mujoco/mjx/_src/types.py | 2 +- python/mujoco/indexer_xmacro.h | 2 +- python/mujoco/introspect/enums.py | 11 +++++++++++ python/mujoco/introspect/structs.py | 22 +++++++++++----------- python/mujoco/structs.cc | 2 +- src/engine/engine_vis_visualize.c | 6 +++--- src/render/render_gl3.c | 20 ++++++++++++++------ src/user/user_model.cc | 2 +- src/xml/xml_base.h | 4 ++-- src/xml/xml_native_reader.cc | 27 +++++++++++++++++++++++---- src/xml/xml_native_writer.cc | 2 +- unity/Runtime/Bindings/MjBindings.cs | 10 ++++++++-- 21 files changed, 130 insertions(+), 49 deletions(-) diff --git a/doc/APIreference/APItypes.rst b/doc/APIreference/APItypes.rst index 4ee6367d..f2ecf868 100644 --- a/doc/APIreference/APItypes.rst +++ b/doc/APIreference/APItypes.rst @@ -165,6 +165,17 @@ values are used in ``m->cam_mode`` and ``m->light_mode``. .. mujoco-include:: mjtCamLight +.. _mjtLightType: + +mjtLightType +~~~~~~~~~~~~ + +The type of a light source describing how its position, orientation and other properties will interact with the +objects in the scene. These values are used in ``m->light_type``. + +.. mujoco-include:: mjtLightType + + .. _mjtTexture: mjtTexture diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 104a92ef..727950af 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -2909,10 +2909,17 @@ Attributes may be applied or ignored depending on the lighting model being used. This is identical to the target attribute of :ref:`camera ` above. It specifies which body should be targeted in "targetbody" and "targetbodycom" modes. +.. _body-light-type: + +:at:`type`: :at-val:`[spot, directional, point, image], "spot"` + Determines the type of light. Note that some light types may not be supported by some renderers (e.g. only spot and + directional lights are supported by the default native renderer). + .. _body-light-directional: :at:`directional`: :at-val:`[false, true], "false"` - The light is directional if this attribute is "true", otherwise it is a spotlight. + This is a deprecated legacy attribute. Please use :ref:`light ` type instead. If set to "true", and no + type is specified, this will change the light type to be directional. .. _body-light-castshadow: @@ -8288,6 +8295,8 @@ if omitted. .. _default-light-dir: +.. _default-light-type: + .. _default-light-directional: .. _default-light-castshadow: diff --git a/doc/XMLschema.rst b/doc/XMLschema.rst index bd71743b..a95e5211 100644 --- a/doc/XMLschema.rst +++ b/doc/XMLschema.rst @@ -313,15 +313,15 @@ | :ref:`light | \* | :class: mjcf-attributes | | ` | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`name` | :ref:`class` | :ref:`directional` | :ref:`castshadow` | | +| | | | :ref:`name` | :ref:`class` | :ref:`directional` | :ref:`type` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`active` | :ref:`pos` | :ref:`dir` | :ref:`bulbradius` | | +| | | | :ref:`castshadow` | :ref:`active` | :ref:`pos` | :ref:`dir` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`intensity` | :ref:`range` | :ref:`attenuation` | :ref:`cutoff` | | +| | | | :ref:`bulbradius` | :ref:`intensity` | :ref:`range` | :ref:`attenuation` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`exponent` | :ref:`ambient` | :ref:`diffuse` | :ref:`specular` | | +| | | | :ref:`cutoff` | :ref:`exponent` | :ref:`ambient` | :ref:`diffuse` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`mode` | :ref:`target` | | | | +| | | | :ref:`specular` | :ref:`mode` | :ref:`target` | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| body |br| |_| |L| | | .. table:: | @@ -1465,11 +1465,11 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | | | | | :ref:`pos` | :ref:`dir` | :ref:`bulbradius` | :ref:`intensity` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`range` | :ref:`directional` | :ref:`castshadow` | :ref:`active` | | +| | | | :ref:`range` | :ref:`directional` | :ref:`type` | :ref:`castshadow` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`attenuation` | :ref:`cutoff` | :ref:`exponent` | :ref:`ambient` | | +| | | | :ref:`active` | :ref:`attenuation` | :ref:`cutoff` | :ref:`exponent` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`diffuse` | :ref:`specular` | :ref:`mode` | | | +| | | | :ref:`ambient` | :ref:`diffuse` | :ref:`specular` | :ref:`mode` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| default |br| |_| |L| | | .. table:: | diff --git a/doc/changelog.rst b/doc/changelog.rst index 3fc979bd..3ddc1b51 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -11,6 +11,9 @@ General solver and clears the way for the addition of the Newton and PGS solvers (currently only CG is supported). - Removed the :at:`shell` plugin. This is now supported by :ref:`flexcomp` and is active depending on the :ref:`elastic2d` attribute (off by default). +- Replaced the :ref:`directional` (boolean) field for lights with a + :ref:`type` field (of type :ref:`mjtLightType`) to allow for additional lighting + types. Simulate ^^^^^^^^ diff --git a/doc/includes/references.h b/doc/includes/references.h index b9adc621..f61acd8e 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -526,6 +526,12 @@ typedef enum mjtCamLight_ { // tracking mode for camera and light mjCAMLIGHT_TARGETBODY, // pos fixed in body, rot tracks target body mjCAMLIGHT_TARGETBODYCOM // pos fixed in body, rot tracks target subtree com } mjtCamLight; +typedef enum mjtLightType_ { // type of light + mjLIGHT_SPOT = 0, // spot + mjLIGHT_DIRECTIONAL, // directional + mjLIGHT_POINT, // point + mjLIGHT_IMAGE, // image-based +} mjtLightType; typedef enum mjtTexture_ { // type of texture mjTEXTURE_2D = 0, // 2d texture, suitable for planes and hfields mjTEXTURE_CUBE, // cube texture, suitable for all other geom types @@ -1179,7 +1185,7 @@ struct mjModel_ { int* light_mode; // light tracking mode (mjtCamLight) (nlight x 1) int* light_bodyid; // id of light's body (nlight x 1) int* light_targetbodyid; // id of targeted body; -1: none (nlight x 1) - mjtByte* light_directional; // directional light (nlight x 1) + int* light_type; // spot, directional, etc. (mjtLightType) (nlight x 1) mjtByte* light_castshadow; // does light cast shadows (nlight x 1) float* light_bulbradius; // light radius for soft shadows (nlight x 1) float* light_intensity; // intensity, in candela (nlight x 1) @@ -2036,7 +2042,7 @@ typedef struct mjsLight_ { // light specification // intrinsics mjtByte active; // is light active - mjtByte directional; // is light directional or spot + mjtLightType type; // type of light mjtByte castshadow; // does light cast shadows float bulbradius; // bulb radius, for soft shadows float intensity; // intensity, in candelas @@ -2840,6 +2846,7 @@ typedef struct mjvGeom_ mjvGeom; struct mjvLight_ { // OpenGL light float pos[3]; // position rel. to body frame float dir[3]; // direction rel. to body frame + int type; // type (mjtLightType) float attenuation[3]; // OpenGL attenuation (quadratic model) float cutoff; // OpenGL cutoff float exponent; // OpenGL exponent @@ -2847,7 +2854,6 @@ struct mjvLight_ { // OpenGL light float diffuse[3]; // diffuse rgb (alpha=1) float specular[3]; // specular rgb (alpha=1) mjtByte headlight; // headlight - mjtByte directional; // directional light mjtByte castshadow; // does light cast shadows float bulbradius; // bulb radius for soft shadows float intensity; // intensity, in candelas diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index 05b3d832..8c50af50 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -129,6 +129,14 @@ typedef enum mjtCamLight_ { // tracking mode for camera and light } mjtCamLight; +typedef enum mjtLightType_ { // type of light + mjLIGHT_SPOT = 0, // spot + mjLIGHT_DIRECTIONAL, // directional + mjLIGHT_POINT, // point + mjLIGHT_IMAGE, // image-based +} mjtLightType; + + typedef enum mjtTexture_ { // type of texture mjTEXTURE_2D = 0, // 2d texture, suitable for planes and hfields mjTEXTURE_CUBE, // cube texture, suitable for all other geom types @@ -849,7 +857,7 @@ struct mjModel_ { int* light_mode; // light tracking mode (mjtCamLight) (nlight x 1) int* light_bodyid; // id of light's body (nlight x 1) int* light_targetbodyid; // id of targeted body; -1: none (nlight x 1) - mjtByte* light_directional; // directional light (nlight x 1) + int* light_type; // spot, directional, etc. (mjtLightType) (nlight x 1) mjtByte* light_castshadow; // does light cast shadows (nlight x 1) float* light_bulbradius; // light radius for soft shadows (nlight x 1) float* light_intensity; // intensity, in candela (nlight x 1) diff --git a/include/mujoco/mjspec.h b/include/mujoco/mjspec.h index 20568f2f..c422eedd 100644 --- a/include/mujoco/mjspec.h +++ b/include/mujoco/mjspec.h @@ -394,7 +394,7 @@ typedef struct mjsLight_ { // light specification // intrinsics mjtByte active; // is light active - mjtByte directional; // is light directional or spot + mjtLightType type; // type of light mjtByte castshadow; // does light cast shadows float bulbradius; // bulb radius, for soft shadows float intensity; // intensity, in candelas diff --git a/include/mujoco/mjvisualize.h b/include/mujoco/mjvisualize.h index 560b8f35..a61fe649 100644 --- a/include/mujoco/mjvisualize.h +++ b/include/mujoco/mjvisualize.h @@ -261,6 +261,7 @@ typedef struct mjvGeom_ mjvGeom; struct mjvLight_ { // OpenGL light float pos[3]; // position rel. to body frame float dir[3]; // direction rel. to body frame + int type; // type (mjtLightType) float attenuation[3]; // OpenGL attenuation (quadratic model) float cutoff; // OpenGL cutoff float exponent; // OpenGL exponent @@ -268,7 +269,6 @@ struct mjvLight_ { // OpenGL light float diffuse[3]; // diffuse rgb (alpha=1) float specular[3]; // specular rgb (alpha=1) mjtByte headlight; // headlight - mjtByte directional; // directional light mjtByte castshadow; // does light cast shadows float bulbradius; // bulb radius for soft shadows float intensity; // intensity, in candelas diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index 57673285..3a8a6fcf 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -299,7 +299,7 @@ X ( int, light_mode, nlight, 1 ) \ X ( int, light_bodyid, nlight, 1 ) \ X ( int, light_targetbodyid, nlight, 1 ) \ - X ( mjtByte, light_directional, nlight, 1 ) \ + X ( int, light_type, nlight, 1 ) \ X ( mjtByte, light_castshadow, nlight, 1 ) \ X ( float, light_bulbradius, nlight, 1 ) \ X ( float, light_intensity, nlight, 1 ) \ diff --git a/mjx/mujoco/mjx/_src/types.py b/mjx/mujoco/mjx/_src/types.py index 2950fb4a..3beb6b54 100644 --- a/mjx/mujoco/mjx/_src/types.py +++ b/mjx/mujoco/mjx/_src/types.py @@ -725,7 +725,7 @@ class Model(PyTreeNode): cam_sensorsize: np.ndarray cam_intrinsic: np.ndarray light_mode: np.ndarray - light_directional: jax.Array + light_type: jax.Array light_castshadow: jax.Array light_pos: jax.Array light_dir: jax.Array diff --git a/python/mujoco/indexer_xmacro.h b/python/mujoco/indexer_xmacro.h index f39d2da2..9f26000d 100644 --- a/python/mujoco/indexer_xmacro.h +++ b/python/mujoco/indexer_xmacro.h @@ -152,7 +152,7 @@ X( int, light_, mode, nlight, 1 ) \ X( int, light_, bodyid, nlight, 1 ) \ X( int, light_, targetbodyid, nlight, 1 ) \ - X( mjtByte, light_, directional, nlight, 1 ) \ + X( int, light_, type, nlight, 1 ) \ X( mjtByte, light_, castshadow, nlight, 1 ) \ X( mjtByte, light_, active, nlight, 1 ) \ X( mjtNum, light_, pos, nlight, 3 ) \ diff --git a/python/mujoco/introspect/enums.py b/python/mujoco/introspect/enums.py index dc828a57..7cbe18bc 100644 --- a/python/mujoco/introspect/enums.py +++ b/python/mujoco/introspect/enums.py @@ -111,6 +111,17 @@ ENUMS: Mapping[str, EnumDecl] = dict([ ('mjCAMLIGHT_TARGETBODYCOM', 4), ]), )), + ('mjtLightType', + EnumDecl( + name='mjtLightType', + declname='enum mjtLightType_', + values=dict([ + ('mjLIGHT_SPOT', 0), + ('mjLIGHT_DIRECTIONAL', 1), + ('mjLIGHT_POINT', 2), + ('mjLIGHT_IMAGE', 3), + ]), + )), ('mjtTexture', EnumDecl( name='mjtTexture', diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index 5685ac1c..8ab28046 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -2220,11 +2220,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ array_extent=('nlight',), ), StructFieldDecl( - name='light_directional', + name='light_type', type=PointerType( - inner_type=ValueType(name='mjtByte'), + inner_type=ValueType(name='int'), ), - doc='directional light', + doc='spot, directional, etc. (mjtLightType)', array_extent=('nlight',), ), StructFieldDecl( @@ -6697,6 +6697,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), doc='direction rel. to body frame', ), + StructFieldDecl( + name='type', + type=ValueType(name='int'), + doc='type (mjtLightType)', + ), StructFieldDecl( name='attenuation', type=ArrayType( @@ -6744,11 +6749,6 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='mjtByte'), doc='headlight', ), - StructFieldDecl( - name='directional', - type=ValueType(name='mjtByte'), - doc='directional light', - ), StructFieldDecl( name='castshadow', type=ValueType(name='mjtByte'), @@ -8992,9 +8992,9 @@ STRUCTS: Mapping[str, StructDecl] = dict([ doc='is light active', ), StructFieldDecl( - name='directional', - type=ValueType(name='mjtByte'), - doc='is light directional or spot', + name='type', + type=ValueType(name='mjtLightType'), + doc='type of light', ), StructFieldDecl( name='castshadow', diff --git a/python/mujoco/structs.cc b/python/mujoco/structs.cc index 735e0a0b..dc98e3dd 100644 --- a/python/mujoco/structs.cc +++ b/python/mujoco/structs.cc @@ -1073,7 +1073,7 @@ This is useful for example when the MJB is not available as a file on disk.)")); X(cutoff); X(exponent); X(headlight); - X(directional); + X(type); X(castshadow); X(bulbradius); X(intensity); diff --git a/src/engine/engine_vis_visualize.c b/src/engine/engine_vis_visualize.c index 0425b85c..6f9364f1 100644 --- a/src/engine/engine_vis_visualize.c +++ b/src/engine/engine_vis_visualize.c @@ -2143,7 +2143,7 @@ void mjv_makeLights(const mjModel* m, const mjData* d, mjvScene* scn) { // set default properties memset(thislight, 0, sizeof(mjvLight)); thislight->headlight = 1; - thislight->directional = 1; + thislight->type = mjLIGHT_DIRECTIONAL; thislight->castshadow = 0; // compute head position and gaze direction in model space @@ -2169,12 +2169,12 @@ void mjv_makeLights(const mjModel* m, const mjData* d, mjvScene* scn) { // copy properties memset(thislight, 0, sizeof(mjvLight)); - thislight->directional = m->light_directional[i]; + thislight->type = m->light_type[i]; thislight->castshadow = m->light_castshadow[i]; thislight->bulbradius = m->light_bulbradius[i]; thislight->intensity = m->light_intensity[i]; thislight->range = m->light_range[i]; - if (!thislight->directional) { + if (thislight->type == mjLIGHT_SPOT) { f2f(thislight->attenuation, m->light_attenuation+3*i, 3); thislight->exponent = m->light_exponent[i]; thislight->cutoff = m->light_cutoff[i]; diff --git a/src/render/render_gl3.c b/src/render/render_gl3.c index 9d168585..1660c67b 100644 --- a/src/render/render_gl3.c +++ b/src/render/render_gl3.c @@ -672,7 +672,7 @@ static void initLights(mjvScene* scn) { glLightfv(GL_LIGHT0+i, GL_SPECULAR, scn->lights[i].specular); // parameters for directional light - if (scn->lights[i].directional) { + if (scn->lights[i].type == mjLIGHT_DIRECTIONAL) { glLightf(GL_LIGHT0+i, GL_SPOT_EXPONENT, 0); glLightf(GL_LIGHT0+i, GL_SPOT_CUTOFF, 180); glLightf(GL_LIGHT0+i, GL_CONSTANT_ATTENUATION, 1); @@ -681,13 +681,17 @@ static void initLights(mjvScene* scn) { } // parameters for spot light - else { + else if (scn->lights[i].type == mjLIGHT_SPOT) { glLightf(GL_LIGHT0+i, GL_SPOT_EXPONENT, scn->lights[i].exponent); glLightf(GL_LIGHT0+i, GL_SPOT_CUTOFF, scn->lights[i].cutoff); glLightf(GL_LIGHT0+i, GL_CONSTANT_ATTENUATION, scn->lights[i].attenuation[0]); glLightf(GL_LIGHT0+i, GL_LINEAR_ATTENUATION, scn->lights[i].attenuation[1]); glLightf(GL_LIGHT0+i, GL_QUADRATIC_ATTENUATION, scn->lights[i].attenuation[2]); } + + else { + mju_error("Unsupported light type: %d", scn->lights[i].type); + } } // disable all lights (enable selectively in render) @@ -790,14 +794,16 @@ static void adjustLight(const mjvLight* thislight, int n) { float temp[4]; // set position and direction according to type - if (thislight->directional) { + if (thislight->type == mjLIGHT_DIRECTIONAL) { mjr_setf4(temp, -thislight->dir[0], -thislight->dir[1], -thislight->dir[2], 0); glLightfv(GL_LIGHT0+n, GL_POSITION, temp); - } else { + } else if (thislight->type == mjLIGHT_SPOT) { mjr_setf4(temp, thislight->dir[0], thislight->dir[1], thislight->dir[2], 0); glLightfv(GL_LIGHT0+n, GL_SPOT_DIRECTION, temp); mjr_setf4(temp, thislight->pos[0], thislight->pos[1], thislight->pos[2], 1); glLightfv(GL_LIGHT0+n, GL_POSITION, temp); + } else { + mju_error("Unsupported light type: %d", thislight->type); } } @@ -1183,13 +1189,15 @@ void mjr_render(mjrRect viewport, mjvScene* scn, const mjrContext* con) { // reverse Z rendering mapping without shift [znear, zfar] -> [1, -1] (ndc) glScalef(1.0f, 1.0f, -1.0f); } - if (thislight->directional) { + if (thislight->type == mjLIGHT_DIRECTIONAL) { glOrtho(-con->shadowClip, con->shadowClip, -con->shadowClip, con->shadowClip, cam.frustum_near, cam.frustum_far); - } else { + } else if (thislight->type == mjLIGHT_SPOT) { mjr_perspective(mju_min(2*thislight->cutoff*con->shadowScale, 160), 1, cam.frustum_near, cam.frustum_far); + } else { + mju_error("Unsupported light type: %d", thislight->type); } glGetFloatv(GL_PROJECTION_MATRIX, lightProject); diff --git a/src/user/user_model.cc b/src/user/user_model.cc index bd3eb0ac..d7f862eb 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -2669,7 +2669,7 @@ void mjCModel::CopyTree(mjModel* m) { m->light_bodyid[lid] = pl->body->id; m->light_mode[lid] = (int)pl->mode; m->light_targetbodyid[lid] = pl->targetbodyid; - m->light_directional[lid] = (mjtByte)pl->directional; + m->light_type[lid] = pl->type; m->light_castshadow[lid] = (mjtByte)pl->castshadow; m->light_active[lid] = (mjtByte)pl->active; mjuu_copyvec(m->light_pos+3*lid, pl->pos, 3); diff --git a/src/xml/xml_base.h b/src/xml/xml_base.h index 4134f46f..f2aca5c8 100644 --- a/src/xml/xml_base.h +++ b/src/xml/xml_base.h @@ -27,7 +27,7 @@ // keyword maps (defined in implementation files) extern const int joint_sz; extern const int camlight_sz; -extern const int light_sz; +extern const int lighttype_sz; extern const int integrator_sz; extern const int collision_sz; extern const int cone_sz; @@ -51,7 +51,7 @@ extern const mjMap TFAuto_map[]; extern const mjMap joint_map[]; extern const mjMap geom_map[]; extern const mjMap camlight_map[]; -extern const mjMap light_map[]; +extern const mjMap lighttype_map[]; extern const mjMap integrator_map[]; extern const mjMap collision_map[]; extern const mjMap impedance_map[]; diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 52d3181a..02908ed2 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -167,8 +167,8 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { {"camera", "?", "17", "orthographic", "fovy", "ipd", "resolution", "pos", "quat", "axisangle", "xyaxes", "zaxis", "euler", "mode", "focal", "focalpixel", "principal", "principalpixel", "sensorsize", "user"}, - {"light", "?", "15", "pos", "dir", "bulbradius", "intensity", "range", - "directional", "castshadow", "active", "attenuation", "cutoff", "exponent", + {"light", "?", "16", "pos", "dir", "bulbradius", "intensity", "range", + "directional", "type", "castshadow", "active", "attenuation", "cutoff", "exponent", "ambient", "diffuse", "specular", "mode"}, {"pair", "?", "7", "condim", "friction", "solref", "solreffriction", "solimp", "gap", "margin"}, @@ -281,7 +281,7 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { {"camera", "*", "20", "name", "class", "orthographic", "fovy", "ipd", "resolution", "pos", "quat", "axisangle", "xyaxes", "zaxis", "euler", "mode", "target", "focal", "focalpixel", "principal", "principalpixel", "sensorsize", "user"}, - {"light", "*", "18", "name", "class", "directional", "castshadow", "active", + {"light", "*", "19", "name", "class", "directional", "type", "castshadow", "active", "pos", "dir", "bulbradius", "intensity", "range", "attenuation", "cutoff", "exponent", "ambient", "diffuse", "specular", "mode", "target"}, {"plugin", "*", "2", "plugin", "instance"}, @@ -580,6 +580,17 @@ const mjMap camlight_map[camlight_sz] = { {"targetbodycom", mjCAMLIGHT_TARGETBODYCOM} }; + +// light type +const int lighttype_sz = 4; +const mjMap lighttype_map[lighttype_sz] = { + {"spot", mjLIGHT_SPOT}, + {"directional", mjLIGHT_DIRECTIONAL}, + {"point", mjLIGHT_POINT}, + {"image", mjLIGHT_IMAGE} +}; + + // texmat role type const int texrole_sz = mjNTEXROLE - 1; const mjMap texrole_map[texrole_sz] = { @@ -1852,6 +1863,7 @@ void mjXReader::OneCamera(XMLElement* elem, mjsCamera* camera) { // light element parser void mjXReader::OneLight(XMLElement* elem, mjsLight* light) { int n; + bool has_directional = false; string text, name, targetbody; // read attributes @@ -1865,7 +1877,14 @@ void mjXReader::OneLight(XMLElement* elem, mjsLight* light) { light->mode = (mjtCamLight)n; } if (MapValue(elem, "directional", &n, bool_map, 2)) { - light->directional = (n == 1); + light->type = (n == 1) ? mjLIGHT_DIRECTIONAL : mjLIGHT_SPOT; + has_directional = true; + } + if (MapValue(elem, "type", &n, lighttype_map, lighttype_sz)) { + if (has_directional) { + throw mjXError(elem, "type and directional cannot both be defined"); + } + light->type = (mjtLightType)n; } if (MapValue(elem, "castshadow", &n, bool_map, 2)) { light->castshadow = (n == 1); diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index f850c82b..9f9a1b9d 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -607,7 +607,7 @@ void mjXWriter::OneLight(XMLElement* elem, const mjCLight* light, mjCDef* def, WriteAttr(elem, "bulbradius", 1, &light->bulbradius, &def->Light().bulbradius); WriteAttr(elem, "intensity", 1, &light->intensity, &def->Light().intensity); WriteAttr(elem, "range", 1, &light->range, &def->Light().range); - WriteAttrKey(elem, "directional", bool_map, 2, light->directional, def->Light().directional); + WriteAttrKey(elem, "type", lighttype_map, lighttype_sz, light->type, def->Light().type); WriteAttrKey(elem, "castshadow", bool_map, 2, light->castshadow, def->Light().castshadow); WriteAttrKey(elem, "active", bool_map, 2, light->active, def->Light().active); WriteAttr(elem, "attenuation", 3, light->attenuation, def->Light().attenuation); diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index 1172e758..ae6a9ac5 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -207,6 +207,12 @@ public enum mjtCamLight : int{ mjCAMLIGHT_TARGETBODY = 3, mjCAMLIGHT_TARGETBODYCOM = 4, } +public enum mjtLightType : int{ + mjLIGHT_SPOT = 0, + mjLIGHT_DIRECTIONAL = 1, + mjLIGHT_POINT = 2, + mjLIGHT_IMAGE = 3, +} public enum mjtTexture : int{ mjTEXTURE_2D = 0, mjTEXTURE_CUBE = 1, @@ -5434,7 +5440,7 @@ public unsafe struct mjModel_ { public int* light_mode; public int* light_bodyid; public int* light_targetbodyid; - public byte* light_directional; + public int* light_type; public byte* light_castshadow; public float* light_bulbradius; public float* light_intensity; @@ -6075,6 +6081,7 @@ public unsafe struct mjvGeom_ { public unsafe struct mjvLight_ { public fixed float pos[3]; public fixed float dir[3]; + public int type; public fixed float attenuation[3]; public float cutoff; public float exponent; @@ -6082,7 +6089,6 @@ public unsafe struct mjvLight_ { public fixed float diffuse[3]; public fixed float specular[3]; public byte headlight; - public byte directional; public byte castshadow; public float bulbradius; public float intensity; From 76ddc3007287f9650d61b3c500dfde7e3b02315a Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Thu, 22 May 2025 14:03:52 -0700 Subject: [PATCH 166/191] Make MJCF SdfFileFormat plugin create hierarchy reflecting MJCF instead of sibling rigidbodies. PiperOrigin-RevId: 762117447 Change-Id: Id13f7a4f4e383a0c9e670e5272836f2fb1099830 --- .../usd/plugins/mjcf/mujoco_to_usd.cc | 79 +++---- .../usd/plugins/mjcf/mjcf_file_format_test.cc | 209 ++++++++---------- 2 files changed, 117 insertions(+), 171 deletions(-) diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc index 289177b2..d8bf0e3e 100644 --- a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc @@ -154,7 +154,6 @@ class ModelWriter { ModelWriter(mjSpec *spec, mjModel *model, pxr::SdfAbstractDataRefPtr &data) : spec_(spec), model_(model), data_(data), class_path_("/Bad_Path") { body_paths_ = std::vector(model->nbody); - body_xforms_ = std::vector(model->nbody); } ~ModelWriter() { mj_deleteModel(model_); } @@ -165,7 +164,6 @@ class ModelWriter { // Create the world body. body_paths_[kWorldIndex] = WriteWorldBody(kWorldIndex); - body_xforms_[kWorldIndex] = pxr::GfMatrix4d().SetIdentity(); SetLayerMetadata(data_, pxr::SdfFieldKeys->Documentation, "Generated by mujoco model writer."); @@ -198,8 +196,6 @@ class ModelWriter { pxr::SdfPath class_path_; // Mapping from Mujoco body id to SdfPath. std::vector body_paths_; - // Mapping from Mujoco body id to world space transform. - std::vector body_xforms_; // Mapping from mesh names to Mesh prim path. std::unordered_map mesh_paths_; @@ -248,44 +244,6 @@ class ModelWriter { pxr::TfToken body_name; }; - pxr::SdfPath CreateParentIfNotExists(mjsBody *body, - const pxr::SdfPath &world_path, - pxr::SdfAbstractDataRefPtr &data) { - // To allow for easier scene authoring and modification, we want to - // place MJCF bodies belonging to the same kinematic chain under some - // identity parent Xform prim. This allows users to move the entire - // asset. - // - // We cannot simply recreate the MJCF kinematic tree structure - // because in USD it is assumed that children move rigidly with their - // parents. This is not true in MJCF if you have joints. We could perhaps - // use a more complex heuristic where we evaluate a common tree prefix - // in MJCF that is effectively welded together but for now we choose - // simplicity. - - // In the trivial case where the parent of body is already the world - // body we want to create a parent xform of the same name. - // So if the MJCF has a child of the world body called "root" we will - // create a parent Xform at /World/root and the actual body will be - // created at /World/root/root. - mjsBody *last_parent = body; - mjsBody *parent = mjs_getParent(body->element); - while (mjs_getId(parent->element) != kWorldIndex) { - last_parent = parent; - parent = mjs_getParent(parent->element); - } - - pxr::TfToken last_parent_name = GetValidPrimName(*last_parent->name); - pxr::SdfPath parent_xform_path = world_path.AppendChild(last_parent_name); - if (!data->HasSpec(parent_xform_path)) { - pxr::SdfPath prim_path = CreatePrimSpec( - data, world_path, last_parent_name, pxr::UsdGeomTokens->Xform); - - SetPrimKind(data_, prim_path, pxr::KindTokens->component); - } - return parent_xform_path; - } - void WriteScaleXformOp(const pxr::SdfPath &prim_path, const pxr::GfVec3f &scale) { pxr::SdfPath scale_attr_path = @@ -1263,8 +1221,10 @@ class ModelWriter { void WriteBody(mjsBody *body, bool write_physics) { int body_id = mjs_getId(body->element); - pxr::SdfPath parent_path = - CreateParentIfNotExists(body, body_paths_[kWorldIndex], data_); + // This should be safe as we process parent bodies before children. + mjsBody *parent = mjs_getParent(body->element); + int parent_id = mjs_getId(parent->element); + pxr::SdfPath parent_path = body_paths_[parent_id]; pxr::TfToken body_name = GetValidPrimName(*body->name); // Create Xform prim for body. @@ -1272,12 +1232,25 @@ class ModelWriter { pxr::UsdGeomTokens->Xform); // The parent_path will be a component which makes the actual articulated // bodies subcomponents. - SetPrimKind(data_, body_path, pxr::KindTokens->subcomponent); + auto kind = parent_id == kWorldIndex ? pxr::KindTokens->component + : pxr::KindTokens->subcomponent; + SetPrimKind(data_, body_path, kind); // Apply the PhysicsRigidBodyAPI schema if we are writing physics. if (write_physics) { ApplyApiSchema(data_, body_path, pxr::UsdPhysicsTokens->PhysicsRigidBodyAPI); + + // If the parent is not the world body, but is child of the world body + // then we need to apply the articulation root API. + if (parent_id != kWorldIndex) { + int parent_parent_id = + mjs_getId(mjs_getParent(parent->element)->element); + if (parent_parent_id == kWorldIndex) { + ApplyApiSchema(data_, parent_path, + pxr::UsdPhysicsTokens->PhysicsArticulationRootAPI); + } + } } // Create classes if necessary @@ -1290,17 +1263,15 @@ class ModelWriter { } // Create XformOp attribute for body transform. - // Make sure to account for the parent since UsdPhysics doesn't support - // nested bodies! - auto parent_xform = body_xforms_[model_->body_parentid[body_id]]; + + pxr::SdfPath xform_op_path = + CreateAttributeSpec(data_, body_path, kTokens->xformOpTransform, + pxr::SdfValueTypeNames->Matrix4d); // mjModel will have all frames already accounted for so no need to worry // about them here. - body_xforms_[body_id] = - MujocoPosQuatToTransform(&model_->body_pos[body_id * 3], - &model_->body_quat[body_id * 4]) * - parent_xform; - WriteUniformAttribute(body_path, pxr::SdfValueTypeNames->Matrix4d, - kTokens->xformOpTransform, body_xforms_[body_id]); + auto body_xform = MujocoPosQuatToTransform(&model_->body_pos[body_id * 3], + &model_->body_quat[body_id * 4]); + SetAttributeDefault(data_, xform_op_path, body_xform); // Create XformOpOrder attribute for body transform order. // For us this is simply the transform we authored above. diff --git a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc index b1359540..9e2995de 100644 --- a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc +++ b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc @@ -48,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -116,8 +117,8 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestBasicMeshSources) { auto stage = pxr::UsdStage::Open(layer); EXPECT_PRIM_VALID(stage, "/mesh_test"); - EXPECT_PRIM_VALID(stage, "/mesh_test/test_body/test_body/tetrahedron"); - EXPECT_PRIM_VALID(stage, "/mesh_test/test_body/test_body/tetrahedron/Mesh"); + EXPECT_PRIM_VALID(stage, "/mesh_test/test_body/tetrahedron"); + EXPECT_PRIM_VALID(stage, "/mesh_test/test_body/tetrahedron/Mesh"); } TEST_F(MjcfSdfFileFormatPluginTest, TestMaterials) { @@ -222,7 +223,7 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestFaceVaryingMeshSourcesSimpleMjcfMesh) { auto stage = pxr::UsdStage::Open(layer); auto mesh = pxr::UsdGeomMesh::Get( - stage, SdfPath("/mesh_test/test_body/test_body/tetrahedron/Mesh")); + stage, SdfPath("/mesh_test/test_body/tetrahedron/Mesh")); ASSERT_TRUE(mesh); pxr::VtArray face_vertex_counts; mesh.GetFaceVertexCountsAttr().Get(&face_vertex_counts); @@ -273,8 +274,8 @@ TEST_F(MjcfSdfFileFormatPluginTest, auto stage = pxr::UsdStage::Open(xml_path); EXPECT_THAT(stage, testing::NotNull()); - auto mesh = pxr::UsdGeomMesh::Get( - stage, SdfPath("/mesh_test/test_body/test_body/mesh/Mesh")); + auto mesh = + pxr::UsdGeomMesh::Get(stage, SdfPath("/mesh_test/test_body/mesh/Mesh")); ASSERT_TRUE(mesh); pxr::VtArray face_vertex_counts; mesh.GetFaceVertexCountsAttr().Get(&face_vertex_counts); @@ -357,9 +358,9 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestBody) { pxr::SdfLayerRefPtr layer = LoadLayer(kXml); auto stage = pxr::UsdStage::Open(layer); + EXPECT_PRIM_VALID(stage, "/body_test"); EXPECT_PRIM_VALID(stage, "/body_test/test_body"); - EXPECT_PRIM_VALID(stage, "/body_test/test_body/test_body"); EXPECT_PRIM_VALID(stage, "/body_test/test_body/test_body_2"); } @@ -381,10 +382,9 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestBasicParenting) { auto stage = pxr::UsdStage::Open(layer); EXPECT_PRIM_VALID(stage, "/test/root"); - EXPECT_PRIM_VALID(stage, "/test/root/root"); EXPECT_PRIM_VALID(stage, "/test/root/root_body_1"); EXPECT_PRIM_VALID(stage, "/test/root/root_body_2"); - EXPECT_PRIM_VALID(stage, "/test/root/root_body_3"); + EXPECT_PRIM_VALID(stage, "/test/root/root_body_2/root_body_3"); } TEST_F(MjcfSdfFileFormatPluginTest, TestJointsDoNotAffectParenting) { @@ -412,9 +412,8 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestJointsDoNotAffectParenting) { auto stage = pxr::UsdStage::Open(layer); EXPECT_PRIM_VALID(stage, "/test/root"); - EXPECT_PRIM_VALID(stage, "/test/root/root"); EXPECT_PRIM_VALID(stage, "/test/root/middle"); - EXPECT_PRIM_VALID(stage, "/test/root/tet"); + EXPECT_PRIM_VALID(stage, "/test/root/middle/tet"); } TEST_F(MjcfSdfFileFormatPluginTest, TestKindAuthoring) { @@ -443,9 +442,9 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestKindAuthoring) { auto stage = pxr::UsdStage::Open(layer); EXPECT_PRIM_KIND(stage, "/test", pxr::KindTokens->group); EXPECT_PRIM_KIND(stage, "/test/root", pxr::KindTokens->component); - EXPECT_PRIM_KIND(stage, "/test/root/root", pxr::KindTokens->subcomponent); EXPECT_PRIM_KIND(stage, "/test/root/middle", pxr::KindTokens->subcomponent); - EXPECT_PRIM_KIND(stage, "/test/root/tet", pxr::KindTokens->subcomponent); + EXPECT_PRIM_KIND(stage, "/test/root/middle/tet", + pxr::KindTokens->subcomponent); } TEST_F(MjcfSdfFileFormatPluginTest, TestGeomsPrims) { @@ -936,28 +935,23 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestSitePrimsAuthored) { auto stage = pxr::UsdStage::Open(layer); EXPECT_PRIM_VALID(stage, "/test/box_site"); EXPECT_PRIM_IS_A(stage, "/test/box_site", pxr::UsdGeomCube); - EXPECT_PRIM_API_APPLIED(stage, "/test/box_site", MjcPhysicsSiteAPI); - - EXPECT_PRIM_VALID(stage, "/test/ball/ball/sphere_site"); - EXPECT_PRIM_IS_A(stage, "/test/ball/ball/sphere_site", pxr::UsdGeomSphere); - EXPECT_PRIM_API_APPLIED(stage, "/test/ball/ball/sphere_site", - MjcPhysicsSiteAPI); - - EXPECT_PRIM_VALID(stage, "/test/ball/ball/capsule_site"); - EXPECT_PRIM_IS_A(stage, "/test/ball/ball/capsule_site", pxr::UsdGeomCapsule); - EXPECT_PRIM_API_APPLIED(stage, "/test/ball/ball/capsule_site", - MjcPhysicsSiteAPI); - - EXPECT_PRIM_VALID(stage, "/test/ball/ball/cylinder_site"); - EXPECT_PRIM_IS_A(stage, "/test/ball/ball/cylinder_site", - pxr::UsdGeomCylinder); - EXPECT_PRIM_API_APPLIED(stage, "/test/ball/ball/cylinder_site", - MjcPhysicsSiteAPI); - - EXPECT_PRIM_VALID(stage, "/test/ball/ball/ellipsoid_site"); - EXPECT_PRIM_IS_A(stage, "/test/ball/ball/ellipsoid_site", pxr::UsdGeomSphere); - EXPECT_PRIM_API_APPLIED(stage, "/test/ball/ball/ellipsoid_site", - MjcPhysicsSiteAPI); + EXPECT_PRIM_API_APPLIED(stage, "/test/box_site", pxr::MjcPhysicsSiteAPI); + EXPECT_PRIM_VALID(stage, "/test/ball/sphere_site"); + EXPECT_PRIM_IS_A(stage, "/test/ball/sphere_site", pxr::UsdGeomSphere); + EXPECT_PRIM_API_APPLIED(stage, "/test/ball/sphere_site", + pxr::MjcPhysicsSiteAPI); + EXPECT_PRIM_VALID(stage, "/test/ball/capsule_site"); + EXPECT_PRIM_IS_A(stage, "/test/ball/capsule_site", pxr::UsdGeomCapsule); + EXPECT_PRIM_API_APPLIED(stage, "/test/ball/capsule_site", + pxr::MjcPhysicsSiteAPI); + EXPECT_PRIM_VALID(stage, "/test/ball/cylinder_site"); + EXPECT_PRIM_IS_A(stage, "/test/ball/cylinder_site", pxr::UsdGeomCylinder); + EXPECT_PRIM_API_APPLIED(stage, "/test/ball/cylinder_site", + pxr::MjcPhysicsSiteAPI); + EXPECT_PRIM_VALID(stage, "/test/ball/ellipsoid_site"); + EXPECT_PRIM_IS_A(stage, "/test/ball/ellipsoid_site", pxr::UsdGeomSphere); + EXPECT_PRIM_API_APPLIED(stage, "/test/ball/ellipsoid_site", + pxr::MjcPhysicsSiteAPI); } TEST_F(MjcfSdfFileFormatPluginTest, TestSitePrimsPurpose) { @@ -965,13 +959,13 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestSitePrimsPurpose) { auto stage = pxr::UsdStage::Open(layer); EXPECT_PRIM_PURPOSE(stage, "/test/box_site", pxr::UsdGeomTokens->guide); - EXPECT_PRIM_PURPOSE(stage, "/test/ball/ball/sphere_site", + EXPECT_PRIM_PURPOSE(stage, "/test/ball/sphere_site", pxr::UsdGeomTokens->guide); - EXPECT_PRIM_PURPOSE(stage, "/test/ball/ball/capsule_site", + EXPECT_PRIM_PURPOSE(stage, "/test/ball/capsule_site", pxr::UsdGeomTokens->guide); - EXPECT_PRIM_PURPOSE(stage, "/test/ball/ball/cylinder_site", + EXPECT_PRIM_PURPOSE(stage, "/test/ball/cylinder_site", pxr::UsdGeomTokens->guide); - EXPECT_PRIM_PURPOSE(stage, "/test/ball/ball/ellipsoid_site", + EXPECT_PRIM_PURPOSE(stage, "/test/ball/ellipsoid_site", pxr::UsdGeomTokens->guide); } @@ -981,9 +975,8 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsToggleSdfFormatArg) { // Test that the default is no physics. auto stage_no_physics = pxr::UsdStage::Open(xml_path); EXPECT_THAT(stage_no_physics, testing::NotNull()); - EXPECT_PRIM_VALID(stage_no_physics, "/mesh_test/test_body/test_body"); - EXPECT_PRIM_API_NOT_APPLIED(stage_no_physics, - "/mesh_test/test_body/test_body", + EXPECT_PRIM_VALID(stage_no_physics, "/mesh_test/test_body"); + EXPECT_PRIM_API_NOT_APPLIED(stage_no_physics, "/mesh_test/test_body", pxr::UsdPhysicsRigidBodyAPI); // Then test that the physics flag enables physics. @@ -992,8 +985,8 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsToggleSdfFormatArg) { auto stage_with_physics = pxr::UsdStage::Open(xml_path_physics_flag); EXPECT_THAT(stage_with_physics, testing::NotNull()); - EXPECT_PRIM_VALID(stage_with_physics, "/mesh_test/test_body/test_body"); - EXPECT_PRIM_API_APPLIED(stage_with_physics, "/mesh_test/test_body/test_body", + EXPECT_PRIM_VALID(stage_with_physics, "/mesh_test/test_body"); + EXPECT_PRIM_API_APPLIED(stage_with_physics, "/mesh_test/test_body", pxr::UsdPhysicsRigidBodyAPI); } @@ -1019,23 +1012,21 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsRigidBody) { EXPECT_THAT(stage, testing::NotNull()); EXPECT_PRIM_VALID(stage, "/physics_test"); EXPECT_PRIM_VALID(stage, "/physics_test/test_body"); - EXPECT_PRIM_VALID(stage, "/physics_test/test_body/test_body"); - // USD does not allow nested rigidbodies so we put them as siblings to the - // first body in the hierarchy. EXPECT_PRIM_VALID(stage, "/physics_test/test_body/test_body_2"); - // The parent containing the body should not have the RigidBodyAPI applied. - EXPECT_PRIM_API_NOT_APPLIED(stage, "/physics_test/test_body", - pxr::UsdPhysicsRigidBodyAPI); - - EXPECT_PRIM_API_APPLIED(stage, "/physics_test/test_body/test_body", + EXPECT_PRIM_API_APPLIED(stage, "/physics_test/test_body", pxr::UsdPhysicsRigidBodyAPI); + EXPECT_PRIM_API_APPLIED(stage, "/physics_test/test_body", + pxr::UsdPhysicsArticulationRootAPI); EXPECT_PRIM_API_APPLIED(stage, "/physics_test/test_body/test_body_2", pxr::UsdPhysicsRigidBodyAPI); + // Only the root body should have the articulation API applied. + EXPECT_PRIM_API_NOT_APPLIED(stage, "/physics_test/test_body/test_body_2", + pxr::UsdPhysicsArticulationRootAPI); + // Geoms should not have RigidBodyAPI applied either. - EXPECT_PRIM_API_NOT_APPLIED(stage, - "/physics_test/test_body/test_body/test_geom", + EXPECT_PRIM_API_NOT_APPLIED(stage, "/physics_test/test_body/test_geom", pxr::UsdPhysicsRigidBodyAPI); EXPECT_PRIM_API_NOT_APPLIED(stage, "/physics_test/test_body/test_body_2/test_geom_2", @@ -1093,22 +1084,22 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsColliders) { // // ground [collider] // - // body_0/body_0 [rigidbody] - // body_0/body_0/body_0_col [collider] + // body_0 [rigidbody] + // body_0/body_0_col [collider] // - // body_0/body_0_0 [rigidbody] <-- USD reparents nested rigid bodies - // body_0/body_0/body_0_0/body_0_0_col [collider] + // body_0/body_0_0 [rigidbody] + // body_0/body_0_0/body_0_0_col [collider] // - // body_1/body_1 [rigidbody] - // body_1/body_1/body_1_col_0 [collider] - // body_1/body_1/body_1_col_1 [collider] + // body_1 [rigidbody] + // body_1/body_1_col_0 [collider] + // body_1/body_1_col_1 [collider] // - // body_2/body_2 [rigidbody] - // body_2/body_2/body_2_nocol [] + // body_2 [rigidbody] + // body_2/body_2_nocol [] // - // body_3/body_3 [rigidbody] - // body_3/body_3/body_3_col [] <-- Intermediate prim for mesh instancing - // body_3/body_3/body_3_col/Mesh [collider, mesh collider] + // body_3 [rigidbody] + // body_3/body_3_col [] <-- Intermediate prim for mesh instancing + // body_3/body_3_col/Mesh [collider, mesh collider] // ground [collider] (Static collider) EXPECT_PRIM_VALID(stage, "/test/ground"); @@ -1116,19 +1107,6 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsColliders) { pxr::UsdPhysicsRigidBodyAPI); EXPECT_PRIM_API_APPLIED(stage, "/test/ground", pxr::UsdPhysicsCollisionAPI); - // body_0/body_0 [rigidbody] - EXPECT_PRIM_VALID(stage, "/test/body_0/body_0"); - EXPECT_PRIM_API_APPLIED(stage, "/test/body_0/body_0", - pxr::UsdPhysicsRigidBodyAPI); - EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_0/body_0", - pxr::UsdPhysicsCollisionAPI); - // body_0/body_0/body_0_col [collider] - EXPECT_PRIM_VALID(stage, "/test/body_0/body_0/body_0_col"); - EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_0/body_0/body_0_col", - pxr::UsdPhysicsRigidBodyAPI); - EXPECT_PRIM_API_APPLIED(stage, "/test/body_0/body_0/body_0_col", - pxr::UsdPhysicsCollisionAPI); - // body_0/body_0_0 [rigidbody] (Nested body - reparented) EXPECT_PRIM_VALID(stage, "/test/body_0/body_0_0"); EXPECT_PRIM_API_APPLIED(stage, "/test/body_0/body_0_0", @@ -1142,61 +1120,58 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsColliders) { EXPECT_PRIM_API_APPLIED(stage, "/test/body_0/body_0_0/body_0_0_col", pxr::UsdPhysicsCollisionAPI); - // body_1/body_1 [rigidbody] - EXPECT_PRIM_VALID(stage, "/test/body_1/body_1"); - EXPECT_PRIM_API_APPLIED(stage, "/test/body_1/body_1", - pxr::UsdPhysicsRigidBodyAPI); - EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_1/body_1", + // body_1 [rigidbody] + EXPECT_PRIM_VALID(stage, "/test/body_1"); + EXPECT_PRIM_API_APPLIED(stage, "/test/body_1", pxr::UsdPhysicsRigidBodyAPI); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_1", pxr::UsdPhysicsCollisionAPI); - // body_1/body_1/body_1_col_0 [collider] - EXPECT_PRIM_VALID(stage, "/test/body_1/body_1/body_1_col_0"); - EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_1/body_1/body_1_col_0", + // body_1/body_1_col_0 [collider] + EXPECT_PRIM_VALID(stage, "/test/body_1/body_1_col_0"); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_1/body_1_col_0", pxr::UsdPhysicsRigidBodyAPI); - EXPECT_PRIM_API_APPLIED(stage, "/test/body_1/body_1/body_1_col_0", + EXPECT_PRIM_API_APPLIED(stage, "/test/body_1/body_1_col_0", pxr::UsdPhysicsCollisionAPI); - // body_1/body_1/body_1_col_1 [collider] - EXPECT_PRIM_VALID(stage, "/test/body_1/body_1/body_1_col_1"); - EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_1/body_1/body_1_col_1", + // body_1/body_1_col_1 [collider] + EXPECT_PRIM_VALID(stage, "/test/body_1/body_1_col_1"); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_1/body_1_col_1", pxr::UsdPhysicsRigidBodyAPI); - EXPECT_PRIM_API_APPLIED(stage, "/test/body_1/body_1/body_1_col_1", + EXPECT_PRIM_API_APPLIED(stage, "/test/body_1/body_1_col_1", pxr::UsdPhysicsCollisionAPI); - // body_2/body_2 [rigidbody] - EXPECT_PRIM_VALID(stage, "/test/body_2/body_2"); - EXPECT_PRIM_API_APPLIED(stage, "/test/body_2/body_2", - pxr::UsdPhysicsRigidBodyAPI); - EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_2/body_2", + // body_2 [rigidbody] + EXPECT_PRIM_VALID(stage, "/test/body_2"); + EXPECT_PRIM_API_APPLIED(stage, "/test/body_2", pxr::UsdPhysicsRigidBodyAPI); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_2", pxr::UsdPhysicsCollisionAPI); - // body_2/body_2/body_2_nocol [] (No physics APIs applied) - EXPECT_PRIM_VALID(stage, "/test/body_2/body_2/body_2_nocol"); - EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_2/body_2/body_2_nocol", + // body_2/body_2_nocol [] (No physics APIs applied) + EXPECT_PRIM_VALID(stage, "/test/body_2/body_2_nocol"); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_2/body_2_nocol", pxr::UsdPhysicsRigidBodyAPI); - EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_2/body_2/body_2_nocol", + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_2/body_2_nocol", pxr::UsdPhysicsCollisionAPI); - // body_3/body_3 [rigidbody] - EXPECT_PRIM_VALID(stage, "/test/body_3/body_3"); - EXPECT_PRIM_API_APPLIED(stage, "/test/body_3/body_3", - pxr::UsdPhysicsRigidBodyAPI); - EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_3/body_3", + // body_3 [rigidbody] + EXPECT_PRIM_VALID(stage, "/test/body_3"); + EXPECT_PRIM_API_APPLIED(stage, "/test/body_3", pxr::UsdPhysicsRigidBodyAPI); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_3", pxr::UsdPhysicsCollisionAPI); - // body_3/body_3/body_3_col [] (Intermediate prim for mesh instancing) - EXPECT_PRIM_VALID(stage, "/test/body_3/body_3/body_3_col"); - EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_3/body_3/body_3_col", + // body_3/body_3_col [] (Intermediate prim for mesh instancing) + EXPECT_PRIM_VALID(stage, "/test/body_3/body_3_col"); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_3/body_3_col", pxr::UsdPhysicsRigidBodyAPI); - EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_3/body_3/body_3_col", + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_3/body_3_col", pxr::UsdPhysicsCollisionAPI); - // body_3/body_3/body_3_col/Mesh [collider, mesh collider] - EXPECT_PRIM_VALID(stage, "/test/body_3/body_3/body_3_col/Mesh"); - EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_3/body_3/body_3_col/Mesh", + // body_3/body_3_col/Mesh [collider, mesh collider] + EXPECT_PRIM_VALID(stage, "/test/body_3/body_3_col/Mesh"); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_3/body_3_col/Mesh", pxr::UsdPhysicsRigidBodyAPI); - EXPECT_PRIM_API_APPLIED(stage, "/test/body_3/body_3/body_3_col/Mesh", + EXPECT_PRIM_API_APPLIED(stage, "/test/body_3/body_3_col/Mesh", pxr::UsdPhysicsCollisionAPI); - EXPECT_PRIM_API_APPLIED(stage, "/test/body_3/body_3/body_3_col/Mesh", + EXPECT_PRIM_API_APPLIED(stage, "/test/body_3/body_3_col/Mesh", pxr::UsdPhysicsMeshCollisionAPI); - ExpectAttributeEqual( - stage, "/test/body_3/body_3/body_3_col/Mesh.physics:approximation", - pxr::UsdPhysicsTokens->convexHull); + ExpectAttributeEqual(stage, + "/test/body_3/body_3_col/Mesh.physics:approximation", + pxr::UsdPhysicsTokens->convexHull); } } // namespace From e389872c7db149f9efb1f1501a963f6ed6f12421 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Fri, 23 May 2025 09:23:28 -0700 Subject: [PATCH 167/191] Add frame property to python bindings. PiperOrigin-RevId: 762452877 Change-Id: I555cec650d04700f6718b4d77a866c821b90685c --- python/mujoco/specs.cc | 43 +++++++++++++++++++++++++++++++++++++ python/mujoco/specs_test.py | 9 ++++++++ 2 files changed, 52 insertions(+) diff --git a/python/mujoco/specs.cc b/python/mujoco/specs.cc index c1bbc3eb..b32942a5 100644 --- a/python/mujoco/specs.cc +++ b/python/mujoco/specs.cc @@ -736,6 +736,13 @@ PYBIND11_MODULE(_specs, m) { return frame; }, py::return_value_policy::reference_internal); + mjsBody.def( + "frame", + [](raw::MjsBody* self) -> raw::MjsFrame* { + return mjs_getFrame(self->element); + }, + py::return_value_policy::reference_internal); + // ============================= MJSFRAME ==================================== mjsFrame.def("delete", [](raw::MjsFrame& self) { mjs_delete(self.element); }); @@ -767,6 +774,12 @@ PYBIND11_MODULE(_specs, m) { py::arg("body"), py::arg("prefix") = py::none(), py::arg("suffix") = py::none(), py::return_value_policy::reference_internal); + mjsFrame.def_property_readonly( + "frame", + [](raw::MjsFrame& self) -> raw::MjsFrame* { + return mjs_getFrame(self.element); + }, + py::return_value_policy::reference_internal); // ============================= MJSGEOM ===================================== mjsGeom.def("delete", [](raw::MjsGeom& self) { mjs_delete(self.element); }); @@ -789,6 +802,12 @@ PYBIND11_MODULE(_specs, m) { [](raw::MjsGeom& self, raw::MjsDefault& default_) -> void { mjs_setDefault(self.element, &default_); }); + mjsGeom.def_property_readonly( + "frame", + [](raw::MjsGeom& self) -> raw::MjsFrame* { + return mjs_getFrame(self.element); + }, + py::return_value_policy::reference_internal); // ============================= MJSJOINT ==================================== mjsJoint.def("delete", [](raw::MjsJoint& self) { mjs_delete(self.element); }); @@ -811,6 +830,12 @@ PYBIND11_MODULE(_specs, m) { [](raw::MjsJoint& self, raw::MjsDefault& default_) -> void { mjs_setDefault(self.element, &default_); }); + mjsJoint.def_property_readonly( + "frame", + [](raw::MjsJoint& self) -> raw::MjsFrame* { + return mjs_getFrame(self.element); + }, + py::return_value_policy::reference_internal); // ============================= MJSSITE ===================================== mjsSite.def("delete", [](raw::MjsSite& self) { mjs_delete(self.element); }); @@ -850,6 +875,12 @@ PYBIND11_MODULE(_specs, m) { py::arg("body"), py::arg("prefix") = py::none(), py::arg("suffix") = py::none(), py::return_value_policy::reference_internal); + mjsSite.def_property_readonly( + "frame", + [](raw::MjsSite& self) -> raw::MjsFrame* { + return mjs_getFrame(self.element); + }, + py::return_value_policy::reference_internal); // ============================= MJSCAMERA =================================== mjsCamera.def("delete", @@ -873,6 +904,12 @@ PYBIND11_MODULE(_specs, m) { [](raw::MjsCamera& self, raw::MjsDefault& default_) -> void { mjs_setDefault(self.element, &default_); }); + mjsCamera.def_property_readonly( + "frame", + [](raw::MjsCamera& self) -> raw::MjsFrame* { + return mjs_getFrame(self.element); + }, + py::return_value_policy::reference_internal); // ============================= MJSLIGHT ==================================== mjsLight.def("delete", [](raw::MjsLight& self) { mjs_delete(self.element); }); @@ -895,6 +932,12 @@ PYBIND11_MODULE(_specs, m) { [](raw::MjsLight& self, raw::MjsDefault& default_) -> void { mjs_setDefault(self.element, &default_); }); + mjsLight.def_property_readonly( + "frame", + [](raw::MjsLight& self) -> raw::MjsFrame* { + return mjs_getFrame(self.element); + }, + py::return_value_policy::reference_internal); // ============================= MJSMATERIAL ================================= mjsMaterial.def("delete", diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py index ac702876..c35c6618 100644 --- a/python/mujoco/specs_test.py +++ b/python/mujoco/specs_test.py @@ -1069,6 +1069,15 @@ class SpecsTest(absltest.TestCase): frame = body.to_frame() np.testing.assert_array_equal(frame.pos, [1, 2, 3]) + def test_get_frame(self): + spec = mujoco.MjSpec() + body = spec.worldbody.add_body() + frame = body.add_frame() + geom = body.add_geom() + geom.set_frame(frame) + self.assertIsNotNone(frame) + self.assertIs(geom.frame, frame) + def test_attach_to_frame(self): parent = mujoco.MjSpec() parent.assets = {'cube.obj': 'cube_content'} From 9e4931b79d9652ee57013b48d93f61a36f09431c Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Fri, 23 May 2025 10:25:32 -0700 Subject: [PATCH 168/191] Add CollisionAPI and MeshCollisionAPI schemas to mjcPhysics. PiperOrigin-RevId: 762478906 Change-Id: I9103e8d3cf99492e2df1c6b4af7937b12993f225 --- .../usd/mjcPhysics/collisionAPI.cpp | 124 ++++++++++++ .../usd/mjcPhysics/collisionAPI.h | 185 +++++++++++++++++ .../usd/mjcPhysics/generatedSchema.usda | 21 ++ .../usd/mjcPhysics/meshCollisionAPI.cpp | 126 ++++++++++++ .../usd/mjcPhysics/meshCollisionAPI.h | 191 ++++++++++++++++++ src/experimental/usd/mjcPhysics/plugInfo.json | 20 ++ src/experimental/usd/mjcPhysics/schema.usda | 33 +++ src/experimental/usd/mjcPhysics/tokens.cpp | 16 ++ src/experimental/usd/mjcPhysics/tokens.h | 32 +++ 9 files changed, 748 insertions(+) create mode 100644 src/experimental/usd/mjcPhysics/collisionAPI.cpp create mode 100644 src/experimental/usd/mjcPhysics/collisionAPI.h create mode 100644 src/experimental/usd/mjcPhysics/meshCollisionAPI.cpp create mode 100644 src/experimental/usd/mjcPhysics/meshCollisionAPI.h diff --git a/src/experimental/usd/mjcPhysics/collisionAPI.cpp b/src/experimental/usd/mjcPhysics/collisionAPI.cpp new file mode 100644 index 00000000..ff113849 --- /dev/null +++ b/src/experimental/usd/mjcPhysics/collisionAPI.cpp @@ -0,0 +1,124 @@ +// Copyright 2025 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. + +#include "./collisionAPI.h" + +#include "pxr/usd/sdf/assetPath.h" +#include "pxr/usd/sdf/types.h" +#include "pxr/usd/usd/schemaRegistry.h" +#include "pxr/usd/usd/typed.h" + +PXR_NAMESPACE_OPEN_SCOPE + +// Register the schema with the TfType system. +TF_REGISTRY_FUNCTION(TfType) { + TfType::Define >(); +} + +/* virtual */ +MjcPhysicsCollisionAPI::~MjcPhysicsCollisionAPI() {} + +/* static */ +MjcPhysicsCollisionAPI MjcPhysicsCollisionAPI::Get(const UsdStagePtr &stage, + const SdfPath &path) { + if (!stage) { + TF_CODING_ERROR("Invalid stage"); + return MjcPhysicsCollisionAPI(); + } + return MjcPhysicsCollisionAPI(stage->GetPrimAtPath(path)); +} + +/* virtual */ +UsdSchemaKind MjcPhysicsCollisionAPI::_GetSchemaKind() const { + return MjcPhysicsCollisionAPI::schemaKind; +} + +/* static */ +bool MjcPhysicsCollisionAPI::CanApply(const UsdPrim &prim, + std::string *whyNot) { + return prim.CanApplyAPI(whyNot); +} + +/* static */ +MjcPhysicsCollisionAPI MjcPhysicsCollisionAPI::Apply(const UsdPrim &prim) { + if (prim.ApplyAPI()) { + return MjcPhysicsCollisionAPI(prim); + } + return MjcPhysicsCollisionAPI(); +} + +/* static */ +const TfType &MjcPhysicsCollisionAPI::_GetStaticTfType() { + static TfType tfType = TfType::Find(); + return tfType; +} + +/* static */ +bool MjcPhysicsCollisionAPI::_IsTypedSchema() { + static bool isTyped = _GetStaticTfType().IsA(); + return isTyped; +} + +/* virtual */ +const TfType &MjcPhysicsCollisionAPI::_GetTfType() const { + return _GetStaticTfType(); +} + +UsdAttribute MjcPhysicsCollisionAPI::GetShellInertiaAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcShellinertia); +} + +UsdAttribute MjcPhysicsCollisionAPI::CreateShellInertiaAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcShellinertia, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +namespace { +static inline TfTokenVector _ConcatenateAttributeNames( + const TfTokenVector &left, const TfTokenVector &right) { + TfTokenVector result; + result.reserve(left.size() + right.size()); + result.insert(result.end(), left.begin(), left.end()); + result.insert(result.end(), right.begin(), right.end()); + return result; +} +} // namespace + +/*static*/ +const TfTokenVector &MjcPhysicsCollisionAPI::GetSchemaAttributeNames( + bool includeInherited) { + static TfTokenVector localNames = { + MjcPhysicsTokens->mjcShellinertia, + }; + static TfTokenVector allNames = _ConcatenateAttributeNames( + UsdAPISchemaBase::GetSchemaAttributeNames(true), localNames); + + if (includeInherited) + return allNames; + else + return localNames; +} + +PXR_NAMESPACE_CLOSE_SCOPE + +// ===================================================================== // +// Feel free to add custom code below this line. It will be preserved by +// the code generator. +// +// Just remember to wrap code in the appropriate delimiters: +// 'PXR_NAMESPACE_OPEN_SCOPE', 'PXR_NAMESPACE_CLOSE_SCOPE'. +// ===================================================================== // +// --(BEGIN CUSTOM CODE)-- diff --git a/src/experimental/usd/mjcPhysics/collisionAPI.h b/src/experimental/usd/mjcPhysics/collisionAPI.h new file mode 100644 index 00000000..e3681526 --- /dev/null +++ b/src/experimental/usd/mjcPhysics/collisionAPI.h @@ -0,0 +1,185 @@ +// Copyright 2025 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. + +#ifndef MJCPHYSICS_GENERATED_COLLISIONAPI_H +#define MJCPHYSICS_GENERATED_COLLISIONAPI_H + +/// \file mjcPhysics/collisionAPI.h + +#include "./api.h" +#include "./tokens.h" +#include "pxr/base/gf/matrix4d.h" +#include "pxr/base/gf/vec3d.h" +#include "pxr/base/gf/vec3f.h" +#include "pxr/base/tf/token.h" +#include "pxr/base/tf/type.h" +#include "pxr/base/vt/value.h" +#include "pxr/pxr.h" +#include "pxr/usd/usd/apiSchemaBase.h" +#include "pxr/usd/usd/prim.h" +#include "pxr/usd/usd/stage.h" + +PXR_NAMESPACE_OPEN_SCOPE + +class SdfAssetPath; + +// -------------------------------------------------------------------------- // +// COLLISIONAPI // +// -------------------------------------------------------------------------- // + +/// \class MjcPhysicsCollisionAPI +/// +/// API describing a Mujoco collider. +/// +class MjcPhysicsCollisionAPI : public UsdAPISchemaBase { + public: + /// Compile time constant representing what kind of schema this class is. + /// + /// \sa UsdSchemaKind + static const UsdSchemaKind schemaKind = UsdSchemaKind::SingleApplyAPI; + + /// Construct a MjcPhysicsCollisionAPI on UsdPrim \p prim . + /// Equivalent to MjcPhysicsCollisionAPI::Get(prim.GetStage(), prim.GetPath()) + /// for a \em valid \p prim, but will not immediately throw an error for + /// an invalid \p prim + explicit MjcPhysicsCollisionAPI(const UsdPrim &prim = UsdPrim()) + : UsdAPISchemaBase(prim) {} + + /// Construct a MjcPhysicsCollisionAPI on the prim held by \p schemaObj . + /// Should be preferred over MjcPhysicsCollisionAPI(schemaObj.GetPrim()), + /// as it preserves SchemaBase state. + explicit MjcPhysicsCollisionAPI(const UsdSchemaBase &schemaObj) + : UsdAPISchemaBase(schemaObj) {} + + /// Destructor. + MJCPHYSICS_API + virtual ~MjcPhysicsCollisionAPI(); + + /// Return a vector of names of all pre-declared attributes for this schema + /// class and all its ancestor classes. Does not include attributes that + /// may be authored by custom/extended methods of the schemas involved. + MJCPHYSICS_API + static const TfTokenVector &GetSchemaAttributeNames( + bool includeInherited = true); + + /// Return a MjcPhysicsCollisionAPI holding the prim adhering to this + /// schema at \p path on \p stage. If no prim exists at \p path on + /// \p stage, or if the prim at that path does not adhere to this schema, + /// return an invalid schema object. This is shorthand for the following: + /// + /// \code + /// MjcPhysicsCollisionAPI(stage->GetPrimAtPath(path)); + /// \endcode + /// + MJCPHYSICS_API + static MjcPhysicsCollisionAPI Get(const UsdStagePtr &stage, + const SdfPath &path); + + /// Returns true if this single-apply API schema can be applied to + /// the given \p prim. If this schema can not be a applied to the prim, + /// this returns false and, if provided, populates \p whyNot with the + /// reason it can not be applied. + /// + /// Note that if CanApply returns false, that does not necessarily imply + /// that calling Apply will fail. Callers are expected to call CanApply + /// before calling Apply if they want to ensure that it is valid to + /// apply a schema. + /// + /// \sa UsdPrim::GetAppliedSchemas() + /// \sa UsdPrim::HasAPI() + /// \sa UsdPrim::CanApplyAPI() + /// \sa UsdPrim::ApplyAPI() + /// \sa UsdPrim::RemoveAPI() + /// + MJCPHYSICS_API + static bool CanApply(const UsdPrim &prim, std::string *whyNot = nullptr); + + /// Applies this single-apply API schema to the given \p prim. + /// This information is stored by adding "CollisionAPI" to the + /// token-valued, listOp metadata \em apiSchemas on the prim. + /// + /// \return A valid MjcPhysicsCollisionAPI object is returned upon success. + /// An invalid (or empty) MjcPhysicsCollisionAPI object is returned upon + /// failure. See \ref UsdPrim::ApplyAPI() for conditions + /// resulting in failure. + /// + /// \sa UsdPrim::GetAppliedSchemas() + /// \sa UsdPrim::HasAPI() + /// \sa UsdPrim::CanApplyAPI() + /// \sa UsdPrim::ApplyAPI() + /// \sa UsdPrim::RemoveAPI() + /// + MJCPHYSICS_API + static MjcPhysicsCollisionAPI Apply(const UsdPrim &prim); + + protected: + /// Returns the kind of schema this class belongs to. + /// + /// \sa UsdSchemaKind + MJCPHYSICS_API + UsdSchemaKind _GetSchemaKind() const override; + + private: + // needs to invoke _GetStaticTfType. + friend class UsdSchemaRegistry; + MJCPHYSICS_API + static const TfType &_GetStaticTfType(); + + static bool _IsTypedSchema(); + + // override SchemaBase virtuals. + MJCPHYSICS_API + const TfType &_GetTfType() const override; + + public: + // --------------------------------------------------------------------- // + // SHELLINERTIA + // --------------------------------------------------------------------- // + /// Enables handling of the inertia assuming mass is concentrated on the + /// surface. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:shellinertia = 0` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetShellInertiaAttr() const; + + /// See GetShellInertiaAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateShellInertiaAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // ===================================================================== // + // Feel free to add custom code below this line, it will be preserved by + // the code generator. + // + // Just remember to: + // - Close the class declaration with }; + // - Close the namespace with PXR_NAMESPACE_CLOSE_SCOPE + // - Close the include guard with #endif + // ===================================================================== // + // --(BEGIN CUSTOM CODE)-- +}; + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif diff --git a/src/experimental/usd/mjcPhysics/generatedSchema.usda b/src/experimental/usd/mjcPhysics/generatedSchema.usda index ee17510b..6b70af62 100644 --- a/src/experimental/usd/mjcPhysics/generatedSchema.usda +++ b/src/experimental/usd/mjcPhysics/generatedSchema.usda @@ -226,3 +226,24 @@ class "SiteAPI" ( { } +class "CollisionAPI" ( + doc = "API describing a Mujoco collider." +) +{ + uniform bool mjc:shellinertia = 0 ( + displayName = "Shell Inertia" + doc = "Enables handling of the inertia assuming mass is concentrated on the surface." + ) +} + +class "MeshCollisionAPI" ( + doc = "API describing a Mujoco collider." +) +{ + uniform token mjc:inertia = "legacy" ( + allowedTokens = ["legacy", "convex", "exact", "shell"] + displayName = "Inertia" + doc = "Controls how a mesh is used when mass and inertia are inferred from geometry." + ) +} + diff --git a/src/experimental/usd/mjcPhysics/meshCollisionAPI.cpp b/src/experimental/usd/mjcPhysics/meshCollisionAPI.cpp new file mode 100644 index 00000000..50c579dd --- /dev/null +++ b/src/experimental/usd/mjcPhysics/meshCollisionAPI.cpp @@ -0,0 +1,126 @@ +// Copyright 2025 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. + +#include "./meshCollisionAPI.h" + +#include "pxr/usd/sdf/assetPath.h" +#include "pxr/usd/sdf/types.h" +#include "pxr/usd/usd/schemaRegistry.h" +#include "pxr/usd/usd/typed.h" + +PXR_NAMESPACE_OPEN_SCOPE + +// Register the schema with the TfType system. +TF_REGISTRY_FUNCTION(TfType) { + TfType::Define >(); +} + +/* virtual */ +MjcPhysicsMeshCollisionAPI::~MjcPhysicsMeshCollisionAPI() {} + +/* static */ +MjcPhysicsMeshCollisionAPI MjcPhysicsMeshCollisionAPI::Get( + const UsdStagePtr &stage, const SdfPath &path) { + if (!stage) { + TF_CODING_ERROR("Invalid stage"); + return MjcPhysicsMeshCollisionAPI(); + } + return MjcPhysicsMeshCollisionAPI(stage->GetPrimAtPath(path)); +} + +/* virtual */ +UsdSchemaKind MjcPhysicsMeshCollisionAPI::_GetSchemaKind() const { + return MjcPhysicsMeshCollisionAPI::schemaKind; +} + +/* static */ +bool MjcPhysicsMeshCollisionAPI::CanApply(const UsdPrim &prim, + std::string *whyNot) { + return prim.CanApplyAPI(whyNot); +} + +/* static */ +MjcPhysicsMeshCollisionAPI MjcPhysicsMeshCollisionAPI::Apply( + const UsdPrim &prim) { + if (prim.ApplyAPI()) { + return MjcPhysicsMeshCollisionAPI(prim); + } + return MjcPhysicsMeshCollisionAPI(); +} + +/* static */ +const TfType &MjcPhysicsMeshCollisionAPI::_GetStaticTfType() { + static TfType tfType = TfType::Find(); + return tfType; +} + +/* static */ +bool MjcPhysicsMeshCollisionAPI::_IsTypedSchema() { + static bool isTyped = _GetStaticTfType().IsA(); + return isTyped; +} + +/* virtual */ +const TfType &MjcPhysicsMeshCollisionAPI::_GetTfType() const { + return _GetStaticTfType(); +} + +UsdAttribute MjcPhysicsMeshCollisionAPI::GetInertiaAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcInertia); +} + +UsdAttribute MjcPhysicsMeshCollisionAPI::CreateInertiaAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcInertia, SdfValueTypeNames->Token, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +namespace { +static inline TfTokenVector _ConcatenateAttributeNames( + const TfTokenVector &left, const TfTokenVector &right) { + TfTokenVector result; + result.reserve(left.size() + right.size()); + result.insert(result.end(), left.begin(), left.end()); + result.insert(result.end(), right.begin(), right.end()); + return result; +} +} // namespace + +/*static*/ +const TfTokenVector &MjcPhysicsMeshCollisionAPI::GetSchemaAttributeNames( + bool includeInherited) { + static TfTokenVector localNames = { + MjcPhysicsTokens->mjcInertia, + }; + static TfTokenVector allNames = _ConcatenateAttributeNames( + UsdAPISchemaBase::GetSchemaAttributeNames(true), localNames); + + if (includeInherited) + return allNames; + else + return localNames; +} + +PXR_NAMESPACE_CLOSE_SCOPE + +// ===================================================================== // +// Feel free to add custom code below this line. It will be preserved by +// the code generator. +// +// Just remember to wrap code in the appropriate delimiters: +// 'PXR_NAMESPACE_OPEN_SCOPE', 'PXR_NAMESPACE_CLOSE_SCOPE'. +// ===================================================================== // +// --(BEGIN CUSTOM CODE)-- diff --git a/src/experimental/usd/mjcPhysics/meshCollisionAPI.h b/src/experimental/usd/mjcPhysics/meshCollisionAPI.h new file mode 100644 index 00000000..55c8c27e --- /dev/null +++ b/src/experimental/usd/mjcPhysics/meshCollisionAPI.h @@ -0,0 +1,191 @@ +// Copyright 2025 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. + +#ifndef MJCPHYSICS_GENERATED_MESHCOLLISIONAPI_H +#define MJCPHYSICS_GENERATED_MESHCOLLISIONAPI_H + +/// \file mjcPhysics/meshCollisionAPI.h + +#include "./api.h" +#include "./tokens.h" +#include "pxr/base/gf/matrix4d.h" +#include "pxr/base/gf/vec3d.h" +#include "pxr/base/gf/vec3f.h" +#include "pxr/base/tf/token.h" +#include "pxr/base/tf/type.h" +#include "pxr/base/vt/value.h" +#include "pxr/pxr.h" +#include "pxr/usd/usd/apiSchemaBase.h" +#include "pxr/usd/usd/prim.h" +#include "pxr/usd/usd/stage.h" + +PXR_NAMESPACE_OPEN_SCOPE + +class SdfAssetPath; + +// -------------------------------------------------------------------------- // +// MESHCOLLISIONAPI // +// -------------------------------------------------------------------------- // + +/// \class MjcPhysicsMeshCollisionAPI +/// +/// API describing a Mujoco collider. +/// +/// For any described attribute \em Fallback \em Value or \em Allowed \em Values +/// below that are text/tokens, the actual token is published and defined in +/// \ref MjcPhysicsTokens. So to set an attribute to the value "rightHanded", +/// use MjcPhysicsTokens->rightHanded as the value. +/// +class MjcPhysicsMeshCollisionAPI : public UsdAPISchemaBase { + public: + /// Compile time constant representing what kind of schema this class is. + /// + /// \sa UsdSchemaKind + static const UsdSchemaKind schemaKind = UsdSchemaKind::SingleApplyAPI; + + /// Construct a MjcPhysicsMeshCollisionAPI on UsdPrim \p prim . + /// Equivalent to MjcPhysicsMeshCollisionAPI::Get(prim.GetStage(), + /// prim.GetPath()) for a \em valid \p prim, but will not immediately throw an + /// error for an invalid \p prim + explicit MjcPhysicsMeshCollisionAPI(const UsdPrim &prim = UsdPrim()) + : UsdAPISchemaBase(prim) {} + + /// Construct a MjcPhysicsMeshCollisionAPI on the prim held by \p schemaObj . + /// Should be preferred over MjcPhysicsMeshCollisionAPI(schemaObj.GetPrim()), + /// as it preserves SchemaBase state. + explicit MjcPhysicsMeshCollisionAPI(const UsdSchemaBase &schemaObj) + : UsdAPISchemaBase(schemaObj) {} + + /// Destructor. + MJCPHYSICS_API + virtual ~MjcPhysicsMeshCollisionAPI(); + + /// Return a vector of names of all pre-declared attributes for this schema + /// class and all its ancestor classes. Does not include attributes that + /// may be authored by custom/extended methods of the schemas involved. + MJCPHYSICS_API + static const TfTokenVector &GetSchemaAttributeNames( + bool includeInherited = true); + + /// Return a MjcPhysicsMeshCollisionAPI holding the prim adhering to this + /// schema at \p path on \p stage. If no prim exists at \p path on + /// \p stage, or if the prim at that path does not adhere to this schema, + /// return an invalid schema object. This is shorthand for the following: + /// + /// \code + /// MjcPhysicsMeshCollisionAPI(stage->GetPrimAtPath(path)); + /// \endcode + /// + MJCPHYSICS_API + static MjcPhysicsMeshCollisionAPI Get(const UsdStagePtr &stage, + const SdfPath &path); + + /// Returns true if this single-apply API schema can be applied to + /// the given \p prim. If this schema can not be a applied to the prim, + /// this returns false and, if provided, populates \p whyNot with the + /// reason it can not be applied. + /// + /// Note that if CanApply returns false, that does not necessarily imply + /// that calling Apply will fail. Callers are expected to call CanApply + /// before calling Apply if they want to ensure that it is valid to + /// apply a schema. + /// + /// \sa UsdPrim::GetAppliedSchemas() + /// \sa UsdPrim::HasAPI() + /// \sa UsdPrim::CanApplyAPI() + /// \sa UsdPrim::ApplyAPI() + /// \sa UsdPrim::RemoveAPI() + /// + MJCPHYSICS_API + static bool CanApply(const UsdPrim &prim, std::string *whyNot = nullptr); + + /// Applies this single-apply API schema to the given \p prim. + /// This information is stored by adding "MeshCollisionAPI" to the + /// token-valued, listOp metadata \em apiSchemas on the prim. + /// + /// \return A valid MjcPhysicsMeshCollisionAPI object is returned upon + /// success. An invalid (or empty) MjcPhysicsMeshCollisionAPI object is + /// returned upon failure. See \ref UsdPrim::ApplyAPI() for conditions + /// resulting in failure. + /// + /// \sa UsdPrim::GetAppliedSchemas() + /// \sa UsdPrim::HasAPI() + /// \sa UsdPrim::CanApplyAPI() + /// \sa UsdPrim::ApplyAPI() + /// \sa UsdPrim::RemoveAPI() + /// + MJCPHYSICS_API + static MjcPhysicsMeshCollisionAPI Apply(const UsdPrim &prim); + + protected: + /// Returns the kind of schema this class belongs to. + /// + /// \sa UsdSchemaKind + MJCPHYSICS_API + UsdSchemaKind _GetSchemaKind() const override; + + private: + // needs to invoke _GetStaticTfType. + friend class UsdSchemaRegistry; + MJCPHYSICS_API + static const TfType &_GetStaticTfType(); + + static bool _IsTypedSchema(); + + // override SchemaBase virtuals. + MJCPHYSICS_API + const TfType &_GetTfType() const override; + + public: + // --------------------------------------------------------------------- // + // INERTIA + // --------------------------------------------------------------------- // + /// Controls how a mesh is used when mass and inertia are inferred from + /// geometry. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform token mjc:inertia = "legacy"` | + /// | C++ Type | TfToken | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Token | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + /// | \ref MjcPhysicsTokens "Allowed Values" | legacy, convex, exact, shell | + MJCPHYSICS_API + UsdAttribute GetInertiaAttr() const; + + /// See GetInertiaAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateInertiaAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // ===================================================================== // + // Feel free to add custom code below this line, it will be preserved by + // the code generator. + // + // Just remember to: + // - Close the class declaration with }; + // - Close the namespace with PXR_NAMESPACE_CLOSE_SCOPE + // - Close the include guard with #endif + // ===================================================================== // + // --(BEGIN CUSTOM CODE)-- +}; + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif diff --git a/src/experimental/usd/mjcPhysics/plugInfo.json b/src/experimental/usd/mjcPhysics/plugInfo.json index 56e11dd5..efc5fe13 100644 --- a/src/experimental/usd/mjcPhysics/plugInfo.json +++ b/src/experimental/usd/mjcPhysics/plugInfo.json @@ -6,6 +6,26 @@ { "Info": { "Types": { + "MjcPhysicsCollisionAPI": { + "alias": { + "UsdSchemaBase": "CollisionAPI" + }, + "autoGenerated": true, + "bases": [ + "UsdAPISchemaBase" + ], + "schemaKind": "singleApplyAPI" + }, + "MjcPhysicsMeshCollisionAPI": { + "alias": { + "UsdSchemaBase": "MeshCollisionAPI" + }, + "autoGenerated": true, + "bases": [ + "UsdAPISchemaBase" + ], + "schemaKind": "singleApplyAPI" + }, "MjcPhysicsSceneAPI": { "alias": { "UsdSchemaBase": "SceneAPI" diff --git a/src/experimental/usd/mjcPhysics/schema.usda b/src/experimental/usd/mjcPhysics/schema.usda index 322a90a4..7996e5b5 100644 --- a/src/experimental/usd/mjcPhysics/schema.usda +++ b/src/experimental/usd/mjcPhysics/schema.usda @@ -515,3 +515,36 @@ class "SiteAPI" ) {} +class "CollisionAPI" +( + doc = """API describing a Mujoco collider.""" + + inherits = +) +{ + uniform bool mjc:shellinertia = False ( + customData = { + string apiName = "ShellInertia" + } + displayName = "Shell Inertia" + doc = """Enables handling of the inertia assuming mass is concentrated on the surface.""" + ) +} + +class "MeshCollisionAPI" +( + doc = """API describing a Mujoco collider.""" + + inherits = +) +{ + uniform token mjc:inertia = "legacy" ( + allowedTokens = ["legacy", "convex", "exact", "shell"] + customData = { + string apiName = "Inertia" + } + displayName = "Inertia" + doc = """Controls how a mesh is used when mass and inertia are inferred from geometry.""" + ) +} + diff --git a/src/experimental/usd/mjcPhysics/tokens.cpp b/src/experimental/usd/mjcPhysics/tokens.cpp index 2cc35a98..fc442b3f 100644 --- a/src/experimental/usd/mjcPhysics/tokens.cpp +++ b/src/experimental/usd/mjcPhysics/tokens.cpp @@ -19,11 +19,14 @@ PXR_NAMESPACE_OPEN_SCOPE MjcPhysicsTokensType::MjcPhysicsTokensType() : auto_("auto", TfToken::Immortal), cg("cg", TfToken::Immortal), + convex("convex", TfToken::Immortal), dense("dense", TfToken::Immortal), elliptic("elliptic", TfToken::Immortal), euler("euler", TfToken::Immortal), + exact("exact", TfToken::Immortal), implicit("implicit", TfToken::Immortal), implicitfast("implicitfast", TfToken::Immortal), + legacy("legacy", TfToken::Immortal), mjcFlagActuation("mjc:flag:actuation", TfToken::Immortal), mjcFlagAutoreset("mjc:flag:autoreset", TfToken::Immortal), mjcFlagClampctrl("mjc:flag:clampctrl", TfToken::Immortal), @@ -47,6 +50,7 @@ MjcPhysicsTokensType::MjcPhysicsTokensType() mjcFlagRefsafe("mjc:flag:refsafe", TfToken::Immortal), mjcFlagSensor("mjc:flag:sensor", TfToken::Immortal), mjcFlagWarmstart("mjc:flag:warmstart", TfToken::Immortal), + mjcInertia("mjc:inertia", TfToken::Immortal), mjcOptionActuatorgroupdisable("mjc:option:actuatorgroupdisable", TfToken::Immortal), mjcOptionApirate("mjc:option:apirate", TfToken::Immortal), @@ -76,20 +80,27 @@ MjcPhysicsTokensType::MjcPhysicsTokensType() mjcOptionTolerance("mjc:option:tolerance", TfToken::Immortal), mjcOptionViscosity("mjc:option:viscosity", TfToken::Immortal), mjcOptionWind("mjc:option:wind", TfToken::Immortal), + mjcShellinertia("mjc:shellinertia", TfToken::Immortal), newton("newton", TfToken::Immortal), pgs("pgs", TfToken::Immortal), pyramidal("pyramidal", TfToken::Immortal), rk4("rk4", TfToken::Immortal), + shell("shell", TfToken::Immortal), sparse("sparse", TfToken::Immortal), + CollisionAPI("CollisionAPI", TfToken::Immortal), + MeshCollisionAPI("MeshCollisionAPI", TfToken::Immortal), SceneAPI("SceneAPI", TfToken::Immortal), SiteAPI("SiteAPI", TfToken::Immortal), allTokens({auto_, cg, + convex, dense, elliptic, euler, + exact, implicit, implicitfast, + legacy, mjcFlagActuation, mjcFlagAutoreset, mjcFlagClampctrl, @@ -113,6 +124,7 @@ MjcPhysicsTokensType::MjcPhysicsTokensType() mjcFlagRefsafe, mjcFlagSensor, mjcFlagWarmstart, + mjcInertia, mjcOptionActuatorgroupdisable, mjcOptionApirate, mjcOptionCcd_iterations, @@ -139,11 +151,15 @@ MjcPhysicsTokensType::MjcPhysicsTokensType() mjcOptionTolerance, mjcOptionViscosity, mjcOptionWind, + mjcShellinertia, newton, pgs, pyramidal, rk4, + shell, sparse, + CollisionAPI, + MeshCollisionAPI, SceneAPI, SiteAPI}) {} diff --git a/src/experimental/usd/mjcPhysics/tokens.h b/src/experimental/usd/mjcPhysics/tokens.h index 7bdf56f2..1624ce6a 100644 --- a/src/experimental/usd/mjcPhysics/tokens.h +++ b/src/experimental/usd/mjcPhysics/tokens.h @@ -63,6 +63,10 @@ struct MjcPhysicsTokensType { /// Possible value for MjcPhysicsSceneAPI::GetSolverAttr(), This token /// represents the CG constraint solver algorithm. const TfToken cg; + /// \brief "convex" + /// + /// Possible value for MjcPhysicsMeshCollisionAPI::GetInertiaAttr() + const TfToken convex; /// \brief "dense" /// /// Possible value for MjcPhysicsSceneAPI::GetJacobianAttr(), This token @@ -78,6 +82,10 @@ struct MjcPhysicsTokensType { /// Fallback value for MjcPhysicsSceneAPI::GetIntegratorAttr(), This token /// represents the Euler numerical integrator. const TfToken euler; + /// \brief "exact" + /// + /// Possible value for MjcPhysicsMeshCollisionAPI::GetInertiaAttr() + const TfToken exact; /// \brief "implicit" /// /// Possible value for MjcPhysicsSceneAPI::GetIntegratorAttr(), This token @@ -88,6 +96,10 @@ struct MjcPhysicsTokensType { /// Possible value for MjcPhysicsSceneAPI::GetIntegratorAttr(), This token /// represents the implicitfast numerical integrator. const TfToken implicitfast; + /// \brief "legacy" + /// + /// Fallback value for MjcPhysicsMeshCollisionAPI::GetInertiaAttr() + const TfToken legacy; /// \brief "mjc:flag:actuation" /// /// MjcPhysicsSceneAPI @@ -180,6 +192,10 @@ struct MjcPhysicsTokensType { /// /// MjcPhysicsSceneAPI const TfToken mjcFlagWarmstart; + /// \brief "mjc:inertia" + /// + /// MjcPhysicsMeshCollisionAPI + const TfToken mjcInertia; /// \brief "mjc:option:actuatorgroupdisable" /// /// MjcPhysicsSceneAPI @@ -284,6 +300,10 @@ struct MjcPhysicsTokensType { /// /// MjcPhysicsSceneAPI const TfToken mjcOptionWind; + /// \brief "mjc:shellinertia" + /// + /// MjcPhysicsCollisionAPI + const TfToken mjcShellinertia; /// \brief "newton" /// /// Fallback value for MjcPhysicsSceneAPI::GetSolverAttr(), This token @@ -304,11 +324,23 @@ struct MjcPhysicsTokensType { /// Possible value for MjcPhysicsSceneAPI::GetIntegratorAttr(), This token /// represents the RK4 numerical integrator. const TfToken rk4; + /// \brief "shell" + /// + /// Possible value for MjcPhysicsMeshCollisionAPI::GetInertiaAttr() + const TfToken shell; /// \brief "sparse" /// /// Possible value for MjcPhysicsSceneAPI::GetJacobianAttr(), This token /// represents the sparse constraint Jacobian and matrices computed from it. const TfToken sparse; + /// \brief "CollisionAPI" + /// + /// Schema identifer and family for MjcPhysicsCollisionAPI + const TfToken CollisionAPI; + /// \brief "MeshCollisionAPI" + /// + /// Schema identifer and family for MjcPhysicsMeshCollisionAPI + const TfToken MeshCollisionAPI; /// \brief "SceneAPI" /// /// Schema identifer and family for MjcPhysicsSceneAPI From eb6f7dc6b7fc3484a903ff5ef928bdff5c5ee1d7 Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Fri, 23 May 2025 12:10:03 -0700 Subject: [PATCH 169/191] Gate some calls with `__EMSCRIPTEN__` PiperOrigin-RevId: 762520516 Change-Id: If9f7729721bab8c947861a968cf96eecc6591228 --- src/engine/engine_util_errmem.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/engine_util_errmem.c b/src/engine/engine_util_errmem.c index 58be05a1..04e98009 100644 --- a/src/engine/engine_util_errmem.c +++ b/src/engine/engine_util_errmem.c @@ -97,7 +97,7 @@ void mju_writeLog(const char* type, const char* msg) { // get time time(&rawtime); -#if defined(_POSIX_C_SOURCE) || defined(__APPLE__) || defined(__STDC_VERSION_TIME_H__) +#if defined(_POSIX_C_SOURCE) || defined(__APPLE__) || defined(__STDC_VERSION_TIME_H__) || defined(__EMSCRIPTEN__) localtime_r(&rawtime, &timeinfo); #elif _MSC_VER localtime_s(&timeinfo, &rawtime); From 7a5b55f57eb5c1bc6148b641d0184ca6512df600 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Fri, 23 May 2025 14:35:58 -0700 Subject: [PATCH 170/191] Expose mjs_resolveOrientation to the Python API. PiperOrigin-RevId: 762571376 Change-Id: I08d8394b24670c7f102356da4234b17f1ba4e05f --- python/mujoco/specs.cc | 14 ++++++++++++++ python/mujoco/specs_test.py | 13 +++++++++++++ 2 files changed, 27 insertions(+) diff --git a/python/mujoco/specs.cc b/python/mujoco/specs.cc index b32942a5..3ec74da8 100644 --- a/python/mujoco/specs.cc +++ b/python/mujoco/specs.cc @@ -490,6 +490,20 @@ PYBIND11_MODULE(_specs, m) { }, py::arg("name"), py::return_value_policy::reference_internal); + mjSpec.def_static( + "resolve_orientation", + [](bool degree, const MjTypeVec& sequence, + const raw::MjsOrientation* orientation) -> std::array { + std::array quat = {0, 0, 0, 0}; + const char* err = mjs_resolveOrientation(quat.data(), degree, + sequence.ptr, orientation); + if (err) { + throw pybind11::value_error(err); + } + return quat; + }, + py::arg("degree"), py::arg("sequence") = py::none(), + py::arg("orientation"), py::return_value_policy::copy); // ============================= MJSBODY ===================================== mjsBody.def( diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py index c35c6618..41a2043c 100644 --- a/python/mujoco/specs_test.py +++ b/python/mujoco/specs_test.py @@ -16,6 +16,7 @@ import gc import inspect +import math import os import textwrap import typing @@ -129,6 +130,18 @@ class SpecsTest(absltest.TestCase): """) self.assertEqual(spec.to_xml(), xml) + def test_resolve_orientation(self): + spec = mujoco.MjSpec() + body = spec.worldbody.add_body(euler=[0, 0, 90]) + quat = mujoco.MjSpec.resolve_orientation( + degree=spec.compiler.degree, + sequence=spec.compiler.eulerseq, + orientation=body.alt, + ) + np.testing.assert_array_almost_equal( + quat, [math.sqrt(2) / 2, 0, 0, math.sqrt(2) / 2] + ) + def test_kwarg(self): # Create a spec. spec = mujoco.MjSpec() From 4a102c37bc5dcf1a31edf658a6a799dc8d1857ef Mon Sep 17 00:00:00 2001 From: Tom Erez Date: Mon, 26 May 2025 07:02:42 -0700 Subject: [PATCH 171/191] Add support for indexing qfrc fields in MJX bind() method. PiperOrigin-RevId: 763415415 Change-Id: Idb567a1711679565444779fb1062d7b65434ef13 --- mjx/mujoco/mjx/_src/support.py | 12 ++++++++---- mjx/mujoco/mjx/_src/support_test.py | 7 ++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/mjx/mujoco/mjx/_src/support.py b/mjx/mujoco/mjx/_src/support.py index ab330067..15715021 100644 --- a/mjx/mujoco/mjx/_src/support.py +++ b/mjx/mujoco/mjx/_src/support.py @@ -433,11 +433,13 @@ class BindData(object): return name else: raise AttributeError('ctrl is not available for this type') - if name == 'qpos' or name == 'qvel' or name == 'qacc': + if name == 'qpos' or name == 'qvel' or name == 'qacc' or name.startswith('qfrc_'): if self.prefix == 'jnt_': return name else: - raise AttributeError('qpos, qvel, qacc are not available for this type') + raise AttributeError( + 'qpos, qvel, qacc, qfrc are not available for this type' + ) else: return self.prefix + name @@ -451,7 +453,9 @@ class BindData(object): return var[..., idx, :] def __getattr__(self, name: str): - if name in ('sensordata', 'qpos', 'qvel', 'qacc'): + if name in ('sensordata', 'qpos', 'qvel', 'qacc') or ( + name.startswith('qfrc_') + ): adr = num = 0 if name == 'sensordata': adr = self.model.sensor_adr[self.id] @@ -460,7 +464,7 @@ class BindData(object): adr = self.model.jnt_qposadr[self.id] typ = self.model.jnt_type[self.id] num = sum((typ == jt) * jt.qpos_width() for jt in JointType) - elif name == 'qvel' or name == 'qacc': + elif name == 'qvel' or name == 'qacc' or name.startswith('qfrc_'): adr = self.model.jnt_dofadr[self.id] typ = self.model.jnt_type[self.id] num = sum((typ == jt) * jt.dof_width() for jt in JointType) diff --git a/mjx/mujoco/mjx/_src/support_test.py b/mjx/mujoco/mjx/_src/support_test.py index 05c1e5bd..3303aa62 100644 --- a/mjx/mujoco/mjx/_src/support_test.py +++ b/mjx/mujoco/mjx/_src/support_test.py @@ -249,6 +249,11 @@ class SupportTest(parameterized.TestCase): dx.bind(mx, s.joints[i]).qacc, d.qacc[m.jnt_dofadr[i]:m.jnt_dofadr[i] + dofnum[i]], decimal=6 ) + np.testing.assert_array_almost_equal( + dx.bind(mx, s.joints[i]).qfrc_actuator, + d.qfrc_actuator[m.jnt_dofadr[i] : m.jnt_dofadr[i] + dofnum[i]], + decimal=6, + ) np.testing.assert_array_equal(dx.bind(mx, s.actuators).ctrl, d.ctrl) for i in range(m.nu): @@ -344,7 +349,7 @@ class SupportTest(parameterized.TestCase): ): print(dx.bind(mx, s.actuators).set('actuator_ctrl', [1, 2, 3])) with self.assertRaisesRegex( - AttributeError, 'qpos, qvel, qacc are not available for this type' + AttributeError, 'qpos, qvel, qacc, qfrc are not available for this type' ): print(dx.bind(mx, s.geoms).qpos) From 89e39dc0a74bccace9b1c1cd86112b2d519a589c Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 27 May 2025 00:33:27 -0700 Subject: [PATCH 172/191] Expose `mj_makeM` as a sub-component. PiperOrigin-RevId: 763662068 Change-Id: I55ced456dca751c1de09fb35e2f73c60ffc0d626 --- doc/APIreference/functions.rst | 15 +++++++++++++-- doc/APIreference/functions_override.rst | 10 ++++++++-- doc/XMLreference.rst | 4 ++-- doc/changelog.rst | 3 +++ doc/computation/index.rst | 2 +- doc/includes/references.h | 3 ++- include/mujoco/mjdata.h | 2 +- include/mujoco/mujoco.h | 3 +++ python/mujoco/functions.cc | 1 + python/mujoco/introspect/functions.py | 20 ++++++++++++++++++++ src/engine/engine_core_smooth.h | 2 +- unity/Runtime/Bindings/MjBindings.cs | 3 +++ 12 files changed, 58 insertions(+), 10 deletions(-) diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index 5b985861..e413f772 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -748,8 +748,7 @@ Compare forward and inverse dynamics, save results in fwdinv. Sub components ^^^^^^^^^^^^^^ -These are sub-components of the simulation pipeline, called internally from the components above. It is very unlikely -that the user will need to call them. +These are sub-components of the simulation pipeline, called internally from the components above. .. _mj_sensorPos: @@ -886,6 +885,18 @@ Compute actuator transmission lengths and moments. Run composite rigid body inertia algorithm (CRB). +.. _mj_makeM: + +`mj_makeM <#mj_makeM>`__ +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. mujoco-include:: mj_makeM + +Compute the composite rigid body inertia with :ref:`mj_crb`, add terms due +to :ref:`tendon armature`. The joint-space inertia matrix is stored in both ``mjData.qM`` and +``mjData.M``. These arrays represent the same quantity using different layouts (parent-based and compressed sparse row, +respectively). + .. _mj_factorM: `mj_factorM <#mj_factorM>`__ diff --git a/doc/APIreference/functions_override.rst b/doc/APIreference/functions_override.rst index ec72a183..5d00db0a 100644 --- a/doc/APIreference/functions_override.rst +++ b/doc/APIreference/functions_override.rst @@ -113,8 +113,14 @@ Integrates the simulation state using an implicit-in-velocity integrator (either .. _Subcomponents: -These are sub-components of the simulation pipeline, called internally from the components above. It is very unlikely -that the user will need to call them. +These are sub-components of the simulation pipeline, called internally from the components above. + +.. _mj_makeM: + +Compute the composite rigid body inertia with :ref:`mj_crb`, add terms due +to :ref:`tendon armature`. The joint-space inertia matrix is stored in both ``mjData.qM`` and +``mjData.M``. These arrays represent the same quantity using different layouts (parent-based and compressed sparse row, +respectively). .. _mj_factorM: diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 727950af..e276c831 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -2918,8 +2918,8 @@ Attributes may be applied or ignored depending on the lighting model being used. .. _body-light-directional: :at:`directional`: :at-val:`[false, true], "false"` - This is a deprecated legacy attribute. Please use :ref:`light ` type instead. If set to "true", and no - type is specified, this will change the light type to be directional. + This is a deprecated legacy attribute. Please use :ref:`light ` type instead. If set to "true", and + no type is specified, this will change the light type to be directional. .. _body-light-castshadow: diff --git a/doc/changelog.rst b/doc/changelog.rst index 3ddc1b51..8f5135b6 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -14,6 +14,9 @@ General - Replaced the :ref:`directional` (boolean) field for lights with a :ref:`type` field (of type :ref:`mjtLightType`) to allow for additional lighting types. +- Added new sub-component :ref:`mj_makeM` which combines the :ref:`mj_crb` call with additional logic to support the + introduction in 3.3.1 of :ref:`tendon armature`. In addition to the traditional + ``mjData.qM``, :ref:`mj_makeM` also computes ``mjData.M``, a CSR representation of the same matrix. Simulate ^^^^^^^^ diff --git a/doc/computation/index.rst b/doc/computation/index.rst index 1021067a..ff87af05 100644 --- a/doc/computation/index.rst +++ b/doc/computation/index.rst @@ -1692,7 +1692,7 @@ The stages below compute quantities that depend on the generalized positions ``m 4. Compute quantities related to :ref:`flex` objects: :ref:`mj_flex` 5. Compute the tendon lengths and moment arms. This includes the computation of minimal-length paths for spatial tendons: :ref:`mj_tendon` -6. Compute the composite rigid body inertias and joint-space inertia matrix: :ref:`mj_crb` +6. Compute the composite rigid body inertias and joint-space inertia matrix: :ref:`mj_makeM` 7. Compute the sparse factorization of the joint-space inertia matrix: :ref:`mj_factorM` 8. Construct the list of active contacts. This includes both broad-phase and near-phase collision detection: :ref:`mj_collision` diff --git a/doc/includes/references.h b/doc/includes/references.h index f61acd8e..2acf7ea6 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -267,7 +267,7 @@ struct mjData_ { int* moment_colind; // column indices in sparse Jacobian (nJmom x 1) mjtNum* actuator_moment; // actuator moments (nJmom x 1) - // computed by mj_fwdPosition/mj_crb + // computed by mj_fwdPosition/mj_makeM mjtNum* crb; // com-based composite inertia and mass (nbody x 10) mjtNum* qM; // inertia (sparse) (nM x 1) mjtNum* M; // reduced inertia (compressed sparse row) (nC x 1) @@ -3068,6 +3068,7 @@ void mj_flex(const mjModel* m, mjData* d); void mj_tendon(const mjModel* m, mjData* d); void mj_transmission(const mjModel* m, mjData* d); void mj_crb(const mjModel* m, mjData* d); +void mj_makeM(const mjModel* m, mjData* d); void mj_factorM(const mjModel* m, mjData* d); void mj_solveM(const mjModel* m, mjData* d, mjtNum* x, const mjtNum* y, int n); void mj_solveM2(const mjModel* m, mjData* d, mjtNum* x, const mjtNum* y, diff --git a/include/mujoco/mjdata.h b/include/mujoco/mjdata.h index c05db9ab..1d894fd4 100644 --- a/include/mujoco/mjdata.h +++ b/include/mujoco/mjdata.h @@ -295,7 +295,7 @@ struct mjData_ { int* moment_colind; // column indices in sparse Jacobian (nJmom x 1) mjtNum* actuator_moment; // actuator moments (nJmom x 1) - // computed by mj_fwdPosition/mj_crb + // computed by mj_fwdPosition/mj_makeM mjtNum* crb; // com-based composite inertia and mass (nbody x 10) mjtNum* qM; // inertia (sparse) (nM x 1) mjtNum* M; // reduced inertia (compressed sparse row) (nC x 1) diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index 6c715427..467d888a 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -364,6 +364,9 @@ MJAPI void mj_transmission(const mjModel* m, mjData* d); // Run composite rigid body inertia algorithm (CRB). MJAPI void mj_crb(const mjModel* m, mjData* d); +// Make inertia matrix. +MJAPI void mj_makeM(const mjModel* m, mjData* d); + // Compute sparse L'*D*L factorizaton of inertia matrix. MJAPI void mj_factorM(const mjModel* m, mjData* d); diff --git a/python/mujoco/functions.cc b/python/mujoco/functions.cc index d4708254..b23c6d1e 100644 --- a/python/mujoco/functions.cc +++ b/python/mujoco/functions.cc @@ -211,6 +211,7 @@ PYBIND11_MODULE(_functions, pymodule) { Def(pymodule); Def(pymodule); Def(pymodule); + Def(pymodule); Def(pymodule); DEF_WITH_OMITTED_PY_ARGS(traits::mj_solveM, "n")( pymodule, diff --git a/python/mujoco/introspect/functions.py b/python/mujoco/introspect/functions.py index cf85a819..44901fe4 100644 --- a/python/mujoco/introspect/functions.py +++ b/python/mujoco/introspect/functions.py @@ -1829,6 +1829,26 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), doc='Run composite rigid body inertia algorithm (CRB).', )), + ('mj_makeM', + FunctionDecl( + name='mj_makeM', + return_type=ValueType(name='void'), + parameters=( + FunctionParameterDecl( + name='m', + type=PointerType( + inner_type=ValueType(name='mjModel', is_const=True), + ), + ), + FunctionParameterDecl( + name='d', + type=PointerType( + inner_type=ValueType(name='mjData'), + ), + ), + ), + doc='Make inertia matrix.', + )), ('mj_factorM', FunctionDecl( name='mj_factorM', diff --git a/src/engine/engine_core_smooth.h b/src/engine/engine_core_smooth.h index a8cd1a2b..6a672b3a 100644 --- a/src/engine/engine_core_smooth.h +++ b/src/engine/engine_core_smooth.h @@ -55,7 +55,7 @@ MJAPI void mj_crb(const mjModel* m, mjData* d); MJAPI void mj_tendonArmature(const mjModel* m, mjData* d); // make inertia matrix -void mj_makeM(const mjModel* m, mjData* d); +MJAPI void mj_makeM(const mjModel* m, mjData* d); // sparse L'*D*L factorizaton of inertia-like matrix M, assumed spd (legacy implementation) MJAPI void mj_factorI_legacy(const mjModel* m, mjData* d, const mjtNum* M, diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index ae6a9ac5..d15803a7 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -6511,6 +6511,9 @@ public static unsafe extern void mj_transmission(mjModel_* m, mjData_* d); [DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] public static unsafe extern void mj_crb(mjModel_* m, mjData_* d); +[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] +public static unsafe extern void mj_makeM(mjModel_* m, mjData_* d); + [DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)] public static unsafe extern void mj_factorM(mjModel_* m, mjData_* d); From ccc912c3b0ae6dba0bcf5a94d9980f296a88fd46 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 27 May 2025 02:16:33 -0700 Subject: [PATCH 173/191] Improve docstring in `mjvGeom`, fixes #2629 PiperOrigin-RevId: 763693102 Change-Id: Icb9a3168691690ccf086adb5a20358b3952d6717 --- doc/includes/references.h | 2 +- include/mujoco/mjvisualize.h | 2 +- python/mujoco/introspect/structs.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/includes/references.h b/doc/includes/references.h index 2acf7ea6..fc9943c1 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -2815,7 +2815,7 @@ typedef struct mjvGLCamera_ mjvGLCamera; struct mjvGeom_ { // abstract geom // type info int type; // geom type (mjtGeom) - int dataid; // mesh, hfield or plane id; -1: none + int dataid; // mesh, hfield or plane id; -1: none; mesh: 2*id or 2*id+1 (hull) int objtype; // mujoco object type; mjOBJ_UNKNOWN for decor int objid; // mujoco object id; -1 for decor int category; // visual category diff --git a/include/mujoco/mjvisualize.h b/include/mujoco/mjvisualize.h index a61fe649..5903ba59 100644 --- a/include/mujoco/mjvisualize.h +++ b/include/mujoco/mjvisualize.h @@ -226,7 +226,7 @@ typedef struct mjvGLCamera_ mjvGLCamera; struct mjvGeom_ { // abstract geom // type info int type; // geom type (mjtGeom) - int dataid; // mesh, hfield or plane id; -1: none + int dataid; // mesh, hfield or plane id; -1: none; mesh: 2*id or 2*id+1 (hull) int objtype; // mujoco object type; mjOBJ_UNKNOWN for decor int objid; // mujoco object id; -1 for decor int category; // visual category diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index 8ab28046..283a158a 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -6567,7 +6567,7 @@ STRUCTS: Mapping[str, StructDecl] = dict([ StructFieldDecl( name='dataid', type=ValueType(name='int'), - doc='mesh, hfield or plane id; -1: none', + doc='mesh, hfield or plane id; -1: none; mesh: 2*id or 2*id+1 (hull)', # pylint: disable=line-too-long ), StructFieldDecl( name='objtype', From de3dc7c234e1207e4dd706a6d8eb844fb3daa8a4 Mon Sep 17 00:00:00 2001 From: Taylor Howell Date: Tue, 27 May 2025 04:25:03 -0700 Subject: [PATCH 174/191] Update documentation from `mjData.solver_iter` to `mjData.solver_niter`. PiperOrigin-RevId: 763729137 Change-Id: I1266539ead38e083d427614d50b2aa79a68aa09c --- doc/APIreference/APIglobals.rst | 2 +- doc/APIreference/APItypes.rst | 2 +- doc/programming/simulation.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/APIreference/APIglobals.rst b/doc/APIreference/APIglobals.rst index 09c01f6c..11077782 100644 --- a/doc/APIreference/APIglobals.rst +++ b/doc/APIreference/APIglobals.rst @@ -448,7 +448,7 @@ shown in the table below. Their names are in the format ``mjKEY_XXX``. They corr - 200 - The number of iterations where solver statistics can be stored in ``mjData.solver``. This array is used to store diagnostic information about each iteration of the constraint solver. - The actual number of iterations is given by ``mjData.solver_iter``. + The actual number of iterations is given by ``mjData.solver_niter``. * - ``mjNISLAND`` - 20 - The number of islands for which solver statistics can be stored in ``mjData.solver``. This array is diff --git a/doc/APIreference/APItypes.rst b/doc/APIreference/APItypes.rst index f2ecf868..f1df0615 100644 --- a/doc/APIreference/APItypes.rst +++ b/doc/APIreference/APItypes.rst @@ -914,7 +914,7 @@ mjSolverStat This is the data structure holding information about one solver iteration. ``mjData.solver`` is a preallocated array of mjSolverStat data structures, one for each iteration of the solver, up to a maximum of mjNSOLVER. The actual number -of solver iterations is given by ``mjData.solver_iter``. +of solver iterations is given by ``mjData.solver_niter``. .. mujoco-include:: mjSolverStat diff --git a/doc/programming/simulation.rst b/doc/programming/simulation.rst index d0cf5029..26bf3fe7 100644 --- a/doc/programming/simulation.rst +++ b/doc/programming/simulation.rst @@ -807,7 +807,7 @@ to implement high-resolution timers in C without bringing in additional dependen does not need timing, and in that case there is no reason to call timing functions. One part of the simulation pipeline that needs to be monitored closely is the iterative constraint solver. The -simplest diagnostic here is ``mjData.solver_iter`` which shows how many iterations the solver took on the last call to +simplest diagnostic here is ``mjData.solver_niter`` which shows how many iterations the solver took on the last call to mj_step or ``mj_forward``. Note that the solver has tolerance parameters for early termination, so this number is usually smaller than the maximum number of iterations allowed. The array ``mjData.solver`` contains one :ref:`mjSolverStat` data structure per iteration of the constraint solver, with information about the constraint state From 441ca838a1f5a614e5d2314982fd738a4e902575 Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Tue, 27 May 2025 04:57:00 -0700 Subject: [PATCH 175/191] Add support for inertia and mass to Mujoco USD interop. PiperOrigin-RevId: 763737956 Change-Id: I6cdf267d76536a26aeb6327ce90165842e32bd8a --- .../usd/plugins/mjcf/mujoco_to_usd.cc | 97 +++++++++++++- .../usd/plugins/mjcf/mjcf_file_format_test.cc | 120 ++++++++++++++++++ test/experimental/usd/test_utils.cc | 9 ++ test/experimental/usd/test_utils.h | 2 + 4 files changed, 223 insertions(+), 5 deletions(-) diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc index d8bf0e3e..69e03ebb 100644 --- a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc @@ -26,6 +26,7 @@ #include "mjcf/utils.h" #include #include +#include #include #include #include @@ -180,7 +181,7 @@ class ModelWriter { WritePhysicsScene(); // Author mesh scope + mesh prims to be referenced. - WriteMeshes(); + WriteMeshes(write_physics); WriteMaterials(); WriteBodies(write_physics); } @@ -298,7 +299,8 @@ class ModelWriter { SetAttributeDefault(data_, xform_op_order_path, new_order); } - void WriteMesh(const mjsMesh *mesh, const pxr::SdfPath &parent_path) { + void WriteMesh(const mjsMesh *mesh, const pxr::SdfPath &parent_path, + bool write_physics) { auto name = GetAvailablePrimName(*mesh->name, pxr::UsdGeomTokens->Mesh, parent_path); pxr::SdfPath subcomponent_path = @@ -308,6 +310,22 @@ class ModelWriter { pxr::UsdGeomTokens->Mesh); mesh_paths_[*mesh->name] = subcomponent_path; + if (write_physics) { + ApplyApiSchema(data_, mesh_path, MjcPhysicsTokens->MeshCollisionAPI); + + pxr::TfToken inertia = MjcPhysicsTokens->legacy; + if (mesh->inertia == mjtMeshInertia::mjMESH_INERTIA_EXACT) { + inertia = MjcPhysicsTokens->exact; + } else if (mesh->inertia == mjtMeshInertia::mjMESH_INERTIA_CONVEX) { + inertia = MjcPhysicsTokens->convex; + } else if (mesh->inertia == mjtMeshInertia::mjMESH_INERTIA_SHELL) { + inertia = MjcPhysicsTokens->shell; + } + + WriteUniformAttribute(mesh_path, pxr::SdfValueTypeNames->Token, + MjcPhysicsTokens->mjcInertia, inertia); + } + // NOTE: The geometry data taken from the spec is the post-compilation // data after it has been mjCMesh::Compile'd. So don't be surprised if // things like user defined vertices have moved due to re-centering to @@ -584,7 +602,7 @@ class ModelWriter { } } - void WriteMeshes() { + void WriteMeshes(bool write_physics) { // Create a scope for the meshes to keep things organized pxr::SdfPath scope_path = CreatePrimSpec(data_, body_paths_[kWorldIndex], kTokens->meshScope, @@ -596,7 +614,7 @@ class ModelWriter { mjsMesh *mesh = mjs_asMesh(mjs_firstElement(spec_, mjOBJ_MESH)); while (mesh) { - WriteMesh(mesh, scope_path); + WriteMesh(mesh, scope_path, write_physics); mesh = mjs_asMesh(mjs_nextElement(spec_, mesh->element)); } } @@ -1032,12 +1050,43 @@ class ModelWriter { return; } - // Apply the PhysicsCollisionAPI schema if we are writing physics and the + // Apply the physics schemas if we are writing physics and the // geom participates in collisions. if (write_physics && (model_->geom_contype[geom_id] != 0 || model_->geom_conaffinity[geom_id] != 0)) { ApplyApiSchema(data_, geom_path, pxr::UsdPhysicsTokens->PhysicsCollisionAPI); + ApplyApiSchema(data_, geom_path, MjcPhysicsTokens->CollisionAPI); + + WriteUniformAttribute( + geom_path, pxr::SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcShellinertia, + geom->typeinertia == mjtGeomInertia::mjINERTIA_SHELL); + + if (geom->mass >= mjMINVAL || geom->density >= mjMINVAL) { + ApplyApiSchema(data_, geom_path, pxr::UsdPhysicsTokens->PhysicsMassAPI); + } + + if (geom->mass >= mjMINVAL) { + pxr::SdfPath mass_attr = CreateAttributeSpec( + data_, geom_path, pxr::UsdPhysicsTokens->physicsMass, + pxr::SdfValueTypeNames->Float, pxr::SdfVariabilityUniform); + + // Make sure to cast to float here since mjtNum might be a double. + SetAttributeDefault(data_, mass_attr, (float)geom->mass); + } + + // Even though density is not used for mass computation when mass exists + // we want to retain the information anyways. + if (geom->density >= mjMINVAL) { + pxr::SdfPath density_attr = CreateAttributeSpec( + data_, geom_path, pxr::UsdPhysicsTokens->physicsDensity, + pxr::SdfValueTypeNames->Float, pxr::SdfVariabilityUniform); + + // Make sure to cast to float here since mjtNum might be a double. + SetAttributeDefault(data_, density_attr, (float)geom->density); + } + // For meshes, also apply PhysicsMeshCollisionAPI and set the // approximation attribute. if (geom->type == mjGEOM_MESH) { @@ -1238,6 +1287,44 @@ class ModelWriter { // Apply the PhysicsRigidBodyAPI schema if we are writing physics. if (write_physics) { + // If the body had a mass specified then it must have either inertia or + // fullinertia specified per inertia element XML documentation. + // Therefore it is sufficient to check if the mass is non-zero to see if + // we should set inertial attributes on the body. + // + // Note that if the user has NOT specified any inertial properties then + // we don't want to pull values from the compiled model since coming back + // into Mujoco would take those values instead of computing them + // automatically from the subtree. + if (body->mass > 0) { + // User might have specified the inertia via fullinertia and the + // compiler has extracted all values properly. So leverage those + // instead of doing the computation ourselves here. + ApplyApiSchema(data_, body_path, pxr::UsdPhysicsTokens->PhysicsMassAPI); + WriteUniformAttribute(body_path, pxr::SdfValueTypeNames->Float, + pxr::UsdPhysicsTokens->physicsMass, + (float)model_->body_mass[body_id]); + + mjtNum *body_ipos = &model_->body_ipos[body_id * 3]; + pxr::GfVec3f inertial_pos(body_ipos[0], body_ipos[1], body_ipos[2]); + WriteUniformAttribute(body_path, pxr::SdfValueTypeNames->Point3f, + pxr::UsdPhysicsTokens->physicsCenterOfMass, + inertial_pos); + + mjtNum *body_iquat = &model_->body_iquat[body_id * 4]; + pxr::GfQuatf inertial_frame(body_iquat[0], body_iquat[1], body_iquat[2], + body_iquat[3]); + WriteUniformAttribute(body_path, pxr::SdfValueTypeNames->Quatf, + pxr::UsdPhysicsTokens->physicsPrincipalAxes, + inertial_frame); + + mjtNum *inertia = &model_->body_inertia[body_id * 3]; + pxr::GfVec3f diag_inertia(inertia[0], inertia[1], inertia[2]); + WriteUniformAttribute(body_path, pxr::SdfValueTypeNames->Float3, + pxr::UsdPhysicsTokens->physicsDiagonalInertia, + diag_inertia); + } + ApplyApiSchema(data_, body_path, pxr::UsdPhysicsTokens->PhysicsRigidBodyAPI); diff --git a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc index 9e2995de..4d35eb89 100644 --- a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc +++ b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc @@ -17,6 +17,8 @@ #include #include +#include "src/experimental/usd/mjcPhysics/collisionAPI.h" +#include "src/experimental/usd/mjcPhysics/meshCollisionAPI.h" #include "src/experimental/usd/mjcPhysics/sceneAPI.h" #include "src/experimental/usd/mjcPhysics/siteAPI.h" #include "src/experimental/usd/mjcPhysics/tokens.h" @@ -50,6 +52,7 @@ #include #include #include +#include #include #include #include @@ -1106,6 +1109,7 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsColliders) { EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/ground", pxr::UsdPhysicsRigidBodyAPI); EXPECT_PRIM_API_APPLIED(stage, "/test/ground", pxr::UsdPhysicsCollisionAPI); + EXPECT_PRIM_API_APPLIED(stage, "/test/ground", pxr::MjcPhysicsCollisionAPI); // body_0/body_0_0 [rigidbody] (Nested body - reparented) EXPECT_PRIM_VALID(stage, "/test/body_0/body_0_0"); @@ -1113,30 +1117,40 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsColliders) { pxr::UsdPhysicsRigidBodyAPI); EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_0/body_0_0", pxr::UsdPhysicsCollisionAPI); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_0/body_0_0", + pxr::MjcPhysicsCollisionAPI); // body_0/body_0_0/body_0_0_col [collider] EXPECT_PRIM_VALID(stage, "/test/body_0/body_0_0/body_0_0_col"); EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_0/body_0_0/body_0_0_col", pxr::UsdPhysicsRigidBodyAPI); EXPECT_PRIM_API_APPLIED(stage, "/test/body_0/body_0_0/body_0_0_col", pxr::UsdPhysicsCollisionAPI); + EXPECT_PRIM_API_APPLIED(stage, "/test/body_0/body_0_0/body_0_0_col", + pxr::MjcPhysicsCollisionAPI); // body_1 [rigidbody] EXPECT_PRIM_VALID(stage, "/test/body_1"); EXPECT_PRIM_API_APPLIED(stage, "/test/body_1", pxr::UsdPhysicsRigidBodyAPI); EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_1", pxr::UsdPhysicsCollisionAPI); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_1", + pxr::MjcPhysicsCollisionAPI); // body_1/body_1_col_0 [collider] EXPECT_PRIM_VALID(stage, "/test/body_1/body_1_col_0"); EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_1/body_1_col_0", pxr::UsdPhysicsRigidBodyAPI); EXPECT_PRIM_API_APPLIED(stage, "/test/body_1/body_1_col_0", pxr::UsdPhysicsCollisionAPI); + EXPECT_PRIM_API_APPLIED(stage, "/test/body_1/body_1_col_0", + pxr::MjcPhysicsCollisionAPI); // body_1/body_1_col_1 [collider] EXPECT_PRIM_VALID(stage, "/test/body_1/body_1_col_1"); EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body_1/body_1_col_1", pxr::UsdPhysicsRigidBodyAPI); EXPECT_PRIM_API_APPLIED(stage, "/test/body_1/body_1_col_1", pxr::UsdPhysicsCollisionAPI); + EXPECT_PRIM_API_APPLIED(stage, "/test/body_1/body_1_col_1", + pxr::MjcPhysicsCollisionAPI); // body_2 [rigidbody] EXPECT_PRIM_VALID(stage, "/test/body_2"); @@ -1169,11 +1183,117 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsColliders) { pxr::UsdPhysicsCollisionAPI); EXPECT_PRIM_API_APPLIED(stage, "/test/body_3/body_3_col/Mesh", pxr::UsdPhysicsMeshCollisionAPI); + EXPECT_PRIM_API_APPLIED(stage, "/test/body_3/body_3_col/Mesh", + pxr::MjcPhysicsMeshCollisionAPI); ExpectAttributeEqual(stage, "/test/body_3/body_3_col/Mesh.physics:approximation", pxr::UsdPhysicsTokens->convexHull); } +TEST_F(MjcfSdfFileFormatPluginTest, TestMjcPhysicsCollisionAPI) { + static constexpr char xml[] = R"( + + + + + + + + )"; + auto stage = OpenStageWithPhysics(xml); + + ExpectAttributeEqual(stage, "/test/body/box.mjc:shellinertia", true); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestMjcPhysicsMeshCollisionAPI) { + static constexpr char xml[] = R"( + + + + + + + + + + + + + + + + + )"; + auto stage = OpenStageWithPhysics(xml); + + ExpectAttributeEqual(stage, "/test/body/tet_legacy/Mesh.mjc:inertia", + MjcPhysicsTokens->legacy); + ExpectAttributeEqual(stage, "/test/body/tet_exact/Mesh.mjc:inertia", + MjcPhysicsTokens->exact); + ExpectAttributeEqual(stage, "/test/body/tet_convex/Mesh.mjc:inertia", + MjcPhysicsTokens->convex); + ExpectAttributeEqual(stage, "/test/body/tet_shell/Mesh.mjc:inertia", + MjcPhysicsTokens->shell); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestMassAPIApplied) { + static constexpr char xml[] = R"( + + + + + + + + )"; + auto stage = OpenStageWithPhysics(xml); + + EXPECT_PRIM_VALID(stage, "/test/body"); + EXPECT_PRIM_VALID(stage, "/test/body/box"); + EXPECT_PRIM_API_APPLIED(stage, "/test/body/box", pxr::UsdPhysicsMassAPI); + EXPECT_PRIM_API_NOT_APPLIED(stage, "/test/body", pxr::UsdPhysicsMassAPI); + ExpectAttributeEqual(stage, "/test/body/box.physics:mass", 0.1f); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestMassAPIAppliedToBody) { + static constexpr char xml[] = R"( + + + + + + + + + )"; + auto stage = OpenStageWithPhysics(xml); + + EXPECT_PRIM_VALID(stage, "/test/body"); + EXPECT_PRIM_VALID(stage, "/test/body/box"); + EXPECT_PRIM_API_APPLIED(stage, "/test/body/box", pxr::UsdPhysicsMassAPI); + EXPECT_PRIM_API_APPLIED(stage, "/test/body", pxr::UsdPhysicsMassAPI); + // Make sure that body gets it's inertial elements from the inertial element + // and not from the subtree. + ExpectAttributeEqual(stage, "/test/body.physics:mass", 3.0f); + ExpectAttributeEqual(stage, "/test/body.physics:centerOfMass", + pxr::GfVec3f(1, 2, 3)); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestMassAPIDensity) { + static constexpr char xml[] = R"( + + + + + + + + )"; + auto stage = OpenStageWithPhysics(xml); + + ExpectAttributeEqual(stage, "/test/body/box.physics:density", 1234.0f); +} + } // namespace } // namespace usd } // namespace mujoco diff --git a/test/experimental/usd/test_utils.cc b/test/experimental/usd/test_utils.cc index a4e93240..8b046793 100644 --- a/test/experimental/usd/test_utils.cc +++ b/test/experimental/usd/test_utils.cc @@ -42,6 +42,15 @@ pxr::SdfLayerRefPtr LoadLayer( return layer; } +pxr::UsdStageRefPtr OpenStageWithPhysics(const std::string& xml) { + pxr::SdfFileFormat::FileFormatArguments args; + args["usdMjcfToggleUsdPhysics"] = "true"; + pxr::SdfLayerRefPtr layer = LoadLayer(xml, args); + auto stage = pxr::UsdStage::Open(layer); + EXPECT_THAT(stage, testing::NotNull()); + return stage; +} + template <> void ExpectAttributeEqual(pxr::UsdStageRefPtr stage, pxr::SdfPath path, diff --git a/test/experimental/usd/test_utils.h b/test/experimental/usd/test_utils.h index 69075ab3..655f06c9 100644 --- a/test/experimental/usd/test_utils.h +++ b/test/experimental/usd/test_utils.h @@ -73,6 +73,8 @@ pxr::SdfLayerRefPtr LoadLayer( const std::string& xml, const pxr::SdfFileFormat::FileFormatArguments& args = {}); +pxr::UsdStageRefPtr OpenStageWithPhysics(const std::string& xml); + template void ExpectAttributeEqual(pxr::UsdStageRefPtr stage, pxr::SdfPath path, const T& value) { From 568620dd2f0f661dae2baafe3c5dee6d1d9f88ae Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Tue, 27 May 2025 05:03:04 -0700 Subject: [PATCH 176/191] Add `texture` attribute to lights. PiperOrigin-RevId: 763739805 Change-Id: I333ed2ac68f30f79f688ca01d7b5fd6bf2d836c9 --- doc/XMLreference.rst | 5 +++++ doc/XMLschema.rst | 2 +- doc/includes/references.h | 3 +++ include/mujoco/mjmodel.h | 1 + include/mujoco/mjspec.h | 1 + include/mujoco/mjvisualize.h | 1 + include/mujoco/mjxmacro.h | 1 + python/mujoco/introspect/structs.py | 20 ++++++++++++++++++++ python/mujoco/structs.cc | 1 + src/engine/engine_vis_visualize.c | 5 +++++ src/user/user_model.cc | 1 + src/user/user_objects.cc | 18 ++++++++++++++++++ src/user/user_objects.h | 4 ++++ src/xml/xml_native_reader.cc | 9 ++++++--- src/xml/xml_native_writer.cc | 1 + test/user/user_objects_test.cc | 23 +++++++++++++++++++++++ test/xml/testdata/lights.xml | 12 ++++++++++++ unity/Runtime/Bindings/MjBindings.cs | 2 ++ 18 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 test/xml/testdata/lights.xml diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index e276c831..2e716ca5 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -2961,6 +2961,11 @@ Attributes may be applied or ignored depending on the lighting model being used. The color of the light. For the Phong (default) lighting model, this defines the diffuse color of the light. +.. _body-light-texture: + +:at:`texture`: :at-val:`string, optional` + The texture to use for image-based lighting. This is unused by the default Phong lighting model. + .. _body-light-intensity: :at:`intensity`: :at-val:`real, "1000.0"` diff --git a/doc/XMLschema.rst b/doc/XMLschema.rst index a95e5211..21272c90 100644 --- a/doc/XMLschema.rst +++ b/doc/XMLschema.rst @@ -321,7 +321,7 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | | | | | :ref:`cutoff` | :ref:`exponent` | :ref:`ambient` | :ref:`diffuse` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`specular` | :ref:`mode` | :ref:`target` | | | +| | | | :ref:`specular` | :ref:`mode` | :ref:`target` | :ref:`texture` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| body |br| |_| |L| | | .. table:: | diff --git a/doc/includes/references.h b/doc/includes/references.h index fc9943c1..92aae862 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -1186,6 +1186,7 @@ struct mjModel_ { int* light_bodyid; // id of light's body (nlight x 1) int* light_targetbodyid; // id of targeted body; -1: none (nlight x 1) int* light_type; // spot, directional, etc. (mjtLightType) (nlight x 1) + int* light_texid; // texture id for image lights (nlight x 1) mjtByte* light_castshadow; // does light cast shadows (nlight x 1) float* light_bulbradius; // light radius for soft shadows (nlight x 1) float* light_intensity; // intensity, in candela (nlight x 1) @@ -2043,6 +2044,7 @@ typedef struct mjsLight_ { // light specification // intrinsics mjtByte active; // is light active mjtLightType type; // type of light + mjString* texture; // texture name for image lights mjtByte castshadow; // does light cast shadows float bulbradius; // bulb radius, for soft shadows float intensity; // intensity, in candelas @@ -2847,6 +2849,7 @@ struct mjvLight_ { // OpenGL light float pos[3]; // position rel. to body frame float dir[3]; // direction rel. to body frame int type; // type (mjtLightType) + int texid; // texture id for image lights float attenuation[3]; // OpenGL attenuation (quadratic model) float cutoff; // OpenGL cutoff float exponent; // OpenGL exponent diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index 8c50af50..75eb3cd0 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -858,6 +858,7 @@ struct mjModel_ { int* light_bodyid; // id of light's body (nlight x 1) int* light_targetbodyid; // id of targeted body; -1: none (nlight x 1) int* light_type; // spot, directional, etc. (mjtLightType) (nlight x 1) + int* light_texid; // texture id for image lights (nlight x 1) mjtByte* light_castshadow; // does light cast shadows (nlight x 1) float* light_bulbradius; // light radius for soft shadows (nlight x 1) float* light_intensity; // intensity, in candela (nlight x 1) diff --git a/include/mujoco/mjspec.h b/include/mujoco/mjspec.h index c422eedd..3c00f478 100644 --- a/include/mujoco/mjspec.h +++ b/include/mujoco/mjspec.h @@ -395,6 +395,7 @@ typedef struct mjsLight_ { // light specification // intrinsics mjtByte active; // is light active mjtLightType type; // type of light + mjString* texture; // texture name for image lights mjtByte castshadow; // does light cast shadows float bulbradius; // bulb radius, for soft shadows float intensity; // intensity, in candelas diff --git a/include/mujoco/mjvisualize.h b/include/mujoco/mjvisualize.h index 5903ba59..efff4d03 100644 --- a/include/mujoco/mjvisualize.h +++ b/include/mujoco/mjvisualize.h @@ -262,6 +262,7 @@ struct mjvLight_ { // OpenGL light float pos[3]; // position rel. to body frame float dir[3]; // direction rel. to body frame int type; // type (mjtLightType) + int texid; // texture id for image lights float attenuation[3]; // OpenGL attenuation (quadratic model) float cutoff; // OpenGL cutoff float exponent; // OpenGL exponent diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index 3a8a6fcf..93542a88 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -300,6 +300,7 @@ X ( int, light_bodyid, nlight, 1 ) \ X ( int, light_targetbodyid, nlight, 1 ) \ X ( int, light_type, nlight, 1 ) \ + X ( int, light_texid, nlight, 1 ) \ X ( mjtByte, light_castshadow, nlight, 1 ) \ X ( float, light_bulbradius, nlight, 1 ) \ X ( float, light_intensity, nlight, 1 ) \ diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index 283a158a..49b8752b 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -2227,6 +2227,14 @@ STRUCTS: Mapping[str, StructDecl] = dict([ doc='spot, directional, etc. (mjtLightType)', array_extent=('nlight',), ), + StructFieldDecl( + name='light_texid', + type=PointerType( + inner_type=ValueType(name='int'), + ), + doc='texture id for image lights', + array_extent=('nlight',), + ), StructFieldDecl( name='light_castshadow', type=PointerType( @@ -6702,6 +6710,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='int'), doc='type (mjtLightType)', ), + StructFieldDecl( + name='texid', + type=ValueType(name='int'), + doc='texture id for image lights', + ), StructFieldDecl( name='attenuation', type=ArrayType( @@ -8996,6 +9009,13 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='mjtLightType'), doc='type of light', ), + StructFieldDecl( + name='texture', + type=PointerType( + inner_type=ValueType(name='mjString'), + ), + doc='texture name for image lights', + ), StructFieldDecl( name='castshadow', type=ValueType(name='mjtByte'), diff --git a/python/mujoco/structs.cc b/python/mujoco/structs.cc index dc98e3dd..1b92c61a 100644 --- a/python/mujoco/structs.cc +++ b/python/mujoco/structs.cc @@ -1074,6 +1074,7 @@ This is useful for example when the MJB is not available as a file on disk.)")); X(exponent); X(headlight); X(type); + X(texid); X(castshadow); X(bulbradius); X(intensity); diff --git a/src/engine/engine_vis_visualize.c b/src/engine/engine_vis_visualize.c index 6f9364f1..d0d57b3f 100644 --- a/src/engine/engine_vis_visualize.c +++ b/src/engine/engine_vis_visualize.c @@ -2143,8 +2143,12 @@ void mjv_makeLights(const mjModel* m, const mjData* d, mjvScene* scn) { // set default properties memset(thislight, 0, sizeof(mjvLight)); thislight->headlight = 1; + thislight->texid = -1; thislight->type = mjLIGHT_DIRECTIONAL; thislight->castshadow = 0; + thislight->bulbradius = 0.02; + thislight->intensity = 1000; + thislight->range = 10; // compute head position and gaze direction in model space mjtNum hpos[3], hfwd[3]; @@ -2170,6 +2174,7 @@ void mjv_makeLights(const mjModel* m, const mjData* d, mjvScene* scn) { // copy properties memset(thislight, 0, sizeof(mjvLight)); thislight->type = m->light_type[i]; + thislight->texid = m->light_texid[i]; thislight->castshadow = m->light_castshadow[i]; thislight->bulbradius = m->light_bulbradius[i]; thislight->intensity = m->light_intensity[i]; diff --git a/src/user/user_model.cc b/src/user/user_model.cc index d7f862eb..e3d4544a 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -2670,6 +2670,7 @@ void mjCModel::CopyTree(mjModel* m) { m->light_mode[lid] = (int)pl->mode; m->light_targetbodyid[lid] = pl->targetbodyid; m->light_type[lid] = pl->type; + m->light_texid[lid] = pl->texid; m->light_castshadow[lid] = (mjtByte)pl->castshadow; m->light_active[lid] = (mjtByte)pl->active; mjuu_copyvec(m->light_pos+3*lid, pl->pos, 3); diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 688c9729..80d57a8d 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -3553,7 +3553,10 @@ mjCLight::mjCLight(mjCModel* _model, mjCDef* _def) { // clear private variables body = 0; targetbodyid = -1; + texid = -1; spec_targetbody_.clear(); + spec_texture_.clear(); + // reset to default if given if (_def) { @@ -3593,6 +3596,7 @@ void mjCLight::PointToLocal() { spec.element = static_cast(this); spec.name = &name; spec.targetbody = &spec_targetbody_; + spec.texture = &spec_texture_; spec.info = &info; targetbody = nullptr; } @@ -3604,6 +3608,9 @@ void mjCLight::NameSpace(const mjCModel* m) { if (!spec_targetbody_.empty()) { spec_targetbody_ = m->prefix + spec_targetbody_ + m->suffix; } + if (!spec_texture_.empty()) { + spec_texture_ = m->prefix + spec_texture_ + m->suffix; + } } @@ -3611,6 +3618,7 @@ void mjCLight::NameSpace(const mjCModel* m) { void mjCLight::CopyFromSpec() { *static_cast(this) = spec; targetbody_ = spec_targetbody_; + texture_ = spec_texture_; } @@ -3643,6 +3651,16 @@ void mjCLight::Compile(void) { throw mjCError(this, "unknown target body in light"); } } + + // get texture + if (!texture_.empty()) { + mjCTexture* tex = (mjCTexture*)model->FindObject(mjOBJ_TEXTURE, texture_); + if (tex) { + texid = tex->id; + } else { + throw mjCError(this, "unknown target body in light"); + } + } } diff --git a/src/user/user_objects.h b/src/user/user_objects.h index db01349a..9c150646 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -793,6 +793,9 @@ class mjCLight_ : public mjCBase { protected: mjCBody* body; // light's body int targetbodyid; // id of target body; -1: none + int texid; // id of texture; -1: none + std::string texture_; + std::string spec_texture_; std::string targetbody_; std::string spec_targetbody_; }; @@ -814,6 +817,7 @@ class mjCLight : public mjCLight_, private mjsLight { // used by mjXWriter and mjCModel const std::string& get_targetbody() const { return targetbody_; } + const std::string& get_texture() const { return texture_; } void SetParent(mjCBody* _body) { body = _body; } mjCBody* GetParent() const { return body; } diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 02908ed2..c5ad9c8f 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -281,9 +281,9 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { {"camera", "*", "20", "name", "class", "orthographic", "fovy", "ipd", "resolution", "pos", "quat", "axisangle", "xyaxes", "zaxis", "euler", "mode", "target", "focal", "focalpixel", "principal", "principalpixel", "sensorsize", "user"}, - {"light", "*", "19", "name", "class", "directional", "type", "castshadow", "active", + {"light", "*", "20", "name", "class", "directional", "type", "castshadow", "active", "pos", "dir", "bulbradius", "intensity", "range", "attenuation", "cutoff", - "exponent", "ambient", "diffuse", "specular", "mode", "target"}, + "exponent", "ambient", "diffuse", "specular", "mode", "target", "texture"}, {"plugin", "*", "2", "plugin", "instance"}, {"<"}, {"config", "*", "2", "key", "value"}, @@ -1864,12 +1864,15 @@ void mjXReader::OneCamera(XMLElement* elem, mjsCamera* camera) { void mjXReader::OneLight(XMLElement* elem, mjsLight* light) { int n; bool has_directional = false; - string text, name, targetbody; + string text, name, texture, targetbody; // read attributes if (ReadAttrTxt(elem, "name", name)) { mjs_setString(light->name, name.c_str()); } + if (ReadAttrTxt(elem, "texture", texture)) { + mjs_setString(light->texture, texture.c_str()); + } if (ReadAttrTxt(elem, "target", targetbody)) { mjs_setString(light->targetbody, targetbody.c_str()); } diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index 9f9a1b9d..f28041a8 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -608,6 +608,7 @@ void mjXWriter::OneLight(XMLElement* elem, const mjCLight* light, mjCDef* def, WriteAttr(elem, "intensity", 1, &light->intensity, &def->Light().intensity); WriteAttr(elem, "range", 1, &light->range, &def->Light().range); WriteAttrKey(elem, "type", lighttype_map, lighttype_sz, light->type, def->Light().type); + WriteAttrTxt(elem, "texture", light->get_texture()); WriteAttrKey(elem, "castshadow", bool_map, 2, light->castshadow, def->Light().castshadow); WriteAttrKey(elem, "active", bool_map, 2, light->active, def->Light().active); WriteAttr(elem, "attenuation", 3, light->attenuation, def->Light().attenuation); diff --git a/test/user/user_objects_test.cc b/test/user/user_objects_test.cc index 37bec917..396b2ea0 100644 --- a/test/user/user_objects_test.cc +++ b/test/user/user_objects_test.cc @@ -2294,6 +2294,29 @@ TEST_F(UserObjectsTest, FrameTransformsLight) { mj_deleteModel(m); } +TEST_F(ContentTypeTest, ImageLightsReferenceTexture) { + static constexpr char xml[] = R"( + + + + + + + + + + )"; + + std::array error; + mjModel* m = LoadModelFromString(xml, error.data(), error.size()); + EXPECT_THAT(m, NotNull()); + EXPECT_EQ(m->ntex, 1); + EXPECT_EQ(m->nlight, 1); + EXPECT_THAT(m->light_texid[0], 0); + mj_deleteModel(m); +} + // ------------- test bvh ------------------------------------------------------ TEST_F(UserObjectsTest, RobustBVH) { diff --git a/test/xml/testdata/lights.xml b/test/xml/testdata/lights.xml new file mode 100644 index 00000000..7937e1e7 --- /dev/null +++ b/test/xml/testdata/lights.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index d15803a7..11698ed6 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -5441,6 +5441,7 @@ public unsafe struct mjModel_ { public int* light_bodyid; public int* light_targetbodyid; public int* light_type; + public int* light_texid; public byte* light_castshadow; public float* light_bulbradius; public float* light_intensity; @@ -6082,6 +6083,7 @@ public unsafe struct mjvLight_ { public fixed float pos[3]; public fixed float dir[3]; public int type; + public int texid; public fixed float attenuation[3]; public float cutoff; public float exponent; From 1fbbfd8079411519f9e60dd31437c178604034bc Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Wed, 28 May 2025 03:46:54 -0700 Subject: [PATCH 177/191] Correct `frame` property in MjsBody from method to read-only attribute. PiperOrigin-RevId: 764188923 Change-Id: I7c753f7f431b79aa320681c45b7614f68b63852c --- python/mujoco/specs.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/mujoco/specs.cc b/python/mujoco/specs.cc index 3ec74da8..51846d2a 100644 --- a/python/mujoco/specs.cc +++ b/python/mujoco/specs.cc @@ -750,7 +750,7 @@ PYBIND11_MODULE(_specs, m) { return frame; }, py::return_value_policy::reference_internal); - mjsBody.def( + mjsBody.def_property_readonly( "frame", [](raw::MjsBody* self) -> raw::MjsFrame* { return mjs_getFrame(self->element); From 56b88b59f05e9a8d6ec8631cfc0fa1fb10673cb1 Mon Sep 17 00:00:00 2001 From: Kyle Bayes Date: Wed, 28 May 2025 04:18:01 -0700 Subject: [PATCH 178/191] Support 0-simplex in GJK code, and refactor code for efficiency and readability. PiperOrigin-RevId: 764197889 Change-Id: Ie0d57474060a34a6b908be0386094f442cda448a --- src/engine/engine_collision_gjk.c | 108 +++++++++++++---------- test/engine/engine_collision_gjk_test.cc | 2 +- 2 files changed, 61 insertions(+), 49 deletions(-) diff --git a/src/engine/engine_collision_gjk.c b/src/engine/engine_collision_gjk.c index 268de4c2..a0a76004 100644 --- a/src/engine/engine_collision_gjk.c +++ b/src/engine/engine_collision_gjk.c @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -30,11 +31,10 @@ // subdistance algorithm for GJK that computes the barycentric coordinates of the point in a // simplex closest to the origin // implementation adapted from Montanari et al, ToG 2017 -static void subdistance(mjtNum lambda[4], int n, const mjtNum s1[3], const mjtNum s2[3], - const mjtNum s3[3], const mjtNum s4[3]); +static void subdistance(mjtNum lambda[4], int n, const Vertex simplex[4]); // compute the barycentric coordinates of the closest point to the origin in the n-simplex, -// where n = 3, 2, 1 respectively +// for n = 3, 2, 1 respectively static void S3D(mjtNum lambda[4], const mjtNum s1[3], const mjtNum s2[3], const mjtNum s3[3], const mjtNum s4[3]); static void S2D(mjtNum lambda[3], const mjtNum s1[3], const mjtNum s2[3], const mjtNum s3[3]); @@ -42,7 +42,7 @@ static void S1D(mjtNum lambda[2], const mjtNum s1[3], const mjtNum s2[3]); // compute the support point for GJK static void gjkSupport(Vertex* v, mjCCDObj* obj1, mjCCDObj* obj2, - const mjtNum x_k[3]); + const mjtNum x_k[3], mjtNum x_norm); // compute the linear combination of 1 - 4 3D vectors static inline void lincomb(mjtNum res[3], const mjtNum* coef, int n, const mjtNum v1[3], @@ -182,21 +182,27 @@ static void gjk(mjCCDStatus* status, mjCCDObj* obj1, mjCCDObj* obj2) { mjtNum cutoff2 = status->dist_cutoff * status->dist_cutoff; // if both geoms are discrete, finite convergence is guaranteed; set tolerance to 0 - mjtNum epsilon = discreteGeoms(obj1, obj2) ? 0 : status->tolerance * status->tolerance; + mjtNum epsilon = discreteGeoms(obj1, obj2) ? 0 : 0.5 * status->tolerance * status->tolerance; + mjtNum x_norm; // set initial guess sub3(x_k, x1_k, x2_k); for (; k < kmax; k++) { // compute the kth support point - gjkSupport(simplex + n, obj1, obj2, x_k); + x_norm = dot3(x_k, x_k); + if (x_norm < mjMINVAL2) { + break; + } + x_norm = mju_sqrt(x_norm); + gjkSupport(simplex + n, obj1, obj2, x_k, x_norm); mjtNum *s_k = simplex[n].vert; // stopping criteria using the Frank-Wolfe duality gap given by // |f(x_k) - f(x_min)|^2 <= < grad f(x_k), (x_k - s_k) > mjtNum diff[3]; sub3(diff, x_k, s_k); - if (2*dot3(x_k, diff) < epsilon) { + if (dot3(x_k, diff) < epsilon) { if (!k) n = 1; break; } @@ -238,12 +244,12 @@ static void gjk(mjCCDStatus* status, mjCCDObj* obj1, mjCCDObj* obj2) { // run the distance subalgorithm to compute the barycentric coordinates // of the closest point to the origin in the simplex - subdistance(lambda, n + 1, simplex[0].vert, simplex[1].vert, simplex[2].vert, simplex[3].vert); + subdistance(lambda, n + 1, simplex); // remove vertices from the simplex no longer needed n = 0; for (int i = 0; i < 4; i++) { - if (lambda[i] == 0) continue; + if (!lambda[i]) continue; simplex[n] = simplex[i]; lambda[n++] = lambda[i]; } @@ -285,7 +291,7 @@ static void gjk(mjCCDStatus* status, mjCCDObj* obj1, mjCCDObj* obj2) { status->nx = 1; status->gjk_iterations = k; status->nsimplex = n; - status->dist = norm3(x_k); + status->dist = x_norm; } @@ -322,17 +328,13 @@ static inline void support(Vertex* v, mjCCDObj* obj1, mjCCDObj* obj2, // compute the support points in obj1 and obj2 for the kth approximation point -static void gjkSupport(Vertex* v, mjCCDObj* obj1, mjCCDObj* obj2, - const mjtNum x_k[3]) { - mjtNum dir[3] = {-1, 0, 0}, dir_neg[3] = {1, 0, 0}; +static inline void gjkSupport(Vertex* v, mjCCDObj* obj1, mjCCDObj* obj2, + const mjtNum x_k[3], mjtNum x_norm) { + mjtNum dir[3], dir_neg[3]; // mjc_support requires a normalized direction - mjtNum norm = dot3(x_k, x_k); - if (norm > mjMINVAL2) { - norm = 1/mju_sqrt(norm); - scl3(dir_neg, x_k, norm); - scl3(dir, dir_neg, -1); - } + scl3(dir_neg, x_k, 1 / x_norm); + scl3(dir, dir_neg, -1); support(v, obj1, obj2, dir, dir_neg); } @@ -537,24 +539,33 @@ static inline int sameSign2(mjtNum a, mjtNum b) { // subdistance algorithm for GJK that computes the barycentric coordinates of the point in a // simplex closest to the origin // implementation adapted from Montanari et al, ToG 2017 -static inline void subdistance(mjtNum lambda[4], int n, const mjtNum s1[3], - const mjtNum s2[3], const mjtNum s3[3], const mjtNum s4[3]) { - lambda[0] = lambda[1] = lambda[2] = lambda[3] = 0; - if (n == 4) { - S3D(lambda, s1, s2, s3, s4); - } else if (n == 3) { - S2D(lambda, s1, s2, s3); - } else if (n == 2) { - S1D(lambda, s1, s2); - } else { +static inline void subdistance(mjtNum lambda[4], int n, const Vertex simplex[4]) { + memset(lambda, 0, 4 * sizeof(mjtNum)); + const mjtNum* s1 = simplex[0].vert; + const mjtNum* s2 = simplex[1].vert; + const mjtNum* s3 = simplex[2].vert; + const mjtNum* s4 = simplex[3].vert; + + switch (n) { + case 4: + S3D(lambda, s1, s2, s3, s4); + break; + case 3: + S2D(lambda, s1, s2, s3); + break; + case 2: + S1D(lambda, s1, s2); + break; + default: lambda[0] = 1; + break; } } -static void S3D(mjtNum lambda[4], const mjtNum s1[3], const mjtNum s2[3], const mjtNum s3[3], - const mjtNum s4[3]) { +static void S3D(mjtNum lambda[4], const mjtNum s1[3], const mjtNum s2[3], + const mjtNum s3[3], const mjtNum s4[3]) { // the matrix M is given by // [[ s1_x, s2_x, s3_x, s4_x ], // [ s1_y, s2_y, s3_y, s4_y ], @@ -639,7 +650,6 @@ static void S3D(mjtNum lambda[4], const mjtNum s1[3], const mjtNum s2[3], const lambda[0] = lambda_2d[0]; lambda[1] = lambda_2d[1]; lambda[2] = lambda_2d[2]; - lambda[3] = 0; } } } @@ -786,27 +796,29 @@ static void S1D(mjtNum lambda[2], const mjtNum s1[3], const mjtNum s2[3]) { projectOriginLine(p_o, s1, s2); // find the axis with the largest projection "shadow" of the simplex - mjtNum mu_max = 0; - int index; - for (int i = 0; i < 3; i++) { - mjtNum mu = s1[i] - s2[i]; - if (mju_abs(mu) >= mju_abs(mu_max)) { - mu_max = mu; - index = i; - } + mjtNum mu = s1[0] - s2[0]; + mjtNum mu_max = mu; + int index = 0; + + mu = s1[1] - s2[1]; + if (mju_abs(mu) >= mju_abs(mu_max)) { + mu_max = mu; + index = 1; + } + + mu = s1[2] - s2[2]; + if (mju_abs(mu) >= mju_abs(mu_max)) { + mu_max = mu; + index = 2; } mjtNum C1 = p_o[index] - s2[index]; mjtNum C2 = s1[index] - p_o[index]; - // inside the simplex - if (sameSign2(mu_max, C1) && sameSign2(mu_max, C2)) { - lambda[0] = C1 / mu_max; - lambda[1] = C2 / mu_max; - } else { - lambda[0] = 0; - lambda[1] = 1; - } + // determine if projection of origin lies inside 1-simplex + int same = sameSign2(mu_max, C1) && sameSign2(mu_max, C2); + lambda[0] = same ? C1 / mu_max : 0; + lambda[1] = same ? C2 / mu_max : 1; } diff --git a/test/engine/engine_collision_gjk_test.cc b/test/engine/engine_collision_gjk_test.cc index 9237aef4..9e5fa5e1 100644 --- a/test/engine/engine_collision_gjk_test.cc +++ b/test/engine/engine_collision_gjk_test.cc @@ -468,7 +468,7 @@ TEST_F(MjGjkTest, BoxBoxTouching) { int ncons = Penetration(status, dist, dir, pos, model, data, geom1, geom2); EXPECT_EQ(ncons, 0); - EXPECT_GT(status.epa_status, 0); + EXPECT_EQ(status.epa_status, -1); mj_deleteData(data); mj_deleteModel(model); From 3a4b6e6c5bbf14963e77ebb9ca1fa80f74416b27 Mon Sep 17 00:00:00 2001 From: Tom Power Date: Wed, 28 May 2025 06:12:22 -0700 Subject: [PATCH 179/191] Added terrain generation to mjspec tutorial PiperOrigin-RevId: 764229337 Change-Id: Ic541eefc4b561d3c39c5ee99c7d38466d7e75903 --- doc/changelog.rst | 9 + .../procedural_terrain_generation.png | Bin 0 -> 85112 bytes python/mjspec.ipynb | 844 +++++++++++++++++- 3 files changed, 848 insertions(+), 5 deletions(-) create mode 100644 doc/images/changelog/procedural_terrain_generation.png diff --git a/doc/changelog.rst b/doc/changelog.rst index 8f5135b6..b425416f 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -24,6 +24,15 @@ Simulate ``mjData`` when the Python viewer is used in passive mode. This functionality is now provided by :ref:`mjv_copyModel` and :ref:`mjv_copyData`, which don't copy arrays which are not required for visualization. +.. image:: images/changelog/procedural_terrain_generation.png + :width: 25% + :align: right + +Python bindings +^^^^^^^^^^^^^^^ + +- Added examples of procedural terrain generation to the Model Editing tutorial: |mjspec_colab| + Version 3.3.2 (April 28, 2025) ------------------------------ diff --git a/doc/images/changelog/procedural_terrain_generation.png b/doc/images/changelog/procedural_terrain_generation.png new file mode 100644 index 0000000000000000000000000000000000000000..7c083dff7513baa769fa2e357b825a13acfae0e8 GIT binary patch literal 85112 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

UyjZpXqs%K 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

`vaj`q7MGH+B5Z+TyuB%sljU90;w(-vD}5rB zL!k^%{KIMsjZxlA4KEK{h8kCSROC@0JqsOF&`S@`Ry)c#o zL!ftFzlaG4d!^0$2Q*dc8#SlZc>%!YqNdcfZtIAyCYD_}z=0!z#VtyZ5P}GV0wG8U zZ};((_N5Q0CY24LI`2^g>xC zj%WEmj2Y*GY@FzEWzKi2!b>Rn%7 zwrEb>f<+A-2l^j;^pogdV%qd7Gh_Yg{`dAA9H^}bG&h$H^~8e#cgU^&?r-m}{pe72 zx&N%wCZ{vz&W%TtNn_^J%9Tqeop$nsk&)D))}bkr%cFhqiBl^lw$ucCs+~5H8MCY| z^5lO$dw0$5x~gEfCcJBV=bD$duU~iIOV=#?+RZ1wv1ZS@clRGTJhcgDVCvK=g+$|B8F>z&NU_Z}^s(+1}KsSOF&NvS z6M7GX7D^xqgkF-+LNB5BZm_XwHa56dxyhE*yR_2wopSGa|Crg`S>Yt`ywCT3OVO4) z{ho5q`JLa9GOUOtTJiuYo?NMov+inYFFC)`X1C~g32G7LVsWrq#LM$t^K)}YX6XTQ z1c-{%k>fy`-qJbIWioLjPSLI{&Hm3@n|l3`$@bi_IR+>iA*^sqiK~81L+rCQ+wnP~ zT{NnxFd`UrL_>o$l5}pdb554qYa<4^5{OPq#;E*~8AU~>7KbWDyU!}xM5BQNHpy)= zo(zf4fJ}U|W{SLoh^9EL%<-C}q2`9~ZIbeIz=#p?oi=sYUH7k=I=&<;$Ias!A$nb* z`FtQ9Mv|1_B)&4GXarq1$SR*VKIgMd-FBO>?QmdrO~znE&2)%4U~AL%y&YxM1z4b(#t6YkB44cBW3`Fzy?h}H$x3gyd{~y`vE6;|%B&pc z1CO170x|zU^vS2!g~IZjS;NQ9s?-Lfh{M5y-6f@&h~VNPFCs+g331K|C3!`@J=

rH8*iMi_oTC0#n;~5wt0Kg^Zz(= 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 T-\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \"\"\"\n", + "\n", + " spec = mj.MjSpec.from_string(arena_xml)\n", + " main = spec.default\n", + " main.geom.type = mj.mjtGeom.mjGEOM_BOX\n", + "\n", + " name = 'base_tile'\n", + "\n", + " spec.worldbody.add_body(pos=[-3, 0, 0], name=name)\n", + " if direction:\n", + " tile_func(spec, direction=direction)\n", + " else:\n", + " tile_func(spec)\n", + "\n", + " model = spec.compile()\n", + " data = mj.MjData(model)\n", + "\n", + " cam = mj.MjvCamera()\n", + " mj.mjv_defaultCamera(cam)\n", + " cam.lookat = [0, 0, 0]\n", + " cam.distance = cam_distance\n", + " cam.elevation = cam_elevation\n", + "\n", + " height = 300\n", + "\n", + " with mj.Renderer(model, 480, 640) as renderer:\n", + " mj.mj_forward(model, data)\n", + " renderer.update_scene(data,cam)\n", + " media.show_image(renderer.render(), height=height)\n", + "\n", + "\n", + "def interpolant(t):\n", + " return t*t*t*(t*(t*6 - 15) + 10)\n", + "\n", + "def perlin(shape, res, tileable=(False, False), interpolant=interpolant):\n", + " \"\"\"Generate a 2D numpy array of perlin noise.\n", + "\n", + " Args:\n", + " shape: The shape of the generated array (tuple of two ints).\n", + " This must be a multiple of res.\n", + " res: The number of periods of noise to generate along each\n", + " axis (tuple of two ints). Note shape must be a multiple of\n", + " res.\n", + " tileable: If the noise should be tileable along each axis\n", + " (tuple of two bools). Defaults to (False, False).\n", + " interpolant: The interpolation function, defaults to\n", + " t*t*t*(t*(t*6 - 15) + 10).\n", + "\n", + " Returns:\n", + " A numpy array of shape shape with the generated noise.\n", + "\n", + " Raises:\n", + " ValueError: If shape is not a multiple of res.\n", + " \"\"\"\n", + " delta = (res[0] / shape[0], res[1] / shape[1])\n", + " d = (shape[0] // res[0], shape[1] // res[1])\n", + " grid = np.mgrid[0:res[0]:delta[0], 0:res[1]:delta[1]]\\\n", + " .transpose(1, 2, 0) % 1\n", + " # Gradients\n", + " angles = 2*np.pi*np.random.rand(res[0]+1, res[1]+1)\n", + " gradients = np.dstack((np.cos(angles), np.sin(angles)))\n", + " if tileable[0]:\n", + " gradients[-1,:] = gradients[0,:]\n", + " if tileable[1]:\n", + " gradients[:,-1] = gradients[:,0]\n", + " gradients = gradients.repeat(d[0], 0).repeat(d[1], 1)\n", + " g00 = gradients[ :-d[0], :-d[1]]\n", + " g10 = gradients[d[0]: , :-d[1]]\n", + " g01 = gradients[ :-d[0],d[1]: ]\n", + " g11 = gradients[d[0]: ,d[1]: ]\n", + " # Ramps\n", + " n00 = np.sum(np.dstack((grid[:,:,0] , grid[:,:,1] )) * g00, 2)\n", + " n10 = np.sum(np.dstack((grid[:,:,0]-1, grid[:,:,1] )) * g10, 2)\n", + " n01 = np.sum(np.dstack((grid[:,:,0] , grid[:,:,1]-1)) * g01, 2)\n", + " n11 = np.sum(np.dstack((grid[:,:,0]-1, grid[:,:,1]-1)) * g11, 2)\n", + " # Interpolation\n", + " t = interpolant(grid)\n", + " n0 = n00*(1-t[:,:,0]) + t[:,:,0]*n10\n", + " n1 = n01*(1-t[:,:,0]) + t[:,:,0]*n11\n", + " return np.sqrt(2)*((1-t[:,:,1])*n0 + t[:,:,1]*n1)\n", + "\n", + "def edge_slope(size, border_width=5, blur_iterations=20):\n", + " \"\"\"Creates a grayscale image with a white center and fading black edges using convolution.\"\"\"\n", + " img = np.ones((size, size), dtype=np.float32)\n", + " img[:border_width, :] = 0\n", + " img[-border_width:, :] = 0\n", + " img[:, :border_width] = 0\n", + " img[:, -border_width:] = 0\n", + "\n", + " kernel = np.array([[1, 1, 1],\n", + " [1, 1, 1],\n", + " [1, 1, 1]]) / 9.0\n", + "\n", + " for _ in range(blur_iterations):\n", + " img = convolve2d(img, kernel, mode='same', boundary='symm')\n", + "\n", + " return img" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "dtFWSSwWSgeD" + }, + "outputs": [], + "source": [ + "# @title Stairs\n", + "def stairs(spec=None, grid_loc=[0, 0] , num_stairs=4, direction=1, name='stair'):\n", + " SQUARE_LENGTH = 2\n", + " V_SIZE = 0.076\n", + " H_SIZE = 0.12\n", + " H_STEP = H_SIZE * 2\n", + " V_STEP = V_SIZE * 2\n", + " BROWN = [0.460, 0.362, 0.216, 1.0]\n", + "\n", + " if spec == None:\n", + " spec = mj.MjSpec()\n", + "\n", + " # Defaults\n", + " main = spec.default\n", + " main.geom.type = mj.mjtGeom.mjGEOM_BOX\n", + "\n", + " body = spec.worldbody.add_body(pos=grid_loc + [0], name=name)\n", + " # Offset\n", + " x_beginning, y_end = [-SQUARE_LENGTH + H_SIZE] * 2\n", + " x_end, y_beginning = [SQUARE_LENGTH - H_SIZE] * 2\n", + " # Dimension\n", + " size_one = [H_SIZE, SQUARE_LENGTH, V_SIZE]\n", + " size_two = [SQUARE_LENGTH, H_SIZE, V_SIZE]\n", + " # Geoms positions\n", + " x_pos_l = [x_beginning, 0, direction * V_SIZE]\n", + " x_pos_r = [x_end, 0, direction * V_SIZE]\n", + " y_pos_up = [0, y_beginning, direction * V_SIZE]\n", + " y_pos_down = [0, y_end, direction * V_SIZE]\n", + "\n", + " for i in range(num_stairs):\n", + " size_one[1] = SQUARE_LENGTH - H_STEP * i\n", + " size_two[0] = SQUARE_LENGTH - H_STEP * i\n", + "\n", + " x_pos_l[2], x_pos_r[2], y_pos_up[2], y_pos_down[2] = [\n", + " direction * ( V_SIZE + V_STEP * i)] * 4\n", + "\n", + " # Left side\n", + " x_pos_l[0] = x_beginning + H_STEP * i\n", + " body.add_geom(pos=x_pos_l, size=size_one, rgba=BROWN)\n", + " # Right side\n", + " x_pos_r[0] = x_end - H_STEP * i\n", + " body.add_geom(pos=x_pos_r, size=size_one, rgba=BROWN)\n", + " # Top\n", + " y_pos_up[1] = y_beginning - H_STEP * i\n", + " body.add_geom(pos=y_pos_up, size=size_two, rgba=BROWN)\n", + " # Bottom\n", + " y_pos_down[1] = y_end + H_STEP * i\n", + " body.add_geom(pos=y_pos_down, size=size_two, rgba=BROWN)\n", + "\n", + " # Closing\n", + " size = [SQUARE_LENGTH - H_STEP * num_stairs,\n", + " SQUARE_LENGTH - H_STEP * num_stairs,\n", + " V_SIZE]\n", + " pos = [0, 0,\n", + " direction * (V_SIZE + V_STEP * num_stairs)]\n", + " body.add_geom(pos=pos, size=size, rgba=BROWN)\n", + "\n", + "render_tile(stairs, direction=random.choice([-1, 1]))" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "YwX50AtzSmnT" + }, + "outputs": [], + "source": [ + "# @title Debris (Geoms)\n", + "def debris_with_simple_geoms(spec=None, grid_loc=[0, 0], name='plane'):\n", + " SQUARE_LENGTH = 2\n", + " THICKNESS = 0.05\n", + " BROWN = [0.460, 0.362, 0.216, 1.0]\n", + " RED = [0.6, 0.12, 0.15, 1.0]\n", + "\n", + " if spec == None:\n", + " spec = mj.MjSpec()\n", + "\n", + " # Defaults\n", + " main = spec.default\n", + " main.geom.type = mj.mjtGeom.mjGEOM_BOX\n", + "\n", + " # Create tile\n", + " body = spec.worldbody.add_body(pos=grid_loc + [0], name=name)\n", + " body.add_geom(size=[SQUARE_LENGTH, SQUARE_LENGTH, THICKNESS], rgba=BROWN )\n", + "\n", + " # Simple Geoms\n", + " x_beginning, y_end = [-SQUARE_LENGTH + THICKNESS] * 2\n", + " x_end, y_beginning = [SQUARE_LENGTH - THICKNESS] * 2\n", + "\n", + " x_grid = np.linspace(x_beginning, x_end, 10)\n", + " y_grid = np.linspace(y_beginning, y_end, 10)\n", + "\n", + " for i in range(10):\n", + " x = np.random.choice(x_grid)\n", + " y = np.random.choice(y_grid)\n", + "\n", + " pos=[grid_loc[0] + x, grid_loc[1] + y, 0.2]\n", + "\n", + " g_type = None\n", + " size = None\n", + " if random.randint(0, 1):\n", + " g_type = mj.mjtGeom.mjGEOM_BOX\n", + " size = [0.1, 0.1, 0.02]\n", + " else:\n", + " g_type = mj.mjtGeom.mjGEOM_CYLINDER\n", + " size = [0.1, 0.02, 0]\n", + "\n", + " body = spec.worldbody.add_body(pos=pos, name=f'g{i}_{name}', mass=1)\n", + " body.add_geom(type=g_type, size=size, rgba=RED)\n", + " body.add_freejoint()\n", + "\n", + "render_tile(debris_with_simple_geoms)" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "Wm_0DHK8S2kQ" + }, + "outputs": [], + "source": [ + "# @title Debris (Mesh)\n", + "def debris(spec=None, grid_loc=[0, 0] , name='debris'):\n", + " SQUARE_LENGTH = 2\n", + " THICKNESS = 0.05\n", + " STEP = THICKNESS * 8\n", + " SCALE = 0.1\n", + " BROWN = [0.460, 0.362, 0.216, 1.0]\n", + " RED = [0.6, 0.12, 0.15, 1.0]\n", + "\n", + " if spec == None:\n", + " spec = mj.MjSpec()\n", + "\n", + " # Defaults\n", + " main = spec.default\n", + " main.geom.type = mj.mjtGeom.mjGEOM_BOX\n", + " main.mesh.scale = np.array([SCALE]*3, dtype=np.float64)\n", + "\n", + " x_beginning = -SQUARE_LENGTH + THICKNESS\n", + " y_beginning = SQUARE_LENGTH - THICKNESS\n", + "\n", + " # Create tile\n", + " body = spec.worldbody.add_body(pos=grid_loc + [0], name=name)\n", + " body.add_geom(size=[SQUARE_LENGTH, SQUARE_LENGTH, THICKNESS], rgba=BROWN)\n", + "\n", + " # Place debris on the tile\n", + " for i in range(10):\n", + " for j in range(10):\n", + " # draw on xy plane\n", + " drawing = np.random.normal(size=(4, 2))\n", + " drawing /= np.linalg.norm(drawing, axis=1, keepdims=True)\n", + " z = np.zeros((drawing.shape[0], 1))\n", + " # Add z value to drawing\n", + " base = np.concatenate((drawing, z), axis=1)\n", + " # Extrude drawing\n", + " z_extrusion = np.full((drawing.shape[0], 1), THICKNESS * 4)\n", + " top = np.concatenate((drawing, z_extrusion), axis=1)\n", + " # Combine to get a mesh\n", + " mesh = np.vstack((base, top))\n", + "\n", + " # Create body and add the mesh to the geom of the body\n", + " spec.add_mesh(name=f'd{i}_{j}_{name}', uservert=mesh.flatten())\n", + " pos=[grid_loc[0] + x_beginning + i * STEP,\n", + " grid_loc[1] + y_beginning - j * STEP,\n", + " 0.2]\n", + "\n", + " body = spec.worldbody.add_body(pos=pos, name=f'd{i}_{j}_{name}', mass=1)\n", + " body.add_geom(type=mj.mjtGeom.mjGEOM_MESH, meshname=f'd{i}_{j}_{name}',\n", + " rgba=RED)\n", + " body.add_freejoint()\n", + "\n", + "render_tile(debris)" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "BqTG_K8uS3v_" + }, + "outputs": [], + "source": [ + "# @title Boxy Terrain\n", + "def boxy_terrain(spec=None, grid_loc=[0, 0], name='boxy_terrain'):\n", + " SQUARE_LENGTH = 2\n", + " CUBE_LENGTH = 0.05\n", + " GRID_SIZE = int(SQUARE_LENGTH / CUBE_LENGTH)\n", + " STEP = CUBE_LENGTH * 2\n", + " BROWN = [0.460, 0.362, 0.216, 1.0]\n", + "\n", + " if spec == None:\n", + " spec=mj.MjSpec()\n", + "\n", + " # Defaults\n", + " main = spec.default\n", + " main.geom.type = mj.mjtGeom.mjGEOM_BOX\n", + "\n", + " # Create tile\n", + " body = spec.worldbody.add_body(pos=grid_loc + [0], name=name)\n", + "\n", + " x_beginning = -SQUARE_LENGTH + CUBE_LENGTH\n", + " y_beginning = SQUARE_LENGTH - CUBE_LENGTH\n", + " for i in range(GRID_SIZE):\n", + " for j in range(GRID_SIZE):\n", + " body.add_geom(\n", + " pos=[x_beginning + i * STEP ,\n", + " y_beginning - j * STEP ,\n", + " random.randint(-1, 1) * CUBE_LENGTH\n", + " ],\n", + " size=[CUBE_LENGTH] * 3,\n", + " rgba=BROWN\n", + " )\n", + "\n", + "render_tile(boxy_terrain)" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "gfQ1CafnS7hs" + }, + "outputs": [], + "source": [ + "# @title Box (Extrusion | Cut)\n", + "def box_extrusions(spec=None, grid_loc=[0, 0], complex=False,\n", + " name='box_extrusions'):\n", + " # Warning! complex sometimes leads to creation of holes\n", + " SQUARE_LENGTH = 2\n", + " CUBE_LENGTH = 0.05\n", + " GRID_SIZE = int(SQUARE_LENGTH / CUBE_LENGTH)\n", + " STEP = CUBE_LENGTH * 2\n", + " BROWN = [0.460, 0.362, 0.216, 1.0]\n", + "\n", + " if spec == None:\n", + " spec = mj.MjSpec()\n", + "\n", + " # Defaults\n", + " main = spec.default\n", + " main.geom.type = mj.mjtGeom.mjGEOM_BOX\n", + "\n", + " # Create tile\n", + " body = spec.worldbody.add_body(pos=grid_loc + [0], name=name)\n", + "\n", + " x_beginning = -SQUARE_LENGTH + CUBE_LENGTH\n", + " y_beginning = SQUARE_LENGTH - CUBE_LENGTH\n", + "\n", + " # Create initial grid and store geoms ref\n", + " grid = [[ 0 for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)]\n", + " for i in range(GRID_SIZE):\n", + " for j in range(GRID_SIZE):\n", + " ref = body.add_geom(\n", + " pos=[x_beginning + i * STEP, y_beginning - j * STEP, 0],\n", + " size=[CUBE_LENGTH] * 3,\n", + " rgba = BROWN\n", + " )\n", + " grid[i][j] = ref\n", + "\n", + " # Extrude or Cut operation using the boxes\n", + " for _ in range(random.randint(4, 50)):\n", + " box = None\n", + " while box == None:\n", + " # Create a box\n", + " start = (random.randint(0, GRID_SIZE - 2), random.randint(0, GRID_SIZE - 2))\n", + " dim = (random.randint(0, GRID_SIZE - 2), random.randint(0, GRID_SIZE-2))\n", + " # Make suer box is valid\n", + " if start[0] + dim [0] < len(grid) and start[1] + dim [1] < len(grid):\n", + " box = {\"start\":start, \"dim\":dim}\n", + "\n", + " # Use the box to Cut or Extrude\n", + " operation = random.choice([1, -1])\n", + " start = box[\"start\"]\n", + " dim = box[\"dim\"]\n", + " for i in range(start[0], dim[0]):\n", + " for j in range(start[1], dim[1]):\n", + " tile = grid[i][j]\n", + " if complex:\n", + " tile.pos[2] += operation * CUBE_LENGTH\n", + " else:\n", + " tile.pos[2] = operation * CUBE_LENGTH\n", + "\n", + "render_tile(box_extrusions)" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "e1PEGQ37S_ee" + }, + "outputs": [], + "source": [ + "# @title Heightfield\n", + "def h_field(spec=None, grid_loc=[0, 0], name='h_field'):\n", + " SQUARE_LENGTH = 2\n", + " HEIGHT = 0.1\n", + " BROWN_RGBA = [0.460, 0.362, 0.216, 1.0]\n", + "\n", + " if spec is None:\n", + " spec = mj.MjSpec()\n", + "\n", + " size = 128\n", + " noise = perlin((size, size), (8, 8))\n", + "\n", + " # Remap noise to 0 to 1\n", + " noise = (noise + 1)/2\n", + " noise -= np.min(noise)\n", + " noise /= np.max(noise)\n", + "\n", + " # Makes the edges slope down to avoid sharp boundary\n", + " noise *= edge_slope(size)\n", + "\n", + " # Create height field\n", + " hfield = spec.add_hfield(name=name,\n", + " size=[SQUARE_LENGTH, SQUARE_LENGTH,\n", + " HEIGHT, HEIGHT/10],\n", + " nrow=noise.shape[0],\n", + " ncol=noise.shape[1],\n", + " userdata=noise.flatten())\n", + "\n", + " body = spec.worldbody.add_body(pos=grid_loc + [0], name=name)\n", + " body.add_geom(type=mj.mjtGeom.mjGEOM_HFIELD, hfieldname=name,\n", + " rgba=BROWN_RGBA)\n", + "\n", + "render_tile(h_field)" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "UwDVd6aUTRKF" + }, + "outputs": [], + "source": [ + "# @title Floating platform\n", + "def floating_platform(spec=None, gird_loc=[0, 0, 0], name='platform'):\n", + " PLATFORM_LENGTH = 0.5\n", + " WIDTH = 0.12\n", + " INWARD_OFFSET = 0.008\n", + " THICKNESS = 0.005\n", + " SIZE = [PLATFORM_LENGTH, WIDTH, THICKNESS]\n", + " TENDON_LENGTH = 0.5\n", + " Z_OFFSET = 0.1\n", + "\n", + " GOLD = [0.850, 0.838, 0.119, 1]\n", + "\n", + " if spec == None:\n", + " spec = mj.MjSpec()\n", + "\n", + " # Defaults\n", + " main = spec.default\n", + " main.geom.type = mj.mjtGeom.mjGEOM_BOX\n", + "\n", + " # Platform with sites\n", + " gird_loc[2] += Z_OFFSET\n", + " platform = spec.worldbody.add_body(pos=gird_loc, name=name)\n", + " platform.add_geom(size=SIZE, rgba=GOLD)\n", + " platform.add_freejoint()\n", + "\n", + " for x_dir in [-1, 1]:\n", + " for y_dir in [-1, 1]:\n", + " # Add site to world\n", + " vector = np.array([x_dir * PLATFORM_LENGTH,\n", + " y_dir * (WIDTH - INWARD_OFFSET)])\n", + " x_w = gird_loc[0] + vector[0]\n", + " y_w = gird_loc[1] + vector[1]\n", + " z_w = gird_loc[2] + TENDON_LENGTH\n", + " # Rotate sites by theta\n", + " spec.worldbody.add_site(name=f'{name}_hook_{x_dir}_{y_dir}',\n", + " pos=[ x_w, y_w, z_w],\n", + " size=[0.01, 0, 0])\n", + " # Add site to platform\n", + " x_p = x_dir * PLATFORM_LENGTH\n", + " y_p = y_dir * (WIDTH - INWARD_OFFSET)\n", + " platform.add_site(name=f'{name}_anchor_{x_dir}_{y_dir}',\n", + " pos=[ x_p, y_p, THICKNESS * 2],\n", + " size=[0.01, 0, 0])\n", + "\n", + " # Connect tendon to sites\n", + " thread = spec.add_tendon(name=f'{name}_thread_{x_dir}_{y_dir}',\n", + " limited=True,\n", + " range=[0, TENDON_LENGTH], width=0.01 )\n", + " thread.wrap_site(f'{name}_hook_{x_dir}_{y_dir}')\n", + " thread.wrap_site(f'{name}_anchor_{x_dir}_{y_dir}')\n", + "\n", + "render_tile(floating_platform, cam_distance=2, cam_elevation=-20)" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "Wb4GmZewTSVI" + }, + "outputs": [], + "source": [ + "# @title Simple stairs\n", + "def simple_suspended_stair(spec=None, grid_loc=[0, 0], num_stair=20,\n", + " name=\"simple_suspended_stair\"):\n", + " BROWN= [0.460, 0.362, 0.216, 1.0]\n", + " SQUARE_LENGTH = 2\n", + " THICKNESS = 0.05\n", + " OFFSET_Y = -4/5 * SQUARE_LENGTH\n", + "\n", + " V_STEP = 0.076\n", + " H_STEP = 0.12\n", + "\n", + " if spec == None:\n", + " spec = mj.MjSpec()\n", + "\n", + " # Defaults\n", + " main = spec.default\n", + " main.geom.type = mj.mjtGeom.mjGEOM_BOX\n", + "\n", + " # Create tile\n", + " body = spec.worldbody.add_body(pos=grid_loc + [0], name=name)\n", + " body.add_geom(size=[SQUARE_LENGTH, SQUARE_LENGTH, THICKNESS], rgba=BROWN)\n", + "\n", + " # Create Stairs\n", + " for i in range(num_stair):\n", + " floating_platform(spec,[grid_loc[0],\n", + " OFFSET_Y + grid_loc[1] + i * 2 * H_STEP,\n", + " i * V_STEP],\n", + " name =f'{name}_p_{i}')\n", + "\n", + "render_tile(simple_suspended_stair,cam_distance=7, cam_elevation=-30)" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "AaoLjrKeTXu-" + }, + "outputs": [], + "source": [ + "# @title Sinusoidal stairs\n", + "def sin_suspended_stair(spec, grid_loc=[0, 0], num_stair=40,\n", + " name=\"sin_suspended_stair\"):\n", + " BROWN = [0.460, 0.362, 0.216, 1.0]\n", + " SQUARE_LENGTH = 2\n", + " THICKNESS = 0.05\n", + " OFFSET_Y = -4/5 * SQUARE_LENGTH\n", + "\n", + " V_STEP = 0.076\n", + " H_STEP = 0.12\n", + " AMPLITUDE = 0.2\n", + " FREQUENCY = 0.5\n", + "\n", + " if spec == None:\n", + " spec = mj.MjSpec()\n", + "\n", + " # Defaults\n", + " main = spec.default\n", + " main.geom.type = mj.mjtGeom.mjGEOM_BOX\n", + "\n", + " # Plane\n", + " body = spec.worldbody.add_body(pos=grid_loc + [0], name=name)\n", + " body.add_geom(size=[SQUARE_LENGTH, SQUARE_LENGTH, THICKNESS], rgba=BROWN)\n", + "\n", + " for i in range(num_stair):\n", + " x_step = AMPLITUDE * np.sin(2 * np.pi * FREQUENCY * (i * H_STEP))\n", + " floating_platform(spec, [grid_loc[0] + x_step,\n", + " OFFSET_Y + grid_loc[1] + i * 2 * H_STEP,\n", + " i * V_STEP],\n", + " name=f'{name}_p_{i}')\n", + "\n", + "render_tile(sin_suspended_stair,cam_distance=7, cam_elevation=-30)" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "2_Ux8h-PTqbW" + }, + "outputs": [], + "source": [ + "# @title Floating platform for circular stair\n", + "def floating_platform_for_circular_stair(spec=None, gird_loc=[0, 0, 0] ,theta=0,\n", + " name='platform'):\n", + " PLATFORM_LENGTH = 0.5\n", + " TENDON_LENGTH = 0.5\n", + " WIDTH = 0.12/4 # Platform (body) is made of 4 separate geoms\n", + " THICKNESS = 0.005\n", + " SIZE = [PLATFORM_LENGTH, WIDTH, THICKNESS]\n", + " Z_OFFSET = 0.1\n", + "\n", + " GOLD = [0.850, 0.838, 0.119, 1]\n", + "\n", + " if spec == None:\n", + " spec = mj.MjSpec()\n", + "\n", + " # Defaults\n", + " main = spec.default\n", + " main.geom.type = mj.mjtGeom.mjGEOM_BOX\n", + " spec.compiler.degree = False\n", + "\n", + " # Platform with sites\n", + " gird_loc[2] += Z_OFFSET\n", + " platform = spec.worldbody.add_body(pos=gird_loc, name=name, euler=[0, 0, theta])\n", + " platform.add_geom(pos=[0, 0, 0] , size=SIZE, euler=[0, 0, 0],rgba=GOLD)\n", + " platform.add_geom(pos=[0, 0.02, 0], size=SIZE, euler=[0, 0, 0.05],rgba=GOLD)\n", + " platform.add_geom(pos=[0, 0.05, 0], size=SIZE, euler=[0, 0, 0.1],rgba=GOLD)\n", + " platform.add_geom(pos=[0, 0.08, 0], size=SIZE, euler=[0, 0, 0.15],rgba=GOLD)\n", + " platform.add_freejoint()\n", + "\n", + " for i, x_dir in enumerate([-1, 1]):\n", + " for j, y_dir in enumerate([-1, 1]):\n", + " # Rotate sites by theta\n", + " rotation_matrix = np.array([[np.cos(-theta), -np.sin(-theta)],\n", + " [np.sin(-theta), np.cos(-theta)]])\n", + " vector = np.array([x_dir * PLATFORM_LENGTH, y_dir * WIDTH ])\n", + " if i + j == 2:\n", + " vector = np.array([x_dir * PLATFORM_LENGTH, y_dir * 6 * WIDTH ])\n", + " vector = np.dot(vector , rotation_matrix)\n", + " x_w = gird_loc[0] + vector[0]\n", + " y_w = gird_loc[1] + vector[1]\n", + " z_w = gird_loc[2] + TENDON_LENGTH\n", + "\n", + " # Add site to world\n", + " spec.worldbody.add_site(name=f'{name}_hook_{x_dir}_{y_dir}',\n", + " pos=[ x_w, y_w, z_w],\n", + " size=[0.01, 0, 0])\n", + " # Add site to platform\n", + " x_p = x_dir * PLATFORM_LENGTH\n", + " y_p = y_dir * WIDTH\n", + " if i + j == 2:\n", + " y_p = y_dir * 6 * WIDTH\n", + " platform.add_site(name=f'{name}_anchor_{x_dir}_{y_dir}',\n", + " pos=[x_p, y_p, THICKNESS * 2],\n", + " size=[0.01, 0, 0])\n", + "\n", + " # Connect tendon to sites\n", + " thread = spec.add_tendon(name=f'{name}_thread_{x_dir}_{y_dir}', limited=True,\n", + " range=[0, TENDON_LENGTH], width=0.01 )\n", + " thread.wrap_site(f'{name}_hook_{x_dir}_{y_dir}')\n", + " thread.wrap_site(f'{name}_anchor_{x_dir}_{y_dir}')\n", + "\n", + "render_tile(floating_platform_for_circular_stair,cam_distance=2, cam_elevation=-40)" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "UXSDdVF8TwmQ" + }, + "outputs": [], + "source": [ + "# @title Circular stairs\n", + "def circular_stairs(spec, grid_loc=[0, 0], num_stair=60, name=\"circular_stairs\"):\n", + " BROWN_RGBA = [0.460, 0.362, 0.216, 1.0]\n", + " SQUARE_LENGTH = 2\n", + " THICKNESS = 0.05\n", + "\n", + " RADIUS = 1.5\n", + " V_STEP = 0.076\n", + "\n", + " if spec == None:\n", + " spec = mj.MjSpec()\n", + "\n", + " # Defaults\n", + " main = spec.default\n", + " main.geom.type = mj.mjtGeom.mjGEOM_BOX\n", + " spec.compiler.degree = False\n", + "\n", + " # Plane\n", + " body = spec.worldbody.add_body(pos=grid_loc + [0], name=name)\n", + " body.add_geom(size = [SQUARE_LENGTH, SQUARE_LENGTH, THICKNESS], rgba = BROWN_RGBA )\n", + "\n", + " theta_step = 2 * np.pi / num_stair\n", + " for i in range(num_stair):\n", + " theta = i * theta_step\n", + " x = grid_loc[0] + RADIUS * np.cos(theta)\n", + " y = grid_loc[1] + RADIUS * np.sin(theta)\n", + " z = i * V_STEP\n", + "\n", + " floating_platform_for_circular_stair(spec, [x, y, z], theta=theta, name=f'{name}_p_{i}')\n", + "\n", + "render_tile(circular_stairs,cam_distance=12, cam_elevation=-30)" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "lsDky1KjT30W" + }, + "outputs": [], + "source": [ + "# @title Tile Generator\n", + "def add_tile(spec=None, grid_loc=[0, 0]):\n", + " if spec is None:\n", + " spec = mj.MjSpec()\n", + "\n", + " tile_type = random.randint(0, 9)\n", + "\n", + " if tile_type == 0:\n", + " debris_with_simple_geoms(spec, grid_loc, name=f\"plane_{grid_loc[0]}_{grid_loc[1]}\")\n", + " elif tile_type == 1:\n", + " stairs(spec, grid_loc, name=f\"stairs_up_{grid_loc[0]}_{grid_loc[1]}\",direction=1)\n", + " elif tile_type == 2:\n", + " stairs(spec, grid_loc, name=f\"stairs_down_{grid_loc[0]}_{grid_loc[1]}\",direction=-1)\n", + " elif tile_type == 3:\n", + " debris(spec, grid_loc, name=f\"debris_{grid_loc[0]}_{grid_loc[1]}\")\n", + " elif tile_type == 4:\n", + " box_extrusions(spec, grid_loc, name=f\"box_extrusions_{grid_loc[0]}_{grid_loc[1]}\")\n", + " elif tile_type == 5:\n", + " boxy_terrain(spec, grid_loc, name=f\"boxy_terrain_{grid_loc[0]}_{grid_loc[1]}\")\n", + " elif tile_type == 6:\n", + " h_field(spec, grid_loc, name=f\"h_field_{grid_loc[0]}_{grid_loc[1]}\")\n", + " elif tile_type == 7:\n", + " simple_suspended_stair(spec, grid_loc, name=f\"sss_{grid_loc[0]}_{grid_loc[1]}\")\n", + " elif tile_type == 8:\n", + " sin_suspended_stair(spec, grid_loc, name=f\"sinss_{grid_loc[0]}_{grid_loc[1]}\")\n", + " elif tile_type == 9:\n", + " circular_stairs(spec, grid_loc, name=f\"circular_s_{grid_loc[0]}_{grid_loc[1]}\")\n", + " return spec" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "id": "P2F0ObC2T8w8" + }, + "outputs": [], + "source": [ + "# @title Generate Terrain\n", + "arena_xml = \"\"\"\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + " \n", + " \n", + "\n", + "\"\"\"\n", + "\n", + "spec = mj.MjSpec.from_string(arena_xml)\n", + "\n", + "spec.option.enableflags |= mj.mjtEnableBit.mjENBL_OVERRIDE\n", + "spec.option.enableflags |= mj.mjtEnableBit.mjENBL_MULTICCD\n", + "spec.option.timestep = 0.0001\n", + "spec.compiler.degree = False\n", + "\n", + "main = spec.default\n", + "main.geom.solref = [0.001, 1]\n", + "\n", + "# Add lights\n", + "for x in [-1, 1]:\n", + " for y in [-1, 1]:\n", + " spec.worldbody.add_light(pos=[x, y, 40], dir=[-x, -y, -15])\n", + "\n", + "SQUARE_LENGTH = 2\n", + "for i in range(-2, 2):\n", + " for j in range(-2, 2):\n", + " add_tile(spec=spec, grid_loc=[i * 2 * SQUARE_LENGTH, j * 2 * SQUARE_LENGTH])\n", + "\n", + "model = spec.compile()\n", + "data = mj.MjData(model)\n", + "\n", + "cam = mj.MjvCamera()\n", + "mj.mjv_defaultCamera(cam)\n", + "cam.lookat = [-2, 0, -2]\n", + "cam.distance = 18\n", + "cam.elevation = -30\n", + "\n", + "with mj.Renderer(model, 720, 1280) as renderer:\n", + " mj.mj_forward(model, data)\n", + " renderer.update_scene(data,cam)\n", + " media.show_image(renderer.render())" + ] + }, { "cell_type": "markdown", "metadata": { @@ -864,6 +1697,7 @@ "cell_type": "code", "execution_count": 0, "metadata": { + "cellView": "form", "id": "223KzKAzLdEJ" }, "outputs": [], @@ -1263,7 +2097,7 @@ "id": "RYbaTPNmLdEK" }, "source": [ - "We can scale the size of a model by traversing the kinematic tree and applying the the scale to the relevant geoms. Above we can see humanoids of three different sizes." + "We can scale the size of a model by traversing the kinematic tree and applying the scale to the relevant geoms. Above we can see humanoids of three different sizes." ] }, { @@ -1605,7 +2439,7 @@ "Note that:\n", "\n", "- MJCF attributes correspond directly to arguments of the `add_()` methods.\n", - "- When referencing elements, e.g when specifying the joint to which an actuator is attached, the name string of the MJCF elements is used." + "- When referencing elements, e.g. when specifying the joint to which an actuator is attached, the name string of the MJCF elements is used." ] }, { @@ -1792,7 +2626,7 @@ "accelerator": "GPU", "colab": { "collapsed_sections": [ - "yXY7HGfVsVlo" + "sJFuNetilv4m" ], "gpuClass": "premium", "private_outputs": true, From 9b2b7cf92a26c9f06677daa49b333943df391338 Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Wed, 28 May 2025 07:46:43 -0700 Subject: [PATCH 180/191] Set light intensity to 0 by default (not 1000). PiperOrigin-RevId: 764257692 Change-Id: If656095e3f8d4b4d9c97fa67bdad508a49095eb3 --- doc/XMLreference.rst | 2 +- src/engine/engine_vis_visualize.c | 2 +- src/user/user_init.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 2e716ca5..588235ab 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -2968,7 +2968,7 @@ Attributes may be applied or ignored depending on the lighting model being used. .. _body-light-intensity: -:at:`intensity`: :at-val:`real, "1000.0"` +:at:`intensity`: :at-val:`real, "0.0"` The intensity of the light source, measured in candela, used for physically-based lighting models. This is unused by the default Phong lighting model. diff --git a/src/engine/engine_vis_visualize.c b/src/engine/engine_vis_visualize.c index d0d57b3f..adcc410c 100644 --- a/src/engine/engine_vis_visualize.c +++ b/src/engine/engine_vis_visualize.c @@ -2147,7 +2147,7 @@ void mjv_makeLights(const mjModel* m, const mjData* d, mjvScene* scn) { thislight->type = mjLIGHT_DIRECTIONAL; thislight->castshadow = 0; thislight->bulbradius = 0.02; - thislight->intensity = 1000; + thislight->intensity = 0; thislight->range = 10; // compute head position and gaze direction in model space diff --git a/src/user/user_init.c b/src/user/user_init.c index fd8a7132..34199812 100644 --- a/src/user/user_init.c +++ b/src/user/user_init.c @@ -203,7 +203,7 @@ void mjs_defaultLight(mjsLight* light) { // intrinsics light->castshadow = 1; light->bulbradius = 0.02; - light->intensity = 1000.0; + light->intensity = 0.0; light->range = 10.0; light->active = 1; light->attenuation[0] = 1; From 18b0e68a5a7d43c263b741ab62299a658113c442 Mon Sep 17 00:00:00 2001 From: Tom Power Date: Wed, 28 May 2025 08:27:42 -0700 Subject: [PATCH 181/191] Fix missing import in model editing colab PiperOrigin-RevId: 764272427 Change-Id: I5623818489dca322a73fa19dd50bc24771b2832a --- python/mjspec.ipynb | 1 + 1 file changed, 1 insertion(+) diff --git a/python/mjspec.ipynb b/python/mjspec.ipynb index b933fac3..ff4b997e 100644 --- a/python/mjspec.ipynb +++ b/python/mjspec.ipynb @@ -97,6 +97,7 @@ "\n", "# Other imports and helper functions\n", "import numpy as np\n", + "import random\n", "from scipy.signal import convolve2d\n", "\n", "# Graphics and plotting.\n", From 287f46b220513dde292f47dc4eb7dd4647610123 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Wed, 28 May 2025 09:25:45 -0700 Subject: [PATCH 182/191] Ensure last XML is freed after tests. This fixes undetected memory leaks in tests calling mj_loadXML. Additionally, a specific test for `mj_freeLastXML` is introduced. PiperOrigin-RevId: 764294041 Change-Id: I8b5a5683d8431ed920cdd52b06ad3515d8e19e14 --- test/fixture.h | 3 +++ test/xml/xml_api_test.cc | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/test/fixture.h b/test/fixture.h index cbb71d5f..0569579d 100644 --- a/test/fixture.h +++ b/test/fixture.h @@ -53,6 +53,9 @@ class MujocoErrorTestGuard { // By default, any MuJoCo operation which triggers a warning or error will // trigger a test failure. class MujocoTest : public ::testing::Test { + public: + ~MujocoTest() { mj_freeLastXML(); } + private: MujocoErrorTestGuard error_guard; }; diff --git a/test/xml/xml_api_test.cc b/test/xml/xml_api_test.cc index fdd1421b..d8b7a58c 100644 --- a/test/xml/xml_api_test.cc +++ b/test/xml/xml_api_test.cc @@ -199,5 +199,15 @@ TEST_F(MujocoTest, SaveXmlWithDefaultMesh) { mj_deleteModel(saved_model); } +TEST_F(MujocoTest, FreeLastXml) { + static constexpr char xml[] = ""; + mjModel* model = LoadModelFromString(xml, 0, 0); + ASSERT_THAT(model, NotNull()); + mj_deleteModel(model); + ASSERT_NE(mj_saveLastXML(nullptr, nullptr, nullptr, 0), 0); + mj_freeLastXML(); + ASSERT_EQ(mj_saveLastXML(nullptr, nullptr, nullptr, 0), 0); +} + } // namespace } // namespace mujoco From fc69ef1084936f8cd2b8c73031e98c15826069b3 Mon Sep 17 00:00:00 2001 From: Robin Alazard Date: Wed, 28 May 2025 13:19:06 -0700 Subject: [PATCH 183/191] Fix authoring type mismatch for geoms sizes attributes. Fixes wrong sizes being displayed in usdview. Also introduce a new test to ensure we catch as many of those kinds of errors as possible in the future. PiperOrigin-RevId: 764388588 Change-Id: Id3c878edd054fe5272c42c9f960c65c850655559 --- .../usd/plugins/mjcf/mujoco_to_usd.cc | 26 +++++----- .../usd/plugins/mjcf/mjcf_file_format_test.cc | 26 ++++++++++ test/experimental/usd/test_utils.cc | 48 +++++++++++++++++++ test/experimental/usd/test_utils.h | 4 ++ 4 files changed, 91 insertions(+), 13 deletions(-) diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc index 69e03ebb..90dbf7e7 100644 --- a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc @@ -820,7 +820,7 @@ class ModelWriter { // MuJoCo uses half sizes. pxr::SdfPath size_attr_path = CreateAttributeSpec(data_, box_path, pxr::UsdGeomTokens->size, - pxr::SdfValueTypeNames->Float); + pxr::SdfValueTypeNames->Double); pxr::GfVec3f scale(static_cast(size[0]), static_cast(size[1]), static_cast(size[2])); SetAttributeDefault(data_, size_attr_path, 2.0); @@ -857,14 +857,14 @@ class ModelWriter { pxr::SdfPath radius_attr_path = CreateAttributeSpec(data_, capsule_path, pxr::UsdGeomTokens->radius, - pxr::SdfValueTypeNames->Float); - SetAttributeDefault(data_, radius_attr_path, size[0]); + pxr::SdfValueTypeNames->Double); + SetAttributeDefault(data_, radius_attr_path, (double)size[0]); pxr::SdfPath height_attr_path = CreateAttributeSpec(data_, capsule_path, pxr::UsdGeomTokens->height, - pxr::SdfValueTypeNames->Float); + pxr::SdfValueTypeNames->Double); // MuJoCo uses half sizes. - SetAttributeDefault(data_, height_attr_path, size[1] * 2); + SetAttributeDefault(data_, height_attr_path, (double)(size[1] * 2)); return capsule_path; } @@ -885,14 +885,14 @@ class ModelWriter { pxr::SdfPath radius_attr_path = CreateAttributeSpec(data_, cylinder_path, pxr::UsdGeomTokens->radius, - pxr::SdfValueTypeNames->Float); - SetAttributeDefault(data_, radius_attr_path, size[0]); + pxr::SdfValueTypeNames->Double); + SetAttributeDefault(data_, radius_attr_path, (double)size[0]); pxr::SdfPath height_attr_path = CreateAttributeSpec(data_, cylinder_path, pxr::UsdGeomTokens->height, - pxr::SdfValueTypeNames->Float); + pxr::SdfValueTypeNames->Double); // MuJoCo uses half sizes. - SetAttributeDefault(data_, height_attr_path, size[1] * 2); + SetAttributeDefault(data_, height_attr_path, (double)(size[1] * 2)); return cylinder_path; } @@ -917,8 +917,8 @@ class ModelWriter { pxr::SdfPath radius_attr_path = CreateAttributeSpec(data_, ellipsoid_path, pxr::UsdGeomTokens->radius, - pxr::SdfValueTypeNames->Float); - SetAttributeDefault(data_, radius_attr_path, 1.0f); + pxr::SdfValueTypeNames->Double); + SetAttributeDefault(data_, radius_attr_path, 1.0); WriteScaleXformOp(ellipsoid_path, scale); WriteXformOpOrder(ellipsoid_path, @@ -943,8 +943,8 @@ class ModelWriter { pxr::SdfPath radius_attr_path = CreateAttributeSpec(data_, sphere_path, pxr::UsdGeomTokens->radius, - pxr::SdfValueTypeNames->Float); - SetAttributeDefault(data_, radius_attr_path, size[0]); + pxr::SdfValueTypeNames->Double); + SetAttributeDefault(data_, radius_attr_path, (double)size[0]); return sphere_path; } diff --git a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc index 4d35eb89..df41c739 100644 --- a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc +++ b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc @@ -37,9 +37,11 @@ #include #include #include +#include #include #include #include +#include // IWYU pragma: keep, used for TraverseAll #include #include #include @@ -450,6 +452,30 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestKindAuthoring) { pxr::KindTokens->subcomponent); } +TEST_F(MjcfSdfFileFormatPluginTest, TestAttributesMatchSchemaTypes) { + // TODO(robinalazard): Make the scene much more comprehensive. We ideally want + // to test all the prims that the plugin can generate. + static constexpr char kXml[] = R"( + + + + + + + + + + + )"; + + pxr::SdfLayerRefPtr layer = LoadLayer(kXml); + auto stage = pxr::UsdStage::Open(layer); + + for (const auto& prim : stage->TraverseAll()) { + ExpectAllAuthoredAttributesMatchSchemaTypes(prim); + } +} + TEST_F(MjcfSdfFileFormatPluginTest, TestGeomsPrims) { static constexpr char kXml[] = R"( diff --git a/test/experimental/usd/test_utils.cc b/test/experimental/usd/test_utils.cc index 8b046793..aedb13b7 100644 --- a/test/experimental/usd/test_utils.cc +++ b/test/experimental/usd/test_utils.cc @@ -18,10 +18,13 @@ #include #include +#include #include +#include #include #include #include +#include #include #include #include @@ -71,5 +74,50 @@ void ExpectAttributeHasConnection(pxr::UsdStageRefPtr stage, const char* path, EXPECT_EQ(sources.size(), 1); EXPECT_EQ(sources[0], SdfPath(connection_path)); } + +void ExpectAllAuthoredAttributesMatchSchemaTypes(const pxr::UsdPrim& prim) { + // Get all properties on the prim that have authored opinions. + for (const pxr::UsdProperty& prop : prim.GetAuthoredProperties()) { + // We only care about attributes, as they are the ones with a typeName. + if (pxr::UsdAttribute attr = prop.As()) { + // 1. Get the official, composed schema type name for the attribute. + const pxr::TfToken schemaTypeName = attr.GetTypeName().GetAsToken(); + + // An empty schema type name means the attribute is not defined by + // a schema, or is of a dynamically-determined type. We can't + // check for a mismatch in this case. + if (schemaTypeName.IsEmpty()) { + continue; + } + + // 2. Get the property stack to check for authored opinions. + // The stack is ordered from strongest to weakest. + const pxr::SdfPropertySpecHandleVector propStack = + attr.GetPropertyStack(); + + for (const pxr::SdfPropertySpecHandle& spec : propStack) { + // We only care about attribute specs. + if (auto attrSpec = TfDynamic_cast(spec)) { + // 3. Check if this spec has an authored `typeName`. + if (attrSpec->HasField(pxr::SdfFieldKeys->TypeName)) { + const pxr::TfToken authoredTypeName = + attrSpec->GetTypeName().GetAsToken(); + + EXPECT_EQ(authoredTypeName, schemaTypeName) + << "Type mismatch for attribute <" << attr.GetPath() + << ">: expected schema-defined type '" + << schemaTypeName.GetString() << "', got authored type '" + << authoredTypeName.GetString() << "' in layer @" + << attrSpec->GetLayer()->GetIdentifier() << "@"; + + // We've found the strongest authored opinion for `typeName`, + // so we can stop checking the stack for this attribute. + break; + } + } + } + } + } +} } // namespace usd } // namespace mujoco diff --git a/test/experimental/usd/test_utils.h b/test/experimental/usd/test_utils.h index 655f06c9..77dd4ad9 100644 --- a/test/experimental/usd/test_utils.h +++ b/test/experimental/usd/test_utils.h @@ -103,6 +103,10 @@ void ExpectAttributeEqual(pxr::UsdStageRefPtr stage, void ExpectAttributeHasConnection(pxr::UsdStageRefPtr stage, const char* path, const char* connection_path); + +// Checks that all authored attributes on the given prim have types that match +// the schema types. +void ExpectAllAuthoredAttributesMatchSchemaTypes(const pxr::UsdPrim& prim); } // namespace usd } // namespace mujoco #endif // MUJOCO_TEST_EXPERIMENTAL_USD_PLUGINS_MJCF_FIXTURE_H_ From 6b919162d1ecc32ee36d2cd48c99b86d867d4830 Mon Sep 17 00:00:00 2001 From: Robin Alazard Date: Wed, 28 May 2025 13:31:59 -0700 Subject: [PATCH 184/191] Add write_physics_ as a state variable on ModelWriter. This way we don't have to worry about passing the write_physics param everywhere. PiperOrigin-RevId: 764394280 Change-Id: I2c879ae57bdc30f50189960864097c932f967330 --- .../usd/plugins/mjcf/mujoco_to_usd.cc | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc index 90dbf7e7..f66cb77c 100644 --- a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc @@ -166,6 +166,9 @@ class ModelWriter { // Create the world body. body_paths_[kWorldIndex] = WriteWorldBody(kWorldIndex); + // Set working parameters. + write_physics_ = write_physics; + SetLayerMetadata(data_, pxr::SdfFieldKeys->Documentation, "Generated by mujoco model writer."); // Mujoco is Z up by default. @@ -181,9 +184,9 @@ class ModelWriter { WritePhysicsScene(); // Author mesh scope + mesh prims to be referenced. - WriteMeshes(write_physics); + WriteMeshes(); WriteMaterials(); - WriteBodies(write_physics); + WriteBodies(); } private: @@ -199,6 +202,8 @@ class ModelWriter { std::vector body_paths_; // Mapping from mesh names to Mesh prim path. std::unordered_map mesh_paths_; + // Whether to write physics data. + bool write_physics_ = false; // Given a name index and a parent prim path this returns a // token such that appending it to the parent prim path does not @@ -299,8 +304,7 @@ class ModelWriter { SetAttributeDefault(data_, xform_op_order_path, new_order); } - void WriteMesh(const mjsMesh *mesh, const pxr::SdfPath &parent_path, - bool write_physics) { + void WriteMesh(const mjsMesh *mesh, const pxr::SdfPath &parent_path) { auto name = GetAvailablePrimName(*mesh->name, pxr::UsdGeomTokens->Mesh, parent_path); pxr::SdfPath subcomponent_path = @@ -310,7 +314,7 @@ class ModelWriter { pxr::UsdGeomTokens->Mesh); mesh_paths_[*mesh->name] = subcomponent_path; - if (write_physics) { + if (write_physics_) { ApplyApiSchema(data_, mesh_path, MjcPhysicsTokens->MeshCollisionAPI); pxr::TfToken inertia = MjcPhysicsTokens->legacy; @@ -602,7 +606,7 @@ class ModelWriter { } } - void WriteMeshes(bool write_physics) { + void WriteMeshes() { // Create a scope for the meshes to keep things organized pxr::SdfPath scope_path = CreatePrimSpec(data_, body_paths_[kWorldIndex], kTokens->meshScope, @@ -614,7 +618,7 @@ class ModelWriter { mjsMesh *mesh = mjs_asMesh(mjs_firstElement(spec_, mjOBJ_MESH)); while (mesh) { - WriteMesh(mesh, scope_path, write_physics); + WriteMesh(mesh, scope_path); mesh = mjs_asMesh(mjs_nextElement(spec_, mesh->element)); } } @@ -1016,7 +1020,7 @@ class ModelWriter { site_path, pxr::VtArray{kTokens->xformOpTransform}); } - void WriteGeom(mjsGeom *geom, const mjsBody *body, bool write_physics) { + void WriteGeom(mjsGeom *geom, const mjsBody *body) { const int body_id = mjs_getId(body->element); const auto &body_path = body_paths_[body_id]; @@ -1052,8 +1056,8 @@ class ModelWriter { // Apply the physics schemas if we are writing physics and the // geom participates in collisions. - if (write_physics && (model_->geom_contype[geom_id] != 0 || - model_->geom_conaffinity[geom_id] != 0)) { + if (write_physics_ && (model_->geom_contype[geom_id] != 0 || + model_->geom_conaffinity[geom_id] != 0)) { ApplyApiSchema(data_, geom_path, pxr::UsdPhysicsTokens->PhysicsCollisionAPI); ApplyApiSchema(data_, geom_path, MjcPhysicsTokens->CollisionAPI); @@ -1178,10 +1182,10 @@ class ModelWriter { } } - void WriteGeoms(mjsBody *body, bool write_physics) { + void WriteGeoms(mjsBody *body) { mjsGeom *geom = mjs_asGeom(mjs_firstChild(body, mjOBJ_GEOM, false)); while (geom) { - WriteGeom(geom, body, write_physics); + WriteGeom(geom, body); geom = mjs_asGeom(mjs_nextChild(body, geom->element, false)); } } @@ -1268,7 +1272,7 @@ class ModelWriter { } } - void WriteBody(mjsBody *body, bool write_physics) { + void WriteBody(mjsBody *body) { int body_id = mjs_getId(body->element); // This should be safe as we process parent bodies before children. mjsBody *parent = mjs_getParent(body->element); @@ -1286,7 +1290,7 @@ class ModelWriter { SetPrimKind(data_, body_path, kind); // Apply the PhysicsRigidBodyAPI schema if we are writing physics. - if (write_physics) { + if (write_physics_) { // If the body had a mass specified then it must have either inertia or // fullinertia specified per inertia element XML documentation. // Therefore it is sufficient to check if the mass is non-zero to see if @@ -1373,17 +1377,17 @@ class ModelWriter { body_paths_[body_id] = body_path; } - void WriteBodies(bool write_physics) { + void WriteBodies() { mjsBody *body = mjs_asBody(mjs_firstElement(spec_, mjOBJ_BODY)); while (body) { // Only write a rigidbody if we are not the world body. // We fall through since the world body might have static // geom children. if (mjs_getId(body->element) != kWorldIndex) { - WriteBody(body, write_physics); + WriteBody(body); } WriteSites(body); - WriteGeoms(body, write_physics); + WriteGeoms(body); WriteCameras(body); WriteLights(body); body = mjs_asBody(mjs_nextElement(spec_, body->element)); From b2e8589e7ab483890edad9205f16fca48318ffa3 Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Thu, 29 May 2025 08:28:52 -0700 Subject: [PATCH 185/191] Add support for actuators to Mujoco USD interop. PiperOrigin-RevId: 764740692 Change-Id: Ib045653fee840087bc3891ee7e770bdaac08548c --- .../usd/mjcPhysics/actuatorAPI.cpp | 391 +++++++++ src/experimental/usd/mjcPhysics/actuatorAPI.h | 828 ++++++++++++++++++ .../usd/mjcPhysics/generatedSchema.usda | 87 ++ src/experimental/usd/mjcPhysics/plugInfo.json | 10 + src/experimental/usd/mjcPhysics/schema.usda | 115 +++ src/experimental/usd/mjcPhysics/tokens.cpp | 76 +- src/experimental/usd/mjcPhysics/tokens.h | 163 +++- .../usd/plugins/mjcf/mujoco_to_usd.cc | 157 +++- .../usd/plugins/mjcf/mjcf_file_format_test.cc | 123 +++ test/experimental/usd/test_utils.h | 8 + 10 files changed, 1952 insertions(+), 6 deletions(-) create mode 100644 src/experimental/usd/mjcPhysics/actuatorAPI.cpp create mode 100644 src/experimental/usd/mjcPhysics/actuatorAPI.h diff --git a/src/experimental/usd/mjcPhysics/actuatorAPI.cpp b/src/experimental/usd/mjcPhysics/actuatorAPI.cpp new file mode 100644 index 00000000..fb43eee7 --- /dev/null +++ b/src/experimental/usd/mjcPhysics/actuatorAPI.cpp @@ -0,0 +1,391 @@ +// Copyright 2025 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. + +#include "./actuatorAPI.h" + +#include "pxr/usd/sdf/assetPath.h" +#include "pxr/usd/sdf/types.h" +#include "pxr/usd/usd/schemaRegistry.h" +#include "pxr/usd/usd/typed.h" + +PXR_NAMESPACE_OPEN_SCOPE + +// Register the schema with the TfType system. +TF_REGISTRY_FUNCTION(TfType) { + TfType::Define >(); +} + +/* virtual */ +MjcPhysicsActuatorAPI::~MjcPhysicsActuatorAPI() {} + +/* static */ +MjcPhysicsActuatorAPI MjcPhysicsActuatorAPI::Get(const UsdStagePtr &stage, + const SdfPath &path) { + if (!stage) { + TF_CODING_ERROR("Invalid stage"); + return MjcPhysicsActuatorAPI(); + } + return MjcPhysicsActuatorAPI(stage->GetPrimAtPath(path)); +} + +/* virtual */ +UsdSchemaKind MjcPhysicsActuatorAPI::_GetSchemaKind() const { + return MjcPhysicsActuatorAPI::schemaKind; +} + +/* static */ +bool MjcPhysicsActuatorAPI::CanApply(const UsdPrim &prim, std::string *whyNot) { + return prim.CanApplyAPI(whyNot); +} + +/* static */ +MjcPhysicsActuatorAPI MjcPhysicsActuatorAPI::Apply(const UsdPrim &prim) { + if (prim.ApplyAPI()) { + return MjcPhysicsActuatorAPI(prim); + } + return MjcPhysicsActuatorAPI(); +} + +/* static */ +const TfType &MjcPhysicsActuatorAPI::_GetStaticTfType() { + static TfType tfType = TfType::Find(); + return tfType; +} + +/* static */ +bool MjcPhysicsActuatorAPI::_IsTypedSchema() { + static bool isTyped = _GetStaticTfType().IsA(); + return isTyped; +} + +/* virtual */ +const TfType &MjcPhysicsActuatorAPI::_GetTfType() const { + return _GetStaticTfType(); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcCtrlLimitedAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcCtrlLimited); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcCtrlLimitedAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcCtrlLimited, SdfValueTypeNames->Token, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcForceLimitedAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcForceLimited); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcForceLimitedAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcForceLimited, SdfValueTypeNames->Token, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcActLimitedAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcActLimited); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcActLimitedAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcActLimited, SdfValueTypeNames->Token, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcCtrlRangeMinAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcCtrlRangeMin); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcCtrlRangeMinAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcCtrlRangeMin, SdfValueTypeNames->Double, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcCtrlRangeMaxAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcCtrlRangeMax); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcCtrlRangeMaxAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcCtrlRangeMax, SdfValueTypeNames->Double, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcForceRangeMinAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcForceRangeMin); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcForceRangeMinAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcForceRangeMin, SdfValueTypeNames->Double, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcForceRangeMaxAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcForceRangeMax); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcForceRangeMaxAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcForceRangeMax, SdfValueTypeNames->Double, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcActRangeMinAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcActRangeMin); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcActRangeMinAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcActRangeMin, SdfValueTypeNames->Double, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcActRangeMaxAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcActRangeMax); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcActRangeMaxAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcActRangeMax, SdfValueTypeNames->Double, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcLengthRangeMinAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcLengthRangeMin); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcLengthRangeMinAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcLengthRangeMin, SdfValueTypeNames->Double, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcLengthRangeMaxAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcLengthRangeMax); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcLengthRangeMaxAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcLengthRangeMax, SdfValueTypeNames->Double, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcGearAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcGear); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcGearAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcGear, SdfValueTypeNames->DoubleArray, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcCrankLengthAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcCrankLength); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcCrankLengthAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcCrankLength, SdfValueTypeNames->Double, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcJointInParentAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcJointInParent); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcJointInParentAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcJointInParent, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcActDimAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcActDim); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcActDimAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcActDim, SdfValueTypeNames->Int, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcDynTypeAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcDynType); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcDynTypeAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcDynType, SdfValueTypeNames->Token, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcGainTypeAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcGainType); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcGainTypeAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcGainType, SdfValueTypeNames->Token, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcBiasTypeAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcBiasType); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcBiasTypeAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcBiasType, SdfValueTypeNames->Token, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcDynPrmAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcDynPrm); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcDynPrmAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcDynPrm, SdfValueTypeNames->DoubleArray, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcGainPrmAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcGainPrm); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcGainPrmAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcGainPrm, SdfValueTypeNames->DoubleArray, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcBiasPrmAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcBiasPrm); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcBiasPrmAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcBiasPrm, SdfValueTypeNames->DoubleArray, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdAttribute MjcPhysicsActuatorAPI::GetMjcActEarlyAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcActEarly); +} + +UsdAttribute MjcPhysicsActuatorAPI::CreateMjcActEarlyAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcActEarly, SdfValueTypeNames->Bool, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +UsdRelationship MjcPhysicsActuatorAPI::GetMjcRefSiteRel() const { + return GetPrim().GetRelationship(MjcPhysicsTokens->mjcRefSite); +} + +UsdRelationship MjcPhysicsActuatorAPI::CreateMjcRefSiteRel() const { + return GetPrim().CreateRelationship(MjcPhysicsTokens->mjcRefSite, + /* custom = */ false); +} + +UsdRelationship MjcPhysicsActuatorAPI::GetMjcCrankSiteRel() const { + return GetPrim().GetRelationship(MjcPhysicsTokens->mjcCrankSite); +} + +UsdRelationship MjcPhysicsActuatorAPI::CreateMjcCrankSiteRel() const { + return GetPrim().CreateRelationship(MjcPhysicsTokens->mjcCrankSite, + /* custom = */ false); +} + +UsdRelationship MjcPhysicsActuatorAPI::GetMjcSliderSiteRel() const { + return GetPrim().GetRelationship(MjcPhysicsTokens->mjcSliderSite); +} + +UsdRelationship MjcPhysicsActuatorAPI::CreateMjcSliderSiteRel() const { + return GetPrim().CreateRelationship(MjcPhysicsTokens->mjcSliderSite, + /* custom = */ false); +} + +namespace { +static inline TfTokenVector _ConcatenateAttributeNames( + const TfTokenVector &left, const TfTokenVector &right) { + TfTokenVector result; + result.reserve(left.size() + right.size()); + result.insert(result.end(), left.begin(), left.end()); + result.insert(result.end(), right.begin(), right.end()); + return result; +} +} // namespace + +/*static*/ +const TfTokenVector &MjcPhysicsActuatorAPI::GetSchemaAttributeNames( + bool includeInherited) { + static TfTokenVector localNames = { + MjcPhysicsTokens->mjcCtrlLimited, MjcPhysicsTokens->mjcForceLimited, + MjcPhysicsTokens->mjcActLimited, MjcPhysicsTokens->mjcCtrlRangeMin, + MjcPhysicsTokens->mjcCtrlRangeMax, MjcPhysicsTokens->mjcForceRangeMin, + MjcPhysicsTokens->mjcForceRangeMax, MjcPhysicsTokens->mjcActRangeMin, + MjcPhysicsTokens->mjcActRangeMax, MjcPhysicsTokens->mjcLengthRangeMin, + MjcPhysicsTokens->mjcLengthRangeMax, MjcPhysicsTokens->mjcGear, + MjcPhysicsTokens->mjcCrankLength, MjcPhysicsTokens->mjcJointInParent, + MjcPhysicsTokens->mjcActDim, MjcPhysicsTokens->mjcDynType, + MjcPhysicsTokens->mjcGainType, MjcPhysicsTokens->mjcBiasType, + MjcPhysicsTokens->mjcDynPrm, MjcPhysicsTokens->mjcGainPrm, + MjcPhysicsTokens->mjcBiasPrm, MjcPhysicsTokens->mjcActEarly, + }; + static TfTokenVector allNames = _ConcatenateAttributeNames( + UsdAPISchemaBase::GetSchemaAttributeNames(true), localNames); + + if (includeInherited) + return allNames; + else + return localNames; +} + +PXR_NAMESPACE_CLOSE_SCOPE + +// ===================================================================== // +// Feel free to add custom code below this line. It will be preserved by +// the code generator. +// +// Just remember to wrap code in the appropriate delimiters: +// 'PXR_NAMESPACE_OPEN_SCOPE', 'PXR_NAMESPACE_CLOSE_SCOPE'. +// ===================================================================== // +// --(BEGIN CUSTOM CODE)-- diff --git a/src/experimental/usd/mjcPhysics/actuatorAPI.h b/src/experimental/usd/mjcPhysics/actuatorAPI.h new file mode 100644 index 00000000..49e6721a --- /dev/null +++ b/src/experimental/usd/mjcPhysics/actuatorAPI.h @@ -0,0 +1,828 @@ +// Copyright 2025 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. + +#ifndef MJCPHYSICS_GENERATED_ACTUATORAPI_H +#define MJCPHYSICS_GENERATED_ACTUATORAPI_H + +/// \file mjcPhysics/actuatorAPI.h + +#include "./api.h" +#include "./tokens.h" +#include "pxr/base/gf/matrix4d.h" +#include "pxr/base/gf/vec3d.h" +#include "pxr/base/gf/vec3f.h" +#include "pxr/base/tf/token.h" +#include "pxr/base/tf/type.h" +#include "pxr/base/vt/value.h" +#include "pxr/pxr.h" +#include "pxr/usd/usd/apiSchemaBase.h" +#include "pxr/usd/usd/prim.h" +#include "pxr/usd/usd/stage.h" + +PXR_NAMESPACE_OPEN_SCOPE + +class SdfAssetPath; + +// -------------------------------------------------------------------------- // +// PHYSICSACTUATORAPI // +// -------------------------------------------------------------------------- // + +/// \class MjcPhysicsActuatorAPI +/// +/// API describing a Mujoco actuator. +/// +/// For any described attribute \em Fallback \em Value or \em Allowed \em Values +/// below that are text/tokens, the actual token is published and defined in +/// \ref MjcPhysicsTokens. So to set an attribute to the value "rightHanded", +/// use MjcPhysicsTokens->rightHanded as the value. +/// +class MjcPhysicsActuatorAPI : public UsdAPISchemaBase { + public: + /// Compile time constant representing what kind of schema this class is. + /// + /// \sa UsdSchemaKind + static const UsdSchemaKind schemaKind = UsdSchemaKind::SingleApplyAPI; + + /// Construct a MjcPhysicsActuatorAPI on UsdPrim \p prim . + /// Equivalent to MjcPhysicsActuatorAPI::Get(prim.GetStage(), prim.GetPath()) + /// for a \em valid \p prim, but will not immediately throw an error for + /// an invalid \p prim + explicit MjcPhysicsActuatorAPI(const UsdPrim &prim = UsdPrim()) + : UsdAPISchemaBase(prim) {} + + /// Construct a MjcPhysicsActuatorAPI on the prim held by \p schemaObj . + /// Should be preferred over MjcPhysicsActuatorAPI(schemaObj.GetPrim()), + /// as it preserves SchemaBase state. + explicit MjcPhysicsActuatorAPI(const UsdSchemaBase &schemaObj) + : UsdAPISchemaBase(schemaObj) {} + + /// Destructor. + MJCPHYSICS_API + virtual ~MjcPhysicsActuatorAPI(); + + /// Return a vector of names of all pre-declared attributes for this schema + /// class and all its ancestor classes. Does not include attributes that + /// may be authored by custom/extended methods of the schemas involved. + MJCPHYSICS_API + static const TfTokenVector &GetSchemaAttributeNames( + bool includeInherited = true); + + /// Return a MjcPhysicsActuatorAPI holding the prim adhering to this + /// schema at \p path on \p stage. If no prim exists at \p path on + /// \p stage, or if the prim at that path does not adhere to this schema, + /// return an invalid schema object. This is shorthand for the following: + /// + /// \code + /// MjcPhysicsActuatorAPI(stage->GetPrimAtPath(path)); + /// \endcode + /// + MJCPHYSICS_API + static MjcPhysicsActuatorAPI Get(const UsdStagePtr &stage, + const SdfPath &path); + + /// Returns true if this single-apply API schema can be applied to + /// the given \p prim. If this schema can not be a applied to the prim, + /// this returns false and, if provided, populates \p whyNot with the + /// reason it can not be applied. + /// + /// Note that if CanApply returns false, that does not necessarily imply + /// that calling Apply will fail. Callers are expected to call CanApply + /// before calling Apply if they want to ensure that it is valid to + /// apply a schema. + /// + /// \sa UsdPrim::GetAppliedSchemas() + /// \sa UsdPrim::HasAPI() + /// \sa UsdPrim::CanApplyAPI() + /// \sa UsdPrim::ApplyAPI() + /// \sa UsdPrim::RemoveAPI() + /// + MJCPHYSICS_API + static bool CanApply(const UsdPrim &prim, std::string *whyNot = nullptr); + + /// Applies this single-apply API schema to the given \p prim. + /// This information is stored by adding "PhysicsActuatorAPI" to the + /// token-valued, listOp metadata \em apiSchemas on the prim. + /// + /// \return A valid MjcPhysicsActuatorAPI object is returned upon success. + /// An invalid (or empty) MjcPhysicsActuatorAPI object is returned upon + /// failure. See \ref UsdPrim::ApplyAPI() for conditions + /// resulting in failure. + /// + /// \sa UsdPrim::GetAppliedSchemas() + /// \sa UsdPrim::HasAPI() + /// \sa UsdPrim::CanApplyAPI() + /// \sa UsdPrim::ApplyAPI() + /// \sa UsdPrim::RemoveAPI() + /// + MJCPHYSICS_API + static MjcPhysicsActuatorAPI Apply(const UsdPrim &prim); + + protected: + /// Returns the kind of schema this class belongs to. + /// + /// \sa UsdSchemaKind + MJCPHYSICS_API + UsdSchemaKind _GetSchemaKind() const override; + + private: + // needs to invoke _GetStaticTfType. + friend class UsdSchemaRegistry; + MJCPHYSICS_API + static const TfType &_GetStaticTfType(); + + static bool _IsTypedSchema(); + + // override SchemaBase virtuals. + MJCPHYSICS_API + const TfType &_GetTfType() const override; + + public: + // --------------------------------------------------------------------- // + // MJCCTRLLIMITED + // --------------------------------------------------------------------- // + /// If true, the control input to this actuator is automatically clamped to + /// ctrlrange at runtime. If false, control input clamping is disabled. If + /// 'auto' and autolimits is set in compiler, control clamping will + /// automatically be set to true if ctrlrange is defined without explicitly + /// setting this attribute to 'true'. Note that control input clamping can + /// also be globally disabled with the clampctrl attribute of option/flag. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform token mjc:ctrlLimited = "auto"` | + /// | C++ Type | TfToken | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Token | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + /// | \ref MjcPhysicsTokens "Allowed Values" | false, true, auto | + MJCPHYSICS_API + UsdAttribute GetMjcCtrlLimitedAttr() const; + + /// See GetMjcCtrlLimitedAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcCtrlLimitedAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCFORCELIMITED + // --------------------------------------------------------------------- // + /// If true, the force output of this actuator is automatically clamped to + /// forcerange at runtime. If false, force clamping is disabled. If 'auto' and + /// autolimits is set in compiler, force clamping will automatically be set to + /// true if forcerange is defined without explicitly setting this attribute to + /// 'true'. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform token mjc:forceLimited = "auto"` | + /// | C++ Type | TfToken | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Token | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + /// | \ref MjcPhysicsTokens "Allowed Values" | false, true, auto | + MJCPHYSICS_API + UsdAttribute GetMjcForceLimitedAttr() const; + + /// See GetMjcForceLimitedAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcForceLimitedAttr( + VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCACTLIMITED + // --------------------------------------------------------------------- // + /// If true, the internal state (activation) associated with this actuator is + /// automatically clamped to actrange at runtime. If false, activation + /// clamping is disabled. If 'auto' and autolimits is set in compiler, + /// activation clamping will automatically be set to true if actrange is + /// defined without explicitly setting this attribute to 'true'. See the + /// Activation clamping section for more details. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform token mjc:actLimited = "auto"` | + /// | C++ Type | TfToken | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Token | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + /// | \ref MjcPhysicsTokens "Allowed Values" | false, true, auto | + MJCPHYSICS_API + UsdAttribute GetMjcActLimitedAttr() const; + + /// See GetMjcActLimitedAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcActLimitedAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCCTRLRANGEMIN + // --------------------------------------------------------------------- // + /// Minimum range for clamping the control input. The first value must be + /// smaller than the second value. Setting this attribute without specifying + /// ctrllimited is an error if autolimits is 'false' in compiler. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double mjc:ctrlRange:min = 0` | + /// | C++ Type | double | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetMjcCtrlRangeMinAttr() const; + + /// See GetMjcCtrlRangeMinAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcCtrlRangeMinAttr( + VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCCTRLRANGEMAX + // --------------------------------------------------------------------- // + /// Maximum range for clamping the control input. The first value must be + /// smaller than the second value. Setting this attribute without specifying + /// ctrllimited is an error if autolimits is 'false' in compiler. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double mjc:ctrlRange:max = 0` | + /// | C++ Type | double | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetMjcCtrlRangeMaxAttr() const; + + /// See GetMjcCtrlRangeMaxAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcCtrlRangeMaxAttr( + VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCFORCERANGEMIN + // --------------------------------------------------------------------- // + /// Minimum range for clamping the force output. The first value must be no + /// greater than the second value. Setting this attribute without specifying + /// forcelimited is an error if autolimits is 'false' in compiler. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double mjc:forceRange:min = 0` | + /// | C++ Type | double | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetMjcForceRangeMinAttr() const; + + /// See GetMjcForceRangeMinAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcForceRangeMinAttr( + VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCFORCERANGEMAX + // --------------------------------------------------------------------- // + /// Maximum range for clamping the force output. The first value must be no + /// greater than the second value. Setting this attribute without specifying + /// forcelimited is an error if autolimits is 'false' in compiler. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double mjc:forceRange:max = 0` | + /// | C++ Type | double | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetMjcForceRangeMaxAttr() const; + + /// See GetMjcForceRangeMaxAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcForceRangeMaxAttr( + VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCACTRANGEMIN + // --------------------------------------------------------------------- // + /// Minimum range for clamping the activation state. The first value must be + /// no greater than the second value. See the Activation clamping section for + /// more details. Setting this attribute without specifying actlimited is an + /// error if autolimits is 'false' in compiler. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double mjc:actRange:min = 0` | + /// | C++ Type | double | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetMjcActRangeMinAttr() const; + + /// See GetMjcActRangeMinAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcActRangeMinAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCACTRANGEMAX + // --------------------------------------------------------------------- // + /// Maximum range for clamping the activation state. The first value must be + /// no greater than the second value. See the Activation clamping section for + /// more details. Setting this attribute without specifying actlimited is an + /// error if autolimits is 'false' in compiler. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double mjc:actRange:max = 0` | + /// | C++ Type | double | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetMjcActRangeMaxAttr() const; + + /// See GetMjcActRangeMaxAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcActRangeMaxAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCLENGTHRANGEMIN + // --------------------------------------------------------------------- // + /// Minimum range of feasible lengths of the actuator’s transmission. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double mjc:lengthRange:min = 0` | + /// | C++ Type | double | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetMjcLengthRangeMinAttr() const; + + /// See GetMjcLengthRangeMinAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcLengthRangeMinAttr( + VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCLENGTHRANGEMAX + // --------------------------------------------------------------------- // + /// Maximum range of feasible lengths of the actuator’s transmission. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double mjc:lengthRange:max = 0` | + /// | C++ Type | double | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetMjcLengthRangeMaxAttr() const; + + /// See GetMjcLengthRangeMaxAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcLengthRangeMaxAttr( + VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCGEAR + // --------------------------------------------------------------------- // + /// This attribute scales the length (and consequently moment arms, velocity + /// and force) of the actuator, for all transmission types. It is different + /// from the gain in the force generation mechanism, because the gain only + /// scales the force output and does not affect the length, moment arms and + /// velocity. For actuators with scalar transmission, only the first element + /// of this vector is used. The remaining elements are needed for joint, + /// jointinparent and site transmissions where this attribute is used to + /// specify 3D force and torque axes. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double[] mjc:gear = [1, 0, 0, 0, 0, 0]` | + /// | C++ Type | VtArray | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->DoubleArray | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetMjcGearAttr() const; + + /// See GetMjcGearAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcGearAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCCRANKLENGTH + // --------------------------------------------------------------------- // + /// Used only for the slider-crank transmission type. Specifies the length of + /// the connecting rod. The compiler expects this value to be positive when a + /// slider-crank transmission is present. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double mjc:crankLength = 0` | + /// | C++ Type | double | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Double | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetMjcCrankLengthAttr() const; + + /// See GetMjcCrankLengthAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcCrankLengthAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCJOINTINPARENT + // --------------------------------------------------------------------- // + /// If true and applied to ball and free joints, the 3d rotation axis given by + /// gear is defined in the parent frame (which is the world frame for free + /// joints) rather than the child frame. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:jointInParent = 0` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetMjcJointInParentAttr() const; + + /// See GetMjcJointInParentAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcJointInParentAttr( + VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCACTDIM + // --------------------------------------------------------------------- // + /// Dimension of the activation state. The default value of -1 instructs the + /// compiler to set the dimension according to the dyntype. Values larger than + /// 1 are only allowed for user-defined activation dynamics, as native types + /// require dimensions of only 0 or 1. For activation dimensions bigger than + /// 1, the last element is used to generate force. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform int mjc:actDim = -1` | + /// | C++ Type | int | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Int | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetMjcActDimAttr() const; + + /// See GetMjcActDimAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcActDimAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCDYNTYPE + // --------------------------------------------------------------------- // + /// Activation dynamics type for the actuator. The available dynamics types + /// were already described in the Actuation model section. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform token mjc:dynType = "none"` | + /// | C++ Type | TfToken | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Token | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + /// | \ref MjcPhysicsTokens "Allowed Values" | none, integrator, filter, + /// filterexact, muscle, user | + MJCPHYSICS_API + UsdAttribute GetMjcDynTypeAttr() const; + + /// See GetMjcDynTypeAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcDynTypeAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCGAINTYPE + // --------------------------------------------------------------------- // + /// The gain and bias together determine the output of the force generation + /// mechanism, which is currently assumed to be affine. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform token mjc:gainType = "fixed"` | + /// | C++ Type | TfToken | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Token | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + /// | \ref MjcPhysicsTokens "Allowed Values" | fixed, affine, muscle, user | + MJCPHYSICS_API + UsdAttribute GetMjcGainTypeAttr() const; + + /// See GetMjcGainTypeAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcGainTypeAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCBIASTYPE + // --------------------------------------------------------------------- // + /// The gain and bias together determine the output of the force generation + /// mechanism, which is currently assumed to be affine. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform token mjc:biasType = "none"` | + /// | C++ Type | TfToken | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Token | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + /// | \ref MjcPhysicsTokens "Allowed Values" | none, affine, muscle, user | + MJCPHYSICS_API + UsdAttribute GetMjcBiasTypeAttr() const; + + /// See GetMjcBiasTypeAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcBiasTypeAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCDYNPRM + // --------------------------------------------------------------------- // + /// Activation dynamics parameters. The built-in activation types (except for + /// muscle) use only the first parameter, but we provide additional parameters + /// in case user callbacks implement a more elaborate model. The length of + /// this array is not enforced by the parser, so the user can enter as many + /// parameters as needed. These defaults are not compatible with muscle + /// actuators. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double[] mjc:dynPrm = [1, 0, 0, 0, 0, 0, 0, 0, 0, + /// 0]` | | C++ Type | VtArray | | \ref Usd_Datatypes "Usd Type" | + /// SdfValueTypeNames->DoubleArray | | \ref SdfVariability "Variability" | + /// SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetMjcDynPrmAttr() const; + + /// See GetMjcDynPrmAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcDynPrmAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCGAINPRM + // --------------------------------------------------------------------- // + /// Gain parameters. The built-in gain types (except for muscle) use only the + /// first parameter, but we provide additional parameters in case user + /// callbacks implement a more elaborate model. The length of this array is + /// not enforced by the parser, so the user can enter as many parameters as + /// needed. These defaults are not compatible with muscle actuators. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double[] mjc:gainPrm = [1, 0, 0, 0, 0, 0, 0, 0, + /// 0, 0]` | | C++ Type | VtArray | | \ref Usd_Datatypes "Usd Type" | + /// SdfValueTypeNames->DoubleArray | | \ref SdfVariability "Variability" | + /// SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetMjcGainPrmAttr() const; + + /// See GetMjcGainPrmAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcGainPrmAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCBIASPRM + // --------------------------------------------------------------------- // + /// Bias parameters. The affine bias type uses three parameters. The length of + /// this array is not enforced by the parser, so the user can enter as many + /// parameters as needed. These defaults are not compatible with muscle + /// actuators. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform double[] mjc:biasPrm = [0, 0, 0, 0, 0, 0, 0, 0, + /// 0, 0]` | | C++ Type | VtArray | | \ref Usd_Datatypes "Usd Type" | + /// SdfValueTypeNames->DoubleArray | | \ref SdfVariability "Variability" | + /// SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetMjcBiasPrmAttr() const; + + /// See GetMjcBiasPrmAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcBiasPrmAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCACTEARLY + // --------------------------------------------------------------------- // + /// If true, force computation will use the next value of the activation + /// variable rather than the current one. Setting this flag reduces the delay + /// between the control and accelerations by one time-step. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform bool mjc:actEarly = 0` | + /// | C++ Type | bool | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Bool | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetMjcActEarlyAttr() const; + + /// See GetMjcActEarlyAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcActEarlyAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // --------------------------------------------------------------------- // + // MJCREFSITE + // --------------------------------------------------------------------- // + /// When applied to a site, measure the translation and rotation w.r.t the + /// frame of the refsite. In this case the actuator does have length and + /// position actuators can be used to directly control an end effector, see + /// refsite.xml example model. As above, the length is the dot product of the + /// gear vector and the frame difference. So gear='0 1 0 0 0 0' means + /// 'Y-offset of site in the refsite frame', while gear='0 0 0 0 0 1' means + /// rotation 'Z- rotation of site in the refsite frame'. It is recommended to + /// use a normalized gear vector with nonzeros in only the first 3 or the last + /// 3 elements of gear, so the actuator length will be in either length units + /// or radians, respectively. As with ball joints (see joint above), for + /// rotations which exceed a total angle of pi will wrap around, so tighter + /// limits are recommended. + /// + MJCPHYSICS_API + UsdRelationship GetMjcRefSiteRel() const; + + /// See GetMjcRefSiteRel(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create + MJCPHYSICS_API + UsdRelationship CreateMjcRefSiteRel() const; + + public: + // --------------------------------------------------------------------- // + // MJCCRANKSITE + // --------------------------------------------------------------------- // + /// If specified, the actuator acts on a slider-crank mechanism which is + /// implicitly determined by the actuator (i.e., it is not a separate model + /// element). The target site corresponds to the pin joining the crank and the + /// connecting rod. + /// + MJCPHYSICS_API + UsdRelationship GetMjcCrankSiteRel() const; + + /// See GetMjcCrankSiteRel(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create + MJCPHYSICS_API + UsdRelationship CreateMjcCrankSiteRel() const; + + public: + // --------------------------------------------------------------------- // + // MJCSLIDERSITE + // --------------------------------------------------------------------- // + /// Used only for the slider-crank transmission type. The target site is the + /// pin joining the slider and the connecting rod. The slider moves along the + /// z-axis of the slidersite frame. Therefore the site should be oriented as + /// needed when it is defined in the kinematic tree; its orientation cannot be + /// changed in the actuator definition. + /// + MJCPHYSICS_API + UsdRelationship GetMjcSliderSiteRel() const; + + /// See GetMjcSliderSiteRel(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create + MJCPHYSICS_API + UsdRelationship CreateMjcSliderSiteRel() const; + + public: + // ===================================================================== // + // Feel free to add custom code below this line, it will be preserved by + // the code generator. + // + // Just remember to: + // - Close the class declaration with }; + // - Close the namespace with PXR_NAMESPACE_CLOSE_SCOPE + // - Close the include guard with #endif + // ===================================================================== // + // --(BEGIN CUSTOM CODE)-- +}; + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif diff --git a/src/experimental/usd/mjcPhysics/generatedSchema.usda b/src/experimental/usd/mjcPhysics/generatedSchema.usda index 6b70af62..b911cf59 100644 --- a/src/experimental/usd/mjcPhysics/generatedSchema.usda +++ b/src/experimental/usd/mjcPhysics/generatedSchema.usda @@ -247,3 +247,90 @@ class "MeshCollisionAPI" ( ) } +class "PhysicsActuatorAPI" ( + doc = "API describing a Mujoco actuator." +) +{ + uniform int mjc:actDim = -1 ( + doc = "Dimension of the activation state. The default value of -1 instructs the compiler to set the dimension according to the dyntype. Values larger than 1 are only allowed for user-defined activation dynamics, as native types require dimensions of only 0 or 1. For activation dimensions bigger than 1, the last element is used to generate force." + ) + uniform bool mjc:actEarly = 0 ( + doc = "If true, force computation will use the next value of the activation variable rather than the current one. Setting this flag reduces the delay between the control and accelerations by one time-step." + ) + uniform token mjc:actLimited = "auto" ( + allowedTokens = ["false", "true", "auto"] + doc = "If true, the internal state (activation) associated with this actuator is automatically clamped to actrange at runtime. If false, activation clamping is disabled. If 'auto' and autolimits is set in compiler, activation clamping will automatically be set to true if actrange is defined without explicitly setting this attribute to 'true'. See the Activation clamping section for more details." + ) + uniform double mjc:actRange:max = 0 ( + doc = "Maximum range for clamping the activation state. The first value must be no greater than the second value. See the Activation clamping section for more details. Setting this attribute without specifying actlimited is an error if autolimits is 'false' in compiler." + ) + uniform double mjc:actRange:min = 0 ( + doc = "Minimum range for clamping the activation state. The first value must be no greater than the second value. See the Activation clamping section for more details. Setting this attribute without specifying actlimited is an error if autolimits is 'false' in compiler." + ) + uniform double[] mjc:biasPrm = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ( + doc = "Bias parameters. The affine bias type uses three parameters. The length of this array is not enforced by the parser, so the user can enter as many parameters as needed. These defaults are not compatible with muscle actuators." + ) + uniform token mjc:biasType = "none" ( + allowedTokens = ["none", "affine", "muscle", "user"] + doc = "The gain and bias together determine the output of the force generation mechanism, which is currently assumed to be affine." + ) + uniform double mjc:crankLength = 0 ( + doc = "Used only for the slider-crank transmission type. Specifies the length of the connecting rod. The compiler expects this value to be positive when a slider-crank transmission is present." + ) + rel mjc:crankSite ( + doc = "If specified, the actuator acts on a slider-crank mechanism which is implicitly determined by the actuator (i.e., it is not a separate model element). The target site corresponds to the pin joining the crank and the connecting rod." + ) + uniform token mjc:ctrlLimited = "auto" ( + allowedTokens = ["false", "true", "auto"] + doc = "If true, the control input to this actuator is automatically clamped to ctrlrange at runtime. If false, control input clamping is disabled. If 'auto' and autolimits is set in compiler, control clamping will automatically be set to true if ctrlrange is defined without explicitly setting this attribute to 'true'. Note that control input clamping can also be globally disabled with the clampctrl attribute of option/flag." + ) + uniform double mjc:ctrlRange:max = 0 ( + doc = "Maximum range for clamping the control input. The first value must be smaller than the second value. Setting this attribute without specifying ctrllimited is an error if autolimits is 'false' in compiler." + ) + uniform double mjc:ctrlRange:min = 0 ( + doc = "Minimum range for clamping the control input. The first value must be smaller than the second value. Setting this attribute without specifying ctrllimited is an error if autolimits is 'false' in compiler." + ) + uniform double[] mjc:dynPrm = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0] ( + doc = "Activation dynamics parameters. The built-in activation types (except for muscle) use only the first parameter, but we provide additional parameters in case user callbacks implement a more elaborate model. The length of this array is not enforced by the parser, so the user can enter as many parameters as needed. These defaults are not compatible with muscle actuators." + ) + uniform token mjc:dynType = "none" ( + allowedTokens = ["none", "integrator", "filter", "filterexact", "muscle", "user"] + doc = "Activation dynamics type for the actuator. The available dynamics types were already described in the Actuation model section." + ) + uniform token mjc:forceLimited = "auto" ( + allowedTokens = ["false", "true", "auto"] + doc = "If true, the force output of this actuator is automatically clamped to forcerange at runtime. If false, force clamping is disabled. If 'auto' and autolimits is set in compiler, force clamping will automatically be set to true if forcerange is defined without explicitly setting this attribute to 'true'." + ) + uniform double mjc:forceRange:max = 0 ( + doc = "Maximum range for clamping the force output. The first value must be no greater than the second value. Setting this attribute without specifying forcelimited is an error if autolimits is 'false' in compiler." + ) + uniform double mjc:forceRange:min = 0 ( + doc = "Minimum range for clamping the force output. The first value must be no greater than the second value. Setting this attribute without specifying forcelimited is an error if autolimits is 'false' in compiler." + ) + uniform double[] mjc:gainPrm = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0] ( + doc = "Gain parameters. The built-in gain types (except for muscle) use only the first parameter, but we provide additional parameters in case user callbacks implement a more elaborate model. The length of this array is not enforced by the parser, so the user can enter as many parameters as needed. These defaults are not compatible with muscle actuators." + ) + uniform token mjc:gainType = "fixed" ( + allowedTokens = ["fixed", "affine", "muscle", "user"] + doc = "The gain and bias together determine the output of the force generation mechanism, which is currently assumed to be affine." + ) + uniform double[] mjc:gear = [1, 0, 0, 0, 0, 0] ( + doc = "This attribute scales the length (and consequently moment arms, velocity and force) of the actuator, for all transmission types. It is different from the gain in the force generation mechanism, because the gain only scales the force output and does not affect the length, moment arms and velocity. For actuators with scalar transmission, only the first element of this vector is used. The remaining elements are needed for joint, jointinparent and site transmissions where this attribute is used to specify 3D force and torque axes." + ) + uniform bool mjc:jointInParent = 0 ( + doc = "If true and applied to ball and free joints, the 3d rotation axis given by gear is defined in the parent frame (which is the world frame for free joints) rather than the child frame." + ) + uniform double mjc:lengthRange:max = 0 ( + doc = "Maximum range of feasible lengths of the actuator’s transmission." + ) + uniform double mjc:lengthRange:min = 0 ( + doc = "Minimum range of feasible lengths of the actuator’s transmission." + ) + rel mjc:refSite ( + doc = "When applied to a site, measure the translation and rotation w.r.t the frame of the refsite. In this case the actuator does have length and position actuators can be used to directly control an end effector, see refsite.xml example model. As above, the length is the dot product of the gear vector and the frame difference. So gear='0 1 0 0 0 0' means 'Y-offset of site in the refsite frame', while gear='0 0 0 0 0 1' means rotation 'Z- rotation of site in the refsite frame'. It is recommended to use a normalized gear vector with nonzeros in only the first 3 or the last 3 elements of gear, so the actuator length will be in either length units or radians, respectively. As with ball joints (see joint above), for rotations which exceed a total angle of pi will wrap around, so tighter limits are recommended." + ) + rel mjc:sliderSite ( + doc = "Used only for the slider-crank transmission type. The target site is the pin joining the slider and the connecting rod. The slider moves along the z-axis of the slidersite frame. Therefore the site should be oriented as needed when it is defined in the kinematic tree; its orientation cannot be changed in the actuator definition." + ) +} + diff --git a/src/experimental/usd/mjcPhysics/plugInfo.json b/src/experimental/usd/mjcPhysics/plugInfo.json index efc5fe13..908651ba 100644 --- a/src/experimental/usd/mjcPhysics/plugInfo.json +++ b/src/experimental/usd/mjcPhysics/plugInfo.json @@ -6,6 +6,16 @@ { "Info": { "Types": { + "MjcPhysicsActuatorAPI": { + "alias": { + "UsdSchemaBase": "PhysicsActuatorAPI" + }, + "autoGenerated": true, + "bases": [ + "UsdAPISchemaBase" + ], + "schemaKind": "singleApplyAPI" + }, "MjcPhysicsCollisionAPI": { "alias": { "UsdSchemaBase": "CollisionAPI" diff --git a/src/experimental/usd/mjcPhysics/schema.usda b/src/experimental/usd/mjcPhysics/schema.usda index 7996e5b5..a59c6bb9 100644 --- a/src/experimental/usd/mjcPhysics/schema.usda +++ b/src/experimental/usd/mjcPhysics/schema.usda @@ -548,3 +548,118 @@ class "MeshCollisionAPI" ) } +class "PhysicsActuatorAPI" +( + customData = { + string className = "ActuatorAPI" + } + doc = """API describing a Mujoco actuator.""" + + inherits = +) +{ + # Control/Force/Activation Limits + uniform token mjc:ctrlLimited = "auto" ( + doc = "If true, the control input to this actuator is automatically clamped to ctrlrange at runtime. If false, control input clamping is disabled. If 'auto' and autolimits is set in compiler, control clamping will automatically be set to true if ctrlrange is defined without explicitly setting this attribute to 'true'. Note that control input clamping can also be globally disabled with the clampctrl attribute of option/flag." + allowedTokens = ["false", "true", "auto"] + ) + uniform token mjc:forceLimited = "auto" ( + doc = "If true, the force output of this actuator is automatically clamped to forcerange at runtime. If false, force clamping is disabled. If 'auto' and autolimits is set in compiler, force clamping will automatically be set to true if forcerange is defined without explicitly setting this attribute to 'true'." + allowedTokens = ["false", "true", "auto"] + ) + uniform token mjc:actLimited = "auto" ( + doc = "If true, the internal state (activation) associated with this actuator is automatically clamped to actrange at runtime. If false, activation clamping is disabled. If 'auto' and autolimits is set in compiler, activation clamping will automatically be set to true if actrange is defined without explicitly setting this attribute to 'true'. See the Activation clamping section for more details." + allowedTokens = ["false", "true", "auto"] + ) + + uniform double mjc:ctrlRange:min = 0 ( + doc = "Minimum range for clamping the control input. The first value must be smaller than the second value. Setting this attribute without specifying ctrllimited is an error if autolimits is 'false' in compiler." + ) + + uniform double mjc:ctrlRange:max = 0 ( + doc = "Maximum range for clamping the control input. The first value must be smaller than the second value. Setting this attribute without specifying ctrllimited is an error if autolimits is 'false' in compiler." + ) + + uniform double mjc:forceRange:min = 0 ( + doc = "Minimum range for clamping the force output. The first value must be no greater than the second value. Setting this attribute without specifying forcelimited is an error if autolimits is 'false' in compiler." + ) + + uniform double mjc:forceRange:max = 0 ( + doc = "Maximum range for clamping the force output. The first value must be no greater than the second value. Setting this attribute without specifying forcelimited is an error if autolimits is 'false' in compiler." + ) + + uniform double mjc:actRange:min = 0 ( + doc = "Minimum range for clamping the activation state. The first value must be no greater than the second value. See the Activation clamping section for more details. Setting this attribute without specifying actlimited is an error if autolimits is 'false' in compiler." + ) + + uniform double mjc:actRange:max = 0 ( + doc = "Maximum range for clamping the activation state. The first value must be no greater than the second value. See the Activation clamping section for more details. Setting this attribute without specifying actlimited is an error if autolimits is 'false' in compiler." + ) + + uniform double mjc:lengthRange:min = 0 ( + doc = "Minimum range of feasible lengths of the actuator’s transmission." + ) + + uniform double mjc:lengthRange:max = 0 ( + doc = "Maximum range of feasible lengths of the actuator’s transmission." + ) + + # Transmission Properties + uniform double[] mjc:gear = [1, 0, 0, 0, 0, 0] ( + doc = "This attribute scales the length (and consequently moment arms, velocity and force) of the actuator, for all transmission types. It is different from the gain in the force generation mechanism, because the gain only scales the force output and does not affect the length, moment arms and velocity. For actuators with scalar transmission, only the first element of this vector is used. The remaining elements are needed for joint, jointinparent and site transmissions where this attribute is used to specify 3D force and torque axes." + ) + + uniform double mjc:crankLength = 0.0 ( + doc = "Used only for the slider-crank transmission type. Specifies the length of the connecting rod. The compiler expects this value to be positive when a slider-crank transmission is present." + ) + + uniform bool mjc:jointInParent = False ( + doc = "If true and applied to ball and free joints, the 3d rotation axis given by gear is defined in the parent frame (which is the world frame for free joints) rather than the child frame." + ) + + rel mjc:refSite ( + doc = "When applied to a site, measure the translation and rotation w.r.t the frame of the refsite. In this case the actuator does have length and position actuators can be used to directly control an end effector, see refsite.xml example model. As above, the length is the dot product of the gear vector and the frame difference. So gear='0 1 0 0 0 0' means 'Y-offset of site in the refsite frame', while gear='0 0 0 0 0 1' means rotation 'Z- rotation of site in the refsite frame'. It is recommended to use a normalized gear vector with nonzeros in only the first 3 or the last 3 elements of gear, so the actuator length will be in either length units or radians, respectively. As with ball joints (see joint above), for rotations which exceed a total angle of pi will wrap around, so tighter limits are recommended." + ) + + rel mjc:sliderSite ( + doc = "Used only for the slider-crank transmission type. The target site is the pin joining the slider and the connecting rod. The slider moves along the z-axis of the slidersite frame. Therefore the site should be oriented as needed when it is defined in the kinematic tree; its orientation cannot be changed in the actuator definition." + ) + + # Activation Dynamics and Force Generation + uniform int mjc:actDim = -1 ( + doc = "Dimension of the activation state. The default value of -1 instructs the compiler to set the dimension according to the dyntype. Values larger than 1 are only allowed for user-defined activation dynamics, as native types require dimensions of only 0 or 1. For activation dimensions bigger than 1, the last element is used to generate force." + ) + + uniform token mjc:dynType = "none" ( + doc = "Activation dynamics type for the actuator. The available dynamics types were already described in the Actuation model section." + allowedTokens = ["none", "integrator", "filter", "filterexact", "muscle", "user"] + ) + + uniform token mjc:gainType = "fixed" ( + doc = "The gain and bias together determine the output of the force generation mechanism, which is currently assumed to be affine." + allowedTokens = ["fixed", "affine", "muscle", "user"] + ) + + uniform token mjc:biasType = "none" ( + doc = "The gain and bias together determine the output of the force generation mechanism, which is currently assumed to be affine." + allowedTokens = ["none", "affine", "muscle", "user"] + ) + + uniform double[] mjc:dynPrm = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0] ( + doc = "Activation dynamics parameters. The built-in activation types (except for muscle) use only the first parameter, but we provide additional parameters in case user callbacks implement a more elaborate model. The length of this array is not enforced by the parser, so the user can enter as many parameters as needed. These defaults are not compatible with muscle actuators." + ) + + uniform double[] mjc:gainPrm = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0] ( + doc = "Gain parameters. The built-in gain types (except for muscle) use only the first parameter, but we provide additional parameters in case user callbacks implement a more elaborate model. The length of this array is not enforced by the parser, so the user can enter as many parameters as needed. These defaults are not compatible with muscle actuators." + ) + + uniform double[] mjc:biasPrm = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ( + doc = "Bias parameters. The affine bias type uses three parameters. The length of this array is not enforced by the parser, so the user can enter as many parameters as needed. These defaults are not compatible with muscle actuators." + ) + + uniform bool mjc:actEarly = False ( + doc = "If true, force computation will use the next value of the activation variable rather than the current one. Setting this flag reduces the delay between the control and accelerations by one time-step." + ) +} + + diff --git a/src/experimental/usd/mjcPhysics/tokens.cpp b/src/experimental/usd/mjcPhysics/tokens.cpp index fc442b3f..2f33d763 100644 --- a/src/experimental/usd/mjcPhysics/tokens.cpp +++ b/src/experimental/usd/mjcPhysics/tokens.cpp @@ -17,16 +17,36 @@ PXR_NAMESPACE_OPEN_SCOPE MjcPhysicsTokensType::MjcPhysicsTokensType() - : auto_("auto", TfToken::Immortal), + : affine("affine", TfToken::Immortal), + auto_("auto", TfToken::Immortal), cg("cg", TfToken::Immortal), convex("convex", TfToken::Immortal), dense("dense", TfToken::Immortal), elliptic("elliptic", TfToken::Immortal), euler("euler", TfToken::Immortal), exact("exact", TfToken::Immortal), + false_("false", TfToken::Immortal), + filter("filter", TfToken::Immortal), + filterexact("filterexact", TfToken::Immortal), + fixed("fixed", TfToken::Immortal), implicit("implicit", TfToken::Immortal), implicitfast("implicitfast", TfToken::Immortal), + integrator("integrator", TfToken::Immortal), legacy("legacy", TfToken::Immortal), + mjcActDim("mjc:actDim", TfToken::Immortal), + mjcActEarly("mjc:actEarly", TfToken::Immortal), + mjcActLimited("mjc:actLimited", TfToken::Immortal), + mjcActRangeMax("mjc:actRange:max", TfToken::Immortal), + mjcActRangeMin("mjc:actRange:min", TfToken::Immortal), + mjcBiasPrm("mjc:biasPrm", TfToken::Immortal), + mjcBiasType("mjc:biasType", TfToken::Immortal), + mjcCrankLength("mjc:crankLength", TfToken::Immortal), + mjcCrankSite("mjc:crankSite", TfToken::Immortal), + mjcCtrlLimited("mjc:ctrlLimited", TfToken::Immortal), + mjcCtrlRangeMax("mjc:ctrlRange:max", TfToken::Immortal), + mjcCtrlRangeMin("mjc:ctrlRange:min", TfToken::Immortal), + mjcDynPrm("mjc:dynPrm", TfToken::Immortal), + mjcDynType("mjc:dynType", TfToken::Immortal), mjcFlagActuation("mjc:flag:actuation", TfToken::Immortal), mjcFlagAutoreset("mjc:flag:autoreset", TfToken::Immortal), mjcFlagClampctrl("mjc:flag:clampctrl", TfToken::Immortal), @@ -50,7 +70,16 @@ MjcPhysicsTokensType::MjcPhysicsTokensType() mjcFlagRefsafe("mjc:flag:refsafe", TfToken::Immortal), mjcFlagSensor("mjc:flag:sensor", TfToken::Immortal), mjcFlagWarmstart("mjc:flag:warmstart", TfToken::Immortal), + mjcForceLimited("mjc:forceLimited", TfToken::Immortal), + mjcForceRangeMax("mjc:forceRange:max", TfToken::Immortal), + mjcForceRangeMin("mjc:forceRange:min", TfToken::Immortal), + mjcGainPrm("mjc:gainPrm", TfToken::Immortal), + mjcGainType("mjc:gainType", TfToken::Immortal), + mjcGear("mjc:gear", TfToken::Immortal), mjcInertia("mjc:inertia", TfToken::Immortal), + mjcJointInParent("mjc:jointInParent", TfToken::Immortal), + mjcLengthRangeMax("mjc:lengthRange:max", TfToken::Immortal), + mjcLengthRangeMin("mjc:lengthRange:min", TfToken::Immortal), mjcOptionActuatorgroupdisable("mjc:option:actuatorgroupdisable", TfToken::Immortal), mjcOptionApirate("mjc:option:apirate", TfToken::Immortal), @@ -80,27 +109,54 @@ MjcPhysicsTokensType::MjcPhysicsTokensType() mjcOptionTolerance("mjc:option:tolerance", TfToken::Immortal), mjcOptionViscosity("mjc:option:viscosity", TfToken::Immortal), mjcOptionWind("mjc:option:wind", TfToken::Immortal), + mjcRefSite("mjc:refSite", TfToken::Immortal), mjcShellinertia("mjc:shellinertia", TfToken::Immortal), + mjcSliderSite("mjc:sliderSite", TfToken::Immortal), + muscle("muscle", TfToken::Immortal), newton("newton", TfToken::Immortal), + none("none", TfToken::Immortal), pgs("pgs", TfToken::Immortal), pyramidal("pyramidal", TfToken::Immortal), rk4("rk4", TfToken::Immortal), shell("shell", TfToken::Immortal), sparse("sparse", TfToken::Immortal), + true_("true", TfToken::Immortal), + user("user", TfToken::Immortal), CollisionAPI("CollisionAPI", TfToken::Immortal), MeshCollisionAPI("MeshCollisionAPI", TfToken::Immortal), + PhysicsActuatorAPI("PhysicsActuatorAPI", TfToken::Immortal), SceneAPI("SceneAPI", TfToken::Immortal), SiteAPI("SiteAPI", TfToken::Immortal), - allTokens({auto_, + allTokens({affine, + auto_, cg, convex, dense, elliptic, euler, exact, + false_, + filter, + filterexact, + fixed, implicit, implicitfast, + integrator, legacy, + mjcActDim, + mjcActEarly, + mjcActLimited, + mjcActRangeMax, + mjcActRangeMin, + mjcBiasPrm, + mjcBiasType, + mjcCrankLength, + mjcCrankSite, + mjcCtrlLimited, + mjcCtrlRangeMax, + mjcCtrlRangeMin, + mjcDynPrm, + mjcDynType, mjcFlagActuation, mjcFlagAutoreset, mjcFlagClampctrl, @@ -124,7 +180,16 @@ MjcPhysicsTokensType::MjcPhysicsTokensType() mjcFlagRefsafe, mjcFlagSensor, mjcFlagWarmstart, + mjcForceLimited, + mjcForceRangeMax, + mjcForceRangeMin, + mjcGainPrm, + mjcGainType, + mjcGear, mjcInertia, + mjcJointInParent, + mjcLengthRangeMax, + mjcLengthRangeMin, mjcOptionActuatorgroupdisable, mjcOptionApirate, mjcOptionCcd_iterations, @@ -151,15 +216,22 @@ MjcPhysicsTokensType::MjcPhysicsTokensType() mjcOptionTolerance, mjcOptionViscosity, mjcOptionWind, + mjcRefSite, mjcShellinertia, + mjcSliderSite, + muscle, newton, + none, pgs, pyramidal, rk4, shell, sparse, + true_, + user, CollisionAPI, MeshCollisionAPI, + PhysicsActuatorAPI, SceneAPI, SiteAPI}) {} diff --git a/src/experimental/usd/mjcPhysics/tokens.h b/src/experimental/usd/mjcPhysics/tokens.h index 1624ce6a..862d2ef8 100644 --- a/src/experimental/usd/mjcPhysics/tokens.h +++ b/src/experimental/usd/mjcPhysics/tokens.h @@ -49,14 +49,22 @@ PXR_NAMESPACE_OPEN_SCOPE /// Use MjcPhysicsTokens like so: /// /// \code -/// gprim.GetMyTokenValuedAttr().Set(MjcPhysicsTokens->auto_); +/// gprim.GetMyTokenValuedAttr().Set(MjcPhysicsTokens->affine); /// \endcode struct MjcPhysicsTokensType { MJCPHYSICS_API MjcPhysicsTokensType(); + /// \brief "affine" + /// + /// Possible value for MjcPhysicsActuatorAPI::GetMjcBiasTypeAttr(), Possible + /// value for MjcPhysicsActuatorAPI::GetMjcGainTypeAttr() + const TfToken affine; /// \brief "auto" /// - /// Fallback value for MjcPhysicsSceneAPI::GetJacobianAttr(), This token - /// represents the auto constraint Jacobian and matrices computed from it. + /// Fallback value for MjcPhysicsSceneAPI::GetJacobianAttr(), Fallback value + /// for MjcPhysicsActuatorAPI::GetMjcActLimitedAttr(), Fallback value for + /// MjcPhysicsActuatorAPI::GetMjcCtrlLimitedAttr(), Fallback value for + /// MjcPhysicsActuatorAPI::GetMjcForceLimitedAttr(), This token represents + /// the auto constraint Jacobian and matrices computed from it. const TfToken auto_; /// \brief "cg" /// @@ -86,6 +94,24 @@ struct MjcPhysicsTokensType { /// /// Possible value for MjcPhysicsMeshCollisionAPI::GetInertiaAttr() const TfToken exact; + /// \brief "false" + /// + /// Possible value for MjcPhysicsActuatorAPI::GetMjcActLimitedAttr(), Possible + /// value for MjcPhysicsActuatorAPI::GetMjcCtrlLimitedAttr(), Possible value + /// for MjcPhysicsActuatorAPI::GetMjcForceLimitedAttr() + const TfToken false_; + /// \brief "filter" + /// + /// Possible value for MjcPhysicsActuatorAPI::GetMjcDynTypeAttr() + const TfToken filter; + /// \brief "filterexact" + /// + /// Possible value for MjcPhysicsActuatorAPI::GetMjcDynTypeAttr() + const TfToken filterexact; + /// \brief "fixed" + /// + /// Fallback value for MjcPhysicsActuatorAPI::GetMjcGainTypeAttr() + const TfToken fixed; /// \brief "implicit" /// /// Possible value for MjcPhysicsSceneAPI::GetIntegratorAttr(), This token @@ -96,10 +122,70 @@ struct MjcPhysicsTokensType { /// Possible value for MjcPhysicsSceneAPI::GetIntegratorAttr(), This token /// represents the implicitfast numerical integrator. const TfToken implicitfast; + /// \brief "integrator" + /// + /// Possible value for MjcPhysicsActuatorAPI::GetMjcDynTypeAttr() + const TfToken integrator; /// \brief "legacy" /// /// Fallback value for MjcPhysicsMeshCollisionAPI::GetInertiaAttr() const TfToken legacy; + /// \brief "mjc:actDim" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcActDim; + /// \brief "mjc:actEarly" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcActEarly; + /// \brief "mjc:actLimited" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcActLimited; + /// \brief "mjc:actRange:max" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcActRangeMax; + /// \brief "mjc:actRange:min" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcActRangeMin; + /// \brief "mjc:biasPrm" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcBiasPrm; + /// \brief "mjc:biasType" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcBiasType; + /// \brief "mjc:crankLength" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcCrankLength; + /// \brief "mjc:crankSite" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcCrankSite; + /// \brief "mjc:ctrlLimited" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcCtrlLimited; + /// \brief "mjc:ctrlRange:max" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcCtrlRangeMax; + /// \brief "mjc:ctrlRange:min" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcCtrlRangeMin; + /// \brief "mjc:dynPrm" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcDynPrm; + /// \brief "mjc:dynType" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcDynType; /// \brief "mjc:flag:actuation" /// /// MjcPhysicsSceneAPI @@ -192,10 +278,46 @@ struct MjcPhysicsTokensType { /// /// MjcPhysicsSceneAPI const TfToken mjcFlagWarmstart; + /// \brief "mjc:forceLimited" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcForceLimited; + /// \brief "mjc:forceRange:max" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcForceRangeMax; + /// \brief "mjc:forceRange:min" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcForceRangeMin; + /// \brief "mjc:gainPrm" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcGainPrm; + /// \brief "mjc:gainType" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcGainType; + /// \brief "mjc:gear" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcGear; /// \brief "mjc:inertia" /// /// MjcPhysicsMeshCollisionAPI const TfToken mjcInertia; + /// \brief "mjc:jointInParent" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcJointInParent; + /// \brief "mjc:lengthRange:max" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcLengthRangeMax; + /// \brief "mjc:lengthRange:min" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcLengthRangeMin; /// \brief "mjc:option:actuatorgroupdisable" /// /// MjcPhysicsSceneAPI @@ -300,15 +422,34 @@ struct MjcPhysicsTokensType { /// /// MjcPhysicsSceneAPI const TfToken mjcOptionWind; + /// \brief "mjc:refSite" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcRefSite; /// \brief "mjc:shellinertia" /// /// MjcPhysicsCollisionAPI const TfToken mjcShellinertia; + /// \brief "mjc:sliderSite" + /// + /// MjcPhysicsActuatorAPI + const TfToken mjcSliderSite; + /// \brief "muscle" + /// + /// Possible value for MjcPhysicsActuatorAPI::GetMjcBiasTypeAttr(), Possible + /// value for MjcPhysicsActuatorAPI::GetMjcDynTypeAttr(), Possible value for + /// MjcPhysicsActuatorAPI::GetMjcGainTypeAttr() + const TfToken muscle; /// \brief "newton" /// /// Fallback value for MjcPhysicsSceneAPI::GetSolverAttr(), This token /// represents the Newton constraint solver algorithm. const TfToken newton; + /// \brief "none" + /// + /// Fallback value for MjcPhysicsActuatorAPI::GetMjcBiasTypeAttr(), Fallback + /// value for MjcPhysicsActuatorAPI::GetMjcDynTypeAttr() + const TfToken none; /// \brief "pgs" /// /// Possible value for MjcPhysicsSceneAPI::GetSolverAttr(), This token @@ -333,6 +474,18 @@ struct MjcPhysicsTokensType { /// Possible value for MjcPhysicsSceneAPI::GetJacobianAttr(), This token /// represents the sparse constraint Jacobian and matrices computed from it. const TfToken sparse; + /// \brief "true" + /// + /// Possible value for MjcPhysicsActuatorAPI::GetMjcActLimitedAttr(), Possible + /// value for MjcPhysicsActuatorAPI::GetMjcCtrlLimitedAttr(), Possible value + /// for MjcPhysicsActuatorAPI::GetMjcForceLimitedAttr() + const TfToken true_; + /// \brief "user" + /// + /// Possible value for MjcPhysicsActuatorAPI::GetMjcBiasTypeAttr(), Possible + /// value for MjcPhysicsActuatorAPI::GetMjcDynTypeAttr(), Possible value for + /// MjcPhysicsActuatorAPI::GetMjcGainTypeAttr() + const TfToken user; /// \brief "CollisionAPI" /// /// Schema identifer and family for MjcPhysicsCollisionAPI @@ -341,6 +494,10 @@ struct MjcPhysicsTokensType { /// /// Schema identifer and family for MjcPhysicsMeshCollisionAPI const TfToken MeshCollisionAPI; + /// \brief "PhysicsActuatorAPI" + /// + /// Schema identifer and family for MjcPhysicsActuatorAPI + const TfToken PhysicsActuatorAPI; /// \brief "SceneAPI" /// /// Schema identifer and family for MjcPhysicsSceneAPI diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc index f66cb77c..aa5f7232 100644 --- a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc @@ -42,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -105,7 +106,7 @@ using Arch_PerLibInit = pxr::Arch_PerLibInit; #if defined(ARCH_OS_DARWIN) using Arch_ConstructorEntry = pxr::Arch_ConstructorEntry; #endif -enum ErrorCodes { UnsupportedGeomTypeError, MujocoCompilationError }; +enum ErrorCodes { UnsupportedActuatorTypeError, UnsupportedGeomTypeError, MujocoCompilationError }; TF_REGISTRY_FUNCTION(pxr::TfEnum) { TF_ADD_ENUM_NAME(UnsupportedGeomTypeError, "UsdGeom type is unsupported.") @@ -155,6 +156,7 @@ class ModelWriter { ModelWriter(mjSpec *spec, mjModel *model, pxr::SdfAbstractDataRefPtr &data) : spec_(spec), model_(model), data_(data), class_path_("/Bad_Path") { body_paths_ = std::vector(model->nbody); + site_paths_ = std::vector(model->nsite); } ~ModelWriter() { mj_deleteModel(model_); } @@ -187,6 +189,9 @@ class ModelWriter { WriteMeshes(); WriteMaterials(); WriteBodies(); + if (write_physics_) { + WriteActuators(); + } } private: @@ -200,6 +205,8 @@ class ModelWriter { pxr::SdfPath class_path_; // Mapping from Mujoco body id to SdfPath. std::vector body_paths_; + // Mapping from Mujoco site id to SdfPath. + std::vector site_paths_; // Mapping from mesh names to Mesh prim path. std::unordered_map mesh_paths_; // Whether to write physics data. @@ -764,6 +771,152 @@ class ModelWriter { } } + void WriteActuator(mjsActuator *actuator) { + pxr::SdfPath transmission_path; + if (actuator->trntype == mjtTrn::mjTRN_BODY) { + int body_id = mj_name2id(model_, mjOBJ_BODY, actuator->target->c_str()); + transmission_path = body_paths_[body_id]; + } else if (actuator->trntype == mjtTrn::mjTRN_SITE || + actuator->trntype == mjtTrn::mjTRN_SLIDERCRANK) { + int site_id = mj_name2id(model_, mjOBJ_SITE, actuator->target->c_str()); + transmission_path = site_paths_[site_id]; + } else { + TF_WARN(UnsupportedActuatorTypeError, + "Unsupported actuator type for actuator %d", + mjs_getId(actuator->element)); + return; + } + + ApplyApiSchema(data_, transmission_path, + MjcPhysicsTokens->PhysicsActuatorAPI); + + if (!actuator->refsite->empty()) { + int refsite_id = mj_name2id(model_, mjOBJ_SITE, actuator->refsite->c_str()); + pxr::SdfPath refsite_path = site_paths_[refsite_id]; + CreateRelationshipSpec(data_, transmission_path, + MjcPhysicsTokens->mjcRefSite, + refsite_path, pxr::SdfVariabilityUniform); + } + + if (!actuator->slidersite->empty()) { + int slidersite_id = mj_name2id(model_, mjOBJ_SITE, actuator->slidersite->c_str()); + pxr::SdfPath slidersite_path = site_paths_[slidersite_id]; + CreateRelationshipSpec(data_, transmission_path, + MjcPhysicsTokens->mjcSliderSite, + slidersite_path, pxr::SdfVariabilityUniform); + } + + + const std::vector> limited_attributes = { + {MjcPhysicsTokens->mjcCtrlLimited, actuator->ctrllimited}, + {MjcPhysicsTokens->mjcForceLimited, actuator->forcelimited}, + {MjcPhysicsTokens->mjcActLimited, actuator->actlimited}, + }; + for (const auto &[token, value] : limited_attributes) { + pxr::TfToken limited_token = pxr::MjcPhysicsTokens->auto_; + if (value == mjLIMITED_TRUE) { + limited_token = pxr::MjcPhysicsTokens->true_; + } else if (value == mjLIMITED_FALSE) { + limited_token = pxr::MjcPhysicsTokens->false_; + } + WriteUniformAttribute(transmission_path, pxr::SdfValueTypeNames->Token, + token, limited_token); + } + + const std::vector> + actuator_double_attributes = { + {MjcPhysicsTokens->mjcCtrlRangeMin, actuator->ctrlrange[0]}, + {MjcPhysicsTokens->mjcCtrlRangeMax, actuator->ctrlrange[1]}, + {MjcPhysicsTokens->mjcForceRangeMin, actuator->forcerange[0]}, + {MjcPhysicsTokens->mjcForceRangeMax, actuator->forcerange[1]}, + {MjcPhysicsTokens->mjcActRangeMin, actuator->actrange[0]}, + {MjcPhysicsTokens->mjcActRangeMax, actuator->actrange[1]}, + {MjcPhysicsTokens->mjcLengthRangeMin, actuator->lengthrange[0]}, + {MjcPhysicsTokens->mjcLengthRangeMax, actuator->lengthrange[1]}, + {MjcPhysicsTokens->mjcCrankLength, actuator->cranklength}, + }; + for (const auto &[token, value] : actuator_double_attributes) { + WriteUniformAttribute(transmission_path, pxr::SdfValueTypeNames->Double, + token, value); + } + + WriteUniformAttribute(transmission_path, pxr::SdfValueTypeNames->Int, + MjcPhysicsTokens->mjcActDim, actuator->actdim); + WriteUniformAttribute(transmission_path, pxr::SdfValueTypeNames->Bool, + MjcPhysicsTokens->mjcActEarly, + (bool)actuator->actearly); + + WriteUniformAttribute( + transmission_path, pxr::SdfValueTypeNames->DoubleArray, + MjcPhysicsTokens->mjcGear, + pxr::VtDoubleArray(actuator->gear, actuator->gear + 6)); + + pxr::TfToken dyn_type; + if (actuator->dyntype == mjtDyn::mjDYN_NONE) { + dyn_type = MjcPhysicsTokens->none; + } else if (actuator->dyntype == mjtDyn::mjDYN_INTEGRATOR) { + dyn_type = MjcPhysicsTokens->integrator; + } else if (actuator->dyntype == mjtDyn::mjDYN_FILTER) { + dyn_type = MjcPhysicsTokens->filter; + } else if (actuator->dyntype == mjtDyn::mjDYN_FILTEREXACT) { + dyn_type = MjcPhysicsTokens->filterexact; + } else if (actuator->dyntype == mjtDyn::mjDYN_MUSCLE) { + dyn_type = MjcPhysicsTokens->muscle; + } else if (actuator->dyntype == mjtDyn::mjDYN_USER) { + dyn_type = MjcPhysicsTokens->user; + } + WriteUniformAttribute(transmission_path, pxr::SdfValueTypeNames->Token, + MjcPhysicsTokens->mjcDynType, dyn_type); + WriteUniformAttribute( + transmission_path, pxr::SdfValueTypeNames->DoubleArray, + MjcPhysicsTokens->mjcDynPrm, + pxr::VtDoubleArray(actuator->dynprm, actuator->dynprm + 10)); + + + pxr::TfToken gain_type; + if (actuator->gaintype == mjtGain::mjGAIN_FIXED) { + gain_type = MjcPhysicsTokens->fixed; + } else if (actuator->gaintype == mjtGain::mjGAIN_AFFINE) { + gain_type = MjcPhysicsTokens->affine; + } else if (actuator->gaintype == mjtGain::mjGAIN_MUSCLE) { + gain_type = MjcPhysicsTokens->muscle; + } else if (actuator->gaintype == mjtGain::mjGAIN_USER) { + gain_type = MjcPhysicsTokens->user; + } + WriteUniformAttribute(transmission_path, pxr::SdfValueTypeNames->Token, + MjcPhysicsTokens->mjcGainType, gain_type); + WriteUniformAttribute( + transmission_path, pxr::SdfValueTypeNames->DoubleArray, + MjcPhysicsTokens->mjcGainPrm, + pxr::VtDoubleArray(actuator->gainprm, actuator->gainprm + 10)); + + pxr::TfToken bias_type; + if (actuator->biastype == mjtBias::mjBIAS_NONE) { + bias_type = MjcPhysicsTokens->fixed; + } else if (actuator->biastype == mjtBias::mjBIAS_AFFINE) { + bias_type = MjcPhysicsTokens->affine; + } else if (actuator->biastype == mjtBias::mjBIAS_MUSCLE) { + bias_type = MjcPhysicsTokens->muscle; + } else if (actuator->biastype == mjtBias::mjBIAS_USER) { + bias_type = MjcPhysicsTokens->user; + } + WriteUniformAttribute(transmission_path, pxr::SdfValueTypeNames->Token, + MjcPhysicsTokens->mjcBiasType, bias_type); + WriteUniformAttribute( + transmission_path, pxr::SdfValueTypeNames->DoubleArray, + MjcPhysicsTokens->mjcBiasPrm, + pxr::VtDoubleArray(actuator->biasprm, actuator->biasprm + 10)); + } + + void WriteActuators() { + mjsActuator *actuator = + mjs_asActuator(mjs_firstElement(spec_, mjOBJ_ACTUATOR)); + while (actuator) { + WriteActuator(actuator); + actuator = mjs_asActuator(mjs_nextElement(spec_, actuator->element)); + } + } + pxr::SdfPath WriteMeshGeom(const mjsGeom *geom, const pxr::SdfPath &body_path) { std::string mj_name = geom->name->empty() ? *geom->meshname : *geom->name; @@ -1018,6 +1171,8 @@ class ModelWriter { PrependToXformOpOrder( site_path, pxr::VtArray{kTokens->xformOpTransform}); + + site_paths_[site_id] = site_path; } void WriteGeom(mjsGeom *geom, const mjsBody *body) { diff --git a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc index df41c739..b8f57bca 100644 --- a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc +++ b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc @@ -17,6 +17,7 @@ #include #include +#include "src/experimental/usd/mjcPhysics/actuatorAPI.h" #include "src/experimental/usd/mjcPhysics/collisionAPI.h" #include "src/experimental/usd/mjcPhysics/meshCollisionAPI.h" #include "src/experimental/usd/mjcPhysics/sceneAPI.h" @@ -31,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -1320,6 +1322,127 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestMassAPIDensity) { ExpectAttributeEqual(stage, "/test/body/box.physics:density", 1234.0f); } +TEST_F(MjcfSdfFileFormatPluginTest, TestMjcPhysicsActuatorGeneral) { + static constexpr char xml[] = R"( + + + + + + + + + + + + + )"; + auto stage = OpenStageWithPhysics(xml); + + EXPECT_PRIM_API_APPLIED(stage, "/test/body/site", pxr::MjcPhysicsActuatorAPI); + EXPECT_REL_HAS_TARGET(stage, "/test/body/site.mjc:refSite", "/test/body/ref"); + ExpectAttributeEqual(stage, "/test/body/site.mjc:ctrlLimited", + pxr::MjcPhysicsTokens->true_); + ExpectAttributeEqual(stage, "/test/body/site.mjc:ctrlRange:min", 0.0); + ExpectAttributeEqual(stage, "/test/body/site.mjc:ctrlRange:max", 1.0); + ExpectAttributeEqual(stage, "/test/body/site.mjc:forceLimited", + pxr::MjcPhysicsTokens->true_); + ExpectAttributeEqual(stage, "/test/body/site.mjc:forceRange:min", 2.0); + ExpectAttributeEqual(stage, "/test/body/site.mjc:forceRange:max", 3.0); + ExpectAttributeEqual(stage, "/test/body/site.mjc:actLimited", + pxr::MjcPhysicsTokens->false_); + ExpectAttributeEqual(stage, "/test/body/site.mjc:actRange:min", 4.0); + ExpectAttributeEqual(stage, "/test/body/site.mjc:actRange:max", 5.0); + ExpectAttributeEqual(stage, "/test/body/site.mjc:lengthRange:min", 6.0); + ExpectAttributeEqual(stage, "/test/body/site.mjc:lengthRange:max", 7.0); + ExpectAttributeEqual(stage, "/test/body/site.mjc:actDim", 1); + ExpectAttributeEqual(stage, "/test/body/site.mjc:dynType", + MjcPhysicsTokens->filter); + ExpectAttributeEqual(stage, "/test/body/site.mjc:gainType", + MjcPhysicsTokens->user); + ExpectAttributeEqual(stage, "/test/body/site.mjc:biasType", + MjcPhysicsTokens->user); + ExpectAttributeEqual(stage, "/test/body/site.mjc:actEarly", true); + ExpectAttributeEqual(stage, "/test/body/site.mjc:gear", + pxr::VtDoubleArray{{1, 2, 3, 4, 5, 6}}); + ExpectAttributeEqual(stage, "/test/body/site.mjc:dynPrm", + pxr::VtDoubleArray{{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}); + ExpectAttributeEqual(stage, "/test/body/site.mjc:gainPrm", + pxr::VtDoubleArray{{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}); + ExpectAttributeEqual(stage, "/test/body/site.mjc:biasPrm", + pxr::VtDoubleArray{{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestMjcPhysicsBodyActuator) { + static constexpr char xml[] = R"( + + + + + + + + + + + )"; + auto stage = OpenStageWithPhysics(xml); + + EXPECT_PRIM_API_APPLIED(stage, "/test/body", pxr::MjcPhysicsActuatorAPI); +} + +TEST_F(MjcfSdfFileFormatPluginTest, TestMjcPhysicsSliderCrankActuator) { + static constexpr char xml[] = R"( + + + + + + + + + + + + + )"; + auto stage = OpenStageWithPhysics(xml); + + EXPECT_PRIM_API_APPLIED(stage, "/test/body/crank", + pxr::MjcPhysicsActuatorAPI); + EXPECT_REL_HAS_TARGET(stage, "/test/body/crank.mjc:sliderSite", + "/test/body/slider"); + ExpectAttributeEqual(stage, "/test/body/crank.mjc:crankLength", 1.23); +} + } // namespace } // namespace usd } // namespace mujoco diff --git a/test/experimental/usd/test_utils.h b/test/experimental/usd/test_utils.h index 77dd4ad9..2aa62a6c 100644 --- a/test/experimental/usd/test_utils.h +++ b/test/experimental/usd/test_utils.h @@ -66,6 +66,14 @@ #define EXPECT_ATTRIBUTE_HAS_NO_VALUE(stage, path) \ EXPECT_FALSE((stage)->GetAttributeAtPath(SdfPath(path)).HasValue()); +#define EXPECT_REL_HAS_TARGET(stage, path, target_path) \ + { \ + pxr::SdfPathVector targets; \ + (stage)->GetRelationshipAtPath(SdfPath(path)).GetTargets(&targets); \ + EXPECT_TRUE(std::find(targets.begin(), targets.end(), \ + SdfPath(target_path)) != targets.end()); \ + } + namespace mujoco { namespace usd { From f72a175a2c6feda0e59cb5c828b05c8e2b14db04 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Fri, 30 May 2025 01:52:00 -0700 Subject: [PATCH 186/191] Enable using sensor and SDF plugins in MJX:C. PiperOrigin-RevId: 765089354 Change-Id: I3cacc2980ac8aeff68cfb927beb5b1dbb13261d4 --- mjx/mujoco/mjx/_src/collision_driver.py | 8 ++++++-- mjx/mujoco/mjx/_src/io.py | 1 + mjx/mujoco/mjx/_src/types.py | 7 ++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/mjx/mujoco/mjx/_src/collision_driver.py b/mjx/mujoco/mjx/_src/collision_driver.py index 04a4a3d6..2f45bca8 100644 --- a/mjx/mujoco/mjx/_src/collision_driver.py +++ b/mjx/mujoco/mjx/_src/collision_driver.py @@ -363,8 +363,12 @@ def make_condim(m: Union[Model, mujoco.MjModel]) -> np.ndarray: condim_counts = {} for k, v in group_counts.items(): - func = _COLLISION_FUNC[k.types] - num_contacts = condim_counts.get(k.condim, 0) + func.ncon * v # pytype: disable=attribute-error + if k.types[1] == mujoco.mjtGeom.mjGEOM_SDF: + ncon = m.opt.sdf_initpoints + else: + func = _COLLISION_FUNC[k.types] + ncon = func.ncon # pytype: disable=attribute-error + num_contacts = condim_counts.get(k.condim, 0) + ncon * v if max_contact_points > -1: num_contacts = min(max_contact_points, num_contacts) condim_counts[k.condim] = num_contacts diff --git a/mjx/mujoco/mjx/_src/io.py b/mjx/mujoco/mjx/_src/io.py index c5b94513..f8aaf565 100644 --- a/mjx/mujoco/mjx/_src/io.py +++ b/mjx/mujoco/mjx/_src/io.py @@ -626,6 +626,7 @@ def _make_data_c( 'qLDiagInv': (m.nv, float_), 'ten_velocity': (m.ntendon, float_), 'actuator_velocity': (m.nu, float_), + 'plugin_data': (m.nplugin, np.uint64), 'B_rownnz': (m.nbody, np.int32), 'B_rowadr': (m.nbody, np.int32), 'B_colind': (m.nB, np.int32), diff --git a/mjx/mujoco/mjx/_src/types.py b/mjx/mujoco/mjx/_src/types.py index 3beb6b54..a60a1dd6 100644 --- a/mjx/mujoco/mjx/_src/types.py +++ b/mjx/mujoco/mjx/_src/types.py @@ -485,6 +485,7 @@ class Option(PyTreeNode): disableflags: DisableBit enableflags: int disableactuator: int + sdf_initpoints: int class OptionC(Option): @@ -495,7 +496,6 @@ class OptionC(Option): ccd_tolerance: jax.Array noslip_iterations: int ccd_iterations: int - sdf_initpoints: int sdf_iterations: int @@ -519,6 +519,7 @@ class ModelC(PyTreeNode): nflexshelldata: jax.Array nflexevpair: jax.Array nflextexcoord: jax.Array + nplugin: jax.Array ntree: jax.Array narena: jax.Array body_bvhadr: jax.Array @@ -526,6 +527,7 @@ class ModelC(PyTreeNode): bvh_child: jax.Array bvh_nodeid: jax.Array bvh_aabb: jax.Array + geom_plugin: jax.Array light_bodyid: jax.Array light_targetbodyid: jax.Array flex_contype: jax.Array @@ -569,6 +571,8 @@ class ModelC(PyTreeNode): flex_bvhadr: jax.Array flex_bvhnum: jax.Array actuator_plugin: jax.Array + sensor_plugin: jax.Array + plugin: jax.Array class ModelJAX(PyTreeNode): @@ -980,6 +984,7 @@ class DataC(PyTreeNode): ten_velocity: jax.Array actuator_velocity: jax.Array cdof_dot: jax.Array + plugin_data: jax.Array qH: jax.Array # pylint:disable=invalid-name qHDiagInv: jax.Array # pylint:disable=invalid-name B_rownnz: jax.Array # pylint:disable=invalid-name From 7932b4b202289a98d5bc9b8041d576a091ccddd3 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Fri, 30 May 2025 04:09:37 -0700 Subject: [PATCH 187/191] Accumulate `inertial` in mjs_bodyToFrame. PiperOrigin-RevId: 765129145 Change-Id: Ib7caca42ef4272ebe762655eee9380dae471be7f --- src/user/user_model.cc | 72 +---------------------------- src/user/user_objects.cc | 93 ++++++++++++++++++++++++++++++++++++++ src/user/user_objects.h | 4 ++ test/user/user_api_test.cc | 30 ++++++++++++ 4 files changed, 128 insertions(+), 71 deletions(-) diff --git a/src/user/user_model.cc b/src/user/user_model.cc index e3d4544a..12a69f17 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -3852,78 +3852,8 @@ void mjCModel::FuseStatic(void) { } //------------- add mass and inertia (if parent not world) - if (body->parent && body->parent->name != "world" && body->mass >= mjMINVAL) { - // body_ipose = body_pose * body_ipose - changeframe(body->ipos, body->iquat, body->pos, body->quat); - - // organize data - double mass[2] = { - par->mass, - body->mass - }; - double inertia[2][3] = { - {par->inertia[0], par->inertia[1], par->inertia[2]}, - {body->inertia[0], body->inertia[1], body->inertia[2]} - }; - double ipos[2][3] = { - {par->ipos[0], par->ipos[1], par->ipos[2]}, - {body->ipos[0], body->ipos[1], body->ipos[2]} - }; - double iquat[2][4] = { - {par->iquat[0], par->iquat[1], par->iquat[2], par->iquat[3]}, - {body->iquat[0], body->iquat[1], body->iquat[2], body->iquat[3]} - }; - - // compute total mass - par->mass = 0; - mjuu_setvec(par->ipos, 0, 0, 0); - for (int j=0; j < 2; j++) { - par->mass += mass[j]; - par->ipos[0] += mass[j]*ipos[j][0]; - par->ipos[1] += mass[j]*ipos[j][1]; - par->ipos[2] += mass[j]*ipos[j][2]; - } - - // small mass: allow for now, check for errors later - if (par->mass < mjMINVAL) { - par->mass = 0; - mjuu_setvec(par->inertia, 0, 0, 0); - mjuu_setvec(par->ipos, 0, 0, 0); - mjuu_setvec(par->iquat, 1, 0, 0, 0); - } - - // proceed with regular computation - else { - // locipos = center-of-mass - par->ipos[0] /= par->mass; - par->ipos[1] /= par->mass; - par->ipos[2] /= par->mass; - - // add inertias - double toti[6] = {0, 0, 0, 0, 0, 0}; - for (int j=0; j < 2; j++) { - double inertA[6], inertB[6]; - double dpos[3] = { - ipos[j][0] - par->ipos[0], - ipos[j][1] - par->ipos[1], - ipos[j][2] - par->ipos[2] - }; - - mjuu_globalinertia(inertA, inertia[j], iquat[j]); - mjuu_offcenter(inertB, mass[j], dpos); - for (int k=0; k < 6; k++) { - toti[k] += inertA[k] + inertB[k]; - } - } - - // compute principal axes of inertia - mjuu_copyvec(par->fullinertia, toti, 6); - const char* err1 = mjuu_fullInertia(par->iquat, par->inertia, par->fullinertia); - if (err1) { - throw mjCError(nullptr, "error '%s' in fusing static body inertias", err1); - } - } + par->AccumulateInertia(body); } //------------- replace body with its children in parent body list diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 80d57a8d..6a1b2451 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -1422,6 +1422,15 @@ mjCFrame* mjCBody::ToFrame() { mjCFrame* newframe = parent->AddFrame(frame); mjuu_copyvec(newframe->spec.pos, spec.pos, 3); mjuu_copyvec(newframe->spec.quat, spec.quat, 4); + if (parent->name != "world" && mass >= mjMINVAL) { + if (!parent->explicitinertial) { + parent->MakeInertialExplicit(); + mjuu_zerovec(parent->spec.ipos, 3); + mjuu_zerovec(parent->spec.iquat, 4); + mjuu_zerovec(parent->spec.inertia, 3); + } + parent->AccumulateInertia(&this->spec, &parent->spec); + } MapFrame(parent->bodies, bodies, newframe, parent); MapFrame(parent->geoms, geoms, newframe, parent); MapFrame(parent->joints, joints, newframe, parent); @@ -1721,6 +1730,90 @@ void mjCBody::MakeInertialExplicit() { +// accumulate inertia of another body into this body +void mjCBody::AccumulateInertia(const mjsBody* other, mjsBody* result) { + if (!result) { + result = this; // use the private mjsBody + } + + // body_ipose = body_pose * body_ipose + double other_ipos[3]; + double other_iquat[4]; + mjuu_copyvec(other_ipos, other->ipos, 3); + mjuu_copyvec(other_iquat, other->iquat, 4); + mjuu_frameaccum(other_ipos, other_iquat, other->pos, other->quat); + + // organize data + double mass[2] = { + result->mass, + other->mass + }; + double inertia[2][3] = { + {result->inertia[0], result->inertia[1], result->inertia[2]}, + {other->inertia[0], other->inertia[1], other->inertia[2]} + }; + double ipos[2][3] = { + {result->ipos[0], result->ipos[1], result->ipos[2]}, + {other_ipos[0], other_ipos[1], other_ipos[2]} + }; + double iquat[2][4] = { + {result->iquat[0], result->iquat[1], result->iquat[2], result->iquat[3]}, + {other->iquat[0], other->iquat[1], other->iquat[2], other->iquat[3]} + }; + + // compute total mass + result->mass = 0; + mjuu_setvec(result->ipos, 0, 0, 0); + for (int j=0; j < 2; j++) { + result->mass += mass[j]; + result->ipos[0] += mass[j]*ipos[j][0]; + result->ipos[1] += mass[j]*ipos[j][1]; + result->ipos[2] += mass[j]*ipos[j][2]; + } + + // small mass: allow for now, check for errors later + if (result->mass < mjMINVAL) { + result->mass = 0; + mjuu_setvec(result->inertia, 0, 0, 0); + mjuu_setvec(result->ipos, 0, 0, 0); + mjuu_setvec(result->iquat, 1, 0, 0, 0); + } + + // proceed with regular computation + else { + // locipos = center-of-mass + result->ipos[0] /= result->mass; + result->ipos[1] /= result->mass; + result->ipos[2] /= result->mass; + + // add inertias + double toti[6] = {0, 0, 0, 0, 0, 0}; + for (int j=0; j < 2; j++) { + double inertA[6], inertB[6]; + double dpos[3] = { + ipos[j][0] - result->ipos[0], + ipos[j][1] - result->ipos[1], + ipos[j][2] - result->ipos[2] + }; + + mjuu_globalinertia(inertA, inertia[j], iquat[j]); + mjuu_offcenter(inertB, mass[j], dpos); + for (int k=0; k < 6; k++) { + toti[k] += inertA[k] + inertB[k]; + } + } + + // compute principal axes of inertia + mjuu_copyvec(result->fullinertia, toti, 6); + const char* err1 = mjuu_fullInertia(result->iquat, result->inertia, result->fullinertia); + if (err1) { + throw mjCError(nullptr, "error '%s' in fusing static body inertias", err1); + } + } +} + + + // compute bounding volume hierarchy void mjCBody::ComputeBVH() { if (geoms.empty()) { diff --git a/src/user/user_objects.h b/src/user/user_objects.h index 9c150646..18158c5c 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -462,6 +462,10 @@ class mjCBody : public mjCBody_, private mjsBody { // getters std::vector Bodies() const { return bodies; } + // accumulate inertia of another body into this body, if `result` is not nullptr, the accumulated + // inertia will be stored in `result`, otherwise the body's private spec will be used. + void AccumulateInertia(const mjsBody* other, mjsBody* result = nullptr); + private: mjCBody(const mjCBody& other, mjCModel* _model); // copy constructor mjCBody& operator=(const mjCBody& other); // copy assignment diff --git a/test/user/user_api_test.cc b/test/user/user_api_test.cc index 1669ea20..a415d740 100644 --- a/test/user/user_api_test.cc +++ b/test/user/user_api_test.cc @@ -1622,6 +1622,36 @@ TEST_F(MujocoTest, BodyToFrame) { mj_deleteModel(expected); } +TEST_F(MujocoTest, BodyToFrameWithInertial) { + static constexpr char xml_child[] = R"( + + + + + + + + + )"; + + std::array er; + mjSpec* spec = mj_parseXMLString(xml_child, 0, er.data(), er.size()); + EXPECT_THAT(spec, NotNull()) << er.data(); + mjModel* model = mj_compile(spec, 0); + EXPECT_THAT(model, NotNull()); + mjsBody* parent = mjs_findBody(spec, "parent"); + EXPECT_THAT(parent, NotNull()); + mjsBody* child = mjs_findBody(spec, "child"); + EXPECT_THAT(child, NotNull()); + mjs_bodyToFrame(&child); + EXPECT_THAT(parent->mass, 1); + EXPECT_THAT(parent->fullinertia[0], 1); + EXPECT_THAT(parent->fullinertia[1], 2); + EXPECT_THAT(parent->fullinertia[2], 3); + mj_deleteSpec(spec); + mj_deleteModel(model); +} + TEST_F(MujocoTest, AttachSpecToSite) { std::array er; mjtNum tol = 0; From 072c872deb3d8d41e85857674f3ec294a79e6f78 Mon Sep 17 00:00:00 2001 From: Robin Alazard Date: Fri, 30 May 2025 07:09:22 -0700 Subject: [PATCH 188/191] Add helper methods for storing USD prim paths into mjsElements. This way we fix a previously uncaught casting error due to using different types. PiperOrigin-RevId: 765177079 Change-Id: I4f817f12c92fe578b25e8c53a94b3c91e22a3db0 --- src/experimental/usd/utils.cc | 44 +++++++++++++++++++++++++++++++++++ src/experimental/usd/utils.h | 35 ++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 src/experimental/usd/utils.cc create mode 100644 src/experimental/usd/utils.h diff --git a/src/experimental/usd/utils.cc b/src/experimental/usd/utils.cc new file mode 100644 index 00000000..220c4178 --- /dev/null +++ b/src/experimental/usd/utils.cc @@ -0,0 +1,44 @@ +// Copyright 2025 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. + +#include "third_party/mujoco/src/experimental/usd/utils.h" + +#include +#include + +namespace mujoco { +namespace usd { + +constexpr const char* kUsdPrimPathKey = "usd_primpath"; + +void SetUsdPrimPathUserValue(mjsElement* element, + const pxr::SdfPath& prim_path) { + // The value is a pointer to a newly allocated SdfPath, which will be deleted + // when the mjsElement is deleted. + const pxr::SdfPath* usd_primpath = new pxr::SdfPath(prim_path); + mjs_setUserValueWithCleanup( + element, kUsdPrimPathKey, usd_primpath, + [](const void* data) { delete static_cast(data); }); +} + +pxr::SdfPath GetUsdPrimPathUserValue(mjsElement* element) { + const void* user_data = mjs_getUserValue(element, kUsdPrimPathKey); + if (user_data) { + return *static_cast(user_data); + } + return pxr::SdfPath(); +} + +} // namespace usd +} // namespace mujoco diff --git a/src/experimental/usd/utils.h b/src/experimental/usd/utils.h new file mode 100644 index 00000000..f3dcd4b5 --- /dev/null +++ b/src/experimental/usd/utils.h @@ -0,0 +1,35 @@ +// Copyright 2025 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. + +#ifndef MUJOCO_SRC_EXPERIMENTAL_USD_UTILS_H_ +#define MUJOCO_SRC_EXPERIMENTAL_USD_UTILS_H_ + +#include +#include + +namespace mujoco { +namespace usd { + +// Sets a user value on an mjsElement with the key "usd_primpath". +void SetUsdPrimPathUserValue(mjsElement* element, + const pxr::SdfPath& prim_path); + +// Gets the user value associated with the key "usd_primpath" from an +// mjsElement. Returns empty pxr::SdfPath() if the value is not found. +pxr::SdfPath GetUsdPrimPathUserValue(mjsElement* element); + +} // namespace usd +} // namespace mujoco + +#endif // MUJOCO_SRC_EXPERIMENTAL_USD_UTILS_H_ From 84ad22a5905a7d0b4e2e67ca8bb13ea90b6f74ef Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Fri, 30 May 2025 17:16:20 -0700 Subject: [PATCH 189/191] Add `mj_copyBack` for copying real-valued arrays from `mjModel` back to `mjSpec`. Also add `SaveAndReadXML` function to test fixture using `mjSpec` as input. PiperOrigin-RevId: 765393821 Change-Id: Ic257addd2fc89678fc11b226c168c077626cc51e --- doc/APIreference/functions.rst | 9 ++++ doc/changelog.rst | 1 + doc/includes/references.h | 1 + include/mujoco/mujoco.h | 3 ++ python/mujoco/introspect/functions.py | 20 +++++++++ src/user/user_api.cc | 8 ++++ src/user/user_api.h | 3 ++ src/user/user_model.cc | 5 +++ src/xml/xml.cc | 2 +- src/xml/xml.h | 2 +- src/xml/xml_api.cc | 5 ++- src/xml/xml_base.cc | 4 +- src/xml/xml_base.h | 2 +- src/xml/xml_native_writer.cc | 4 +- src/xml/xml_native_writer.h | 2 +- test/fixture.cc | 20 +++++++-- test/fixture.h | 3 ++ test/xml/xml_native_writer_test.cc | 59 +++++++++++++++++++++++++++ 18 files changed, 139 insertions(+), 14 deletions(-) diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index e413f772..2bba2a3c 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -51,6 +51,15 @@ Compile :ref:`mjSpec` to :ref:`mjModel`. A spec can be edited and compiled multi :ref:`mjModel` instance that takes the edits into account. If compilation fails, :ref:`mj_compile` returns ``NULL``; the error can be read with :ref:`mjs_getError`. +.. _mj_copyBack: + +`mj_copyBack <#mj_copyBack>`__ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. mujoco-include:: mj_copyBack + +Copy real-valued arrays from model to spec, returns 1 on success. + .. _mj_recompile: `mj_recompile <#mj_recompile>`__ diff --git a/doc/changelog.rst b/doc/changelog.rst index b425416f..324d41e7 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -17,6 +17,7 @@ General - Added new sub-component :ref:`mj_makeM` which combines the :ref:`mj_crb` call with additional logic to support the introduction in 3.3.1 of :ref:`tendon armature`. In addition to the traditional ``mjData.qM``, :ref:`mj_makeM` also computes ``mjData.M``, a CSR representation of the same matrix. +- Added a new function :ref:`mj_copyBack` to copy real-valued arrays in an mjModel to a compatible mjSpec. Simulate ^^^^^^^^ diff --git a/doc/includes/references.h b/doc/includes/references.h index 92aae862..3f725ad9 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -2992,6 +2992,7 @@ mjModel* mj_loadXML(const char* filename, const mjVFS* vfs, char* error, int err mjSpec* mj_parseXML(const char* filename, const mjVFS* vfs, char* error, int error_sz); mjSpec* mj_parseXMLString(const char* xml, const mjVFS* vfs, char* error, int error_sz); mjModel* mj_compile(mjSpec* s, const mjVFS* vfs); +int mj_copyBack(mjSpec* s, const mjModel* m); int mj_recompile(mjSpec* s, const mjVFS* vfs, mjModel* m, mjData* d); int mj_saveLastXML(const char* filename, const mjModel* m, char* error, int error_sz); void mj_freeLastXML(void); diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index 467d888a..59283756 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -107,6 +107,9 @@ MJAPI mjSpec* mj_parseXMLString(const char* xml, const mjVFS* vfs, char* error, // Compile spec to model. MJAPI mjModel* mj_compile(mjSpec* s, const mjVFS* vfs); +// Copy real-valued arrays from model to spec, returns 1 on success. +MJAPI int mj_copyBack(mjSpec* s, const mjModel* m); + // Recompile spec to model, preserving the state, return 0 on success. MJAPI int mj_recompile(mjSpec* s, const mjVFS* vfs, mjModel* m, mjData* d); diff --git a/python/mujoco/introspect/functions.py b/python/mujoco/introspect/functions.py index 44901fe4..1c4877b9 100644 --- a/python/mujoco/introspect/functions.py +++ b/python/mujoco/introspect/functions.py @@ -248,6 +248,26 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), doc='Compile spec to model.', )), + ('mj_copyBack', + FunctionDecl( + name='mj_copyBack', + return_type=ValueType(name='int'), + parameters=( + FunctionParameterDecl( + name='s', + type=PointerType( + inner_type=ValueType(name='mjSpec'), + ), + ), + FunctionParameterDecl( + name='m', + type=PointerType( + inner_type=ValueType(name='mjModel', is_const=True), + ), + ), + ), + doc='Copy real-valued arrays from model to spec, returns 1 on success.', # pylint: disable=line-too-long + )), ('mj_recompile', FunctionDecl( name='mj_recompile', diff --git a/src/user/user_api.cc b/src/user/user_api.cc index 41cba2d2..c25049b1 100644 --- a/src/user/user_api.cc +++ b/src/user/user_api.cc @@ -375,6 +375,14 @@ int mjs_setDeepCopy(mjSpec* s, int deepcopy) { +// copy real-valued arrays from model to spec, returns 1 on success +int mj_copyBack(mjSpec* s, const mjModel* m) { + mjCModel* model = static_cast(s->element); + return model->CopyBack(m); +} + + + // delete object, return 0 on success int mjs_delete(mjsElement* element) { mjCModel* model; diff --git a/src/user/user_api.h b/src/user/user_api.h index cd228147..04cdf13e 100644 --- a/src/user/user_api.h +++ b/src/user/user_api.h @@ -66,6 +66,9 @@ MJAPI int mjs_activatePlugin(mjSpec* s, const char* name); // Turn deep copy on or off attach. Returns 0 on success. MJAPI int mjs_setDeepCopy(mjSpec* s, int deepcopy); +// Copy real-valued arrays from model to spec, returns 1 on success. +MJAPI int mj_copyBack(mjSpec* s, const mjModel* m); + //---------------------------------- Attachment ---------------------------------------------------- diff --git a/src/user/user_model.cc b/src/user/user_model.cc index 12a69f17..8d4e2b5a 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -4759,6 +4759,11 @@ bool mjCModel::CopyBack(const mjModel* m) { return false; } + if (spec.element->signature != m->signature) { + errInfo = mjCError(0, "incompatible signatures in CopyBack"); + return false; + } + // option and visual option = m->opt; visual = m->vis; diff --git a/src/xml/xml.cc b/src/xml/xml.cc index 5277a534..57b1e123 100644 --- a/src/xml/xml.cc +++ b/src/xml/xml.cc @@ -395,7 +395,7 @@ mjSpec* ParseSpecFromString(std::string_view xml, const mjVFS* vfs, char* error, } // Main writer function - calls mjXWrite -std::string WriteXML(const mjModel* m, const mjSpec* spec, char* error, int nerror) { +std::string WriteXML(const mjModel* m, mjSpec* spec, char* error, int nerror) { LocaleOverride locale_override; // check for empty model diff --git a/src/xml/xml.h b/src/xml/xml.h index 44b9b23e..5f85d016 100644 --- a/src/xml/xml.h +++ b/src/xml/xml.h @@ -30,6 +30,6 @@ mjSpec* ParseSpecFromString(std::string_view xml, const mjVFS* vfs = nullptr, char* error = nullptr, int nerror = 0); // Main writer function -std::string WriteXML(const mjModel* m, const mjSpec* spec, char* error, int nerror); +std::string WriteXML(const mjModel* m, mjSpec* spec, char* error, int nerror); #endif // MUJOCO_SRC_XML_XML_H_ diff --git a/src/xml/xml_api.cc b/src/xml/xml_api.cc index bde10787..b289adb3 100644 --- a/src/xml/xml_api.cc +++ b/src/xml/xml_api.cc @@ -230,7 +230,8 @@ mjSpec* mj_parseXMLString(const char* xml, const mjVFS* vfs, char* error, int er // save spec to XML file, return 0 on success, -1 otherwise int mj_saveXML(const mjSpec* s, const char* filename, char* error, int error_sz) { - std::string result = WriteXML(NULL, s, error, error_sz); + // cast to mjSpec since WriteXML can in principle perform mj_copyBack (not here) + std::string result = WriteXML(NULL, (mjSpec*)s, error, error_sz); if (result.empty()) { return -1; } @@ -247,7 +248,7 @@ int mj_saveXML(const mjSpec* s, const char* filename, char* error, int error_sz) // save spec to XML string, return 0 on success, -1 on failure // if length of the output buffer is too small, returns the required size int mj_saveXMLString(const mjSpec* s, char* xml, int xml_sz, char* error, int error_sz) { - std::string result = WriteXML(NULL, s, error, error_sz); + std::string result = WriteXML(NULL, (mjSpec*)s, error, error_sz); if (result.empty()) { return -1; } else if (result.size() >= xml_sz) { diff --git a/src/xml/xml_base.cc b/src/xml/xml_base.cc index 1a1dd1b9..914d99de 100644 --- a/src/xml/xml_base.cc +++ b/src/xml/xml_base.cc @@ -44,8 +44,8 @@ mjXBase::mjXBase() { // set model field -void mjXBase::SetModel(const mjSpec* _model, const mjModel* m) { - spec = (mjSpec*)_model; +void mjXBase::SetModel(mjSpec* _model, const mjModel* m) { + spec = _model; } diff --git a/src/xml/xml_base.h b/src/xml/xml_base.h index f2aca5c8..9dc261b1 100644 --- a/src/xml/xml_base.h +++ b/src/xml/xml_base.h @@ -92,7 +92,7 @@ class mjXBase : public mjXUtil { }; // set the model allocated externally - virtual void SetModel(const mjSpec*, const mjModel* = nullptr); + virtual void SetModel(mjSpec*, const mjModel* = nullptr); // read alternative orientation specification static int ReadAlternative(tinyxml2::XMLElement* elem, mjsOrientation& alt); diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index f28041a8..de2aef96 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -885,12 +885,12 @@ mjXWriter::mjXWriter(void) { // cast model -void mjXWriter::SetModel(const mjSpec* _spec, const mjModel* m) { +void mjXWriter::SetModel(mjSpec* _spec, const mjModel* m) { if (_spec) { model = static_cast(_spec->element); } if (m) { - model->CopyBack(m); + mj_copyBack(&model->spec, m); } } diff --git a/src/xml/xml_native_writer.h b/src/xml/xml_native_writer.h index b4941429..09962522 100644 --- a/src/xml/xml_native_writer.h +++ b/src/xml/xml_native_writer.h @@ -29,7 +29,7 @@ class mjXWriter : public mjXBase { public: mjXWriter(); // constructor virtual ~mjXWriter() = default; // destructor - void SetModel(const mjSpec* _spec, const mjModel* m = nullptr); + void SetModel(mjSpec* _spec, const mjModel* m = nullptr); // write XML document to string std::string Write(char *error, std::size_t error_sz); diff --git a/test/fixture.cc b/test/fixture.cc index 545691b4..cdf714b4 100644 --- a/test/fixture.cc +++ b/test/fixture.cc @@ -130,9 +130,7 @@ std::string GetFileContents(const char* path) { return sstream.str(); } -std::string SaveAndReadXml(const mjModel* model) { - EXPECT_THAT(model, testing::NotNull()); - +std::string SaveAndReadXmlImpl(const mjModel* model, const mjSpec* spec) { constexpr int kMaxPathLen = 1024; std::string path_template = std::filesystem::temp_directory_path().append("tmp.XXXXXX").string(); @@ -148,7 +146,11 @@ std::string SaveAndReadXml(const mjModel* model) { EXPECT_NE(_mktemp_s(filepath), EINVAL); #endif - mj_saveLastXML(filepath, model, nullptr, 0); + if (spec) { + mj_saveXML(spec, filepath, nullptr, 0); + } else if (model) { + mj_saveLastXML(filepath, model, nullptr, 0); + } std::string contents = GetFileContents(filepath); #if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L @@ -159,6 +161,16 @@ std::string SaveAndReadXml(const mjModel* model) { return contents; } +std::string SaveAndReadXml(const mjModel* model) { + EXPECT_THAT(model, testing::NotNull()); + return SaveAndReadXmlImpl(model, nullptr); +} + +std::string SaveAndReadXml(const mjSpec* spec) { + EXPECT_THAT(spec, testing::NotNull()); + return SaveAndReadXmlImpl(nullptr, spec); +} + std::vector GetCtrlNoise(const mjModel* m, int nsteps, mjtNum ctrlnoise) { std::vector ctrl; diff --git a/test/fixture.h b/test/fixture.h index 0569579d..f9d0cb1f 100644 --- a/test/fixture.h +++ b/test/fixture.h @@ -102,6 +102,9 @@ mjModel* LoadModelFromPath(const char* model_path); // Returns a string loaded from first saving the model given an input. std::string SaveAndReadXml(const mjModel* model); +// Returns a string loaded from first saving the spec given an input. +std::string SaveAndReadXml(const mjSpec* spec); + // Adds control noise. std::vector GetCtrlNoise(const mjModel* m, int nsteps, mjtNum ctrlnoise = 0.01); diff --git a/test/xml/xml_native_writer_test.cc b/test/xml/xml_native_writer_test.cc index c938b465..fd87a86d 100644 --- a/test/xml/xml_native_writer_test.cc +++ b/test/xml/xml_native_writer_test.cc @@ -1498,6 +1498,65 @@ TEST_F(DecompilerTest, SavesStatistics) { mj_deleteModel(model); } +TEST_F(DecompilerTest, SaveAndReadXml) { + static constexpr char xml1[] = R"( + + + + + + + )"; + static constexpr char xml2[] = R"( + + + + + + + + )"; + std::array error; + mjModel* m1 = LoadModelFromString(xml1, error.data(), error.size()); + ASSERT_THAT(m1, NotNull()) << error.data(); + m1->geom_size[0] = 10; + m1->geom_size[3] = 20; + std::string saved_xml = SaveAndReadXml(m1); + EXPECT_THAT(saved_xml, HasSubstr("geom size=\"10\"")); + EXPECT_THAT(saved_xml, HasSubstr("geom size=\"20\"")); + + // parse the mjSpec, save it and read it back + mjSpec* spec = mj_parseXMLString(xml2, nullptr, error.data(), error.size()); + EXPECT_THAT(spec, NotNull()) << error.data(); + mjModel* m2 = mj_compile(spec, nullptr); + std::string saved_xml1 = SaveAndReadXml(spec); + EXPECT_THAT(saved_xml1, HasSubstr("geom size=\"1\"")); + EXPECT_THAT(saved_xml1, HasSubstr("geom size=\"2\"")); + EXPECT_THAT(saved_xml1, HasSubstr("geom size=\"3\"")); + + // modify the mjModel, save it and read it back + m2->geom_size[0] = .1; + m2->geom_size[3] = .2; + m2->geom_size[6] = .3; + EXPECT_EQ(mj_copyBack(spec, m1), 0); + EXPECT_THAT(mjs_getError(spec), HasSubstr("CopyBack")); + EXPECT_EQ(mj_copyBack(spec, m2), 1); + std::string saved_xml2 = SaveAndReadXml(spec); + EXPECT_THAT(saved_xml2, HasSubstr("geom size=\"0.1\"")); + EXPECT_THAT(saved_xml2, HasSubstr("geom size=\"0.2\"")); + EXPECT_THAT(saved_xml2, HasSubstr("geom size=\"0.3\"")); + + // check that using mjModel as argument writes in the wrong mjSpec + std::string saved_xml3 = SaveAndReadXml(m2); + EXPECT_THAT(saved_xml3, Not(HasSubstr("geom size=\"0.1\""))); + EXPECT_THAT(saved_xml3, Not(HasSubstr("geom size=\"0.2\""))); + EXPECT_THAT(saved_xml3, Not(HasSubstr("geom size=\"0.3\""))); + + mj_deleteSpec(spec); + mj_deleteModel(m1); + mj_deleteModel(m2); +} + TEST_F(DecompilerTest, DoesntSaveInferredStatistics) { static constexpr char xml[] = R"( From fe81373ffe7e648a5cecef16c8cb72dac38fcfa6 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Sat, 31 May 2025 00:55:32 -0700 Subject: [PATCH 190/191] Enable WriteReadCompare for models with assets. PiperOrigin-RevId: 765500742 Change-Id: I119dbea11ed6771839b5e81d04070e95a51d3ed0 --- test/xml/xml_native_writer_test.cc | 76 +++++++++++++++++------------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/test/xml/xml_native_writer_test.cc b/test/xml/xml_native_writer_test.cc index fd87a86d..2ad13bc7 100644 --- a/test/xml/xml_native_writer_test.cc +++ b/test/xml/xml_native_writer_test.cc @@ -1381,19 +1381,30 @@ TEST_F(XMLWriterTest, WriteReadCompare) { std::string xml = p.path().string(); // if file is meant to fail, skip it - if (absl::StrContains(p.path().string(), "100_humanoids") || - absl::StrContains(p.path().string(), "malformed_") || + if (absl::StrContains(p.path().string(), "malformed_") || + // exclude files that are too slow to load absl::StrContains(p.path().string(), "cow") || absl::StrContains(p.path().string(), "gmsh_") || absl::StrContains(p.path().string(), "shark_") || - absl::StrContains(p.path().string(), "frameless_contact_hfield") || - absl::StrContains(p.path().string(), "spheremesh")) { + absl::StrContains(p.path().string(), "spheremesh") || + // exclude files that fail the comparison test + absl::StrContains(p.path().string(), "usd") || + absl::StrContains(p.path().string(), "torus_maxhull") || + absl::StrContains(p.path().string(), "fitmesh_") || + absl::StrContains(p.path().string(), "lengthrange") || + absl::StrContains(p.path().string(), "hfield_xml") || + absl::StrContains(p.path().string(), "fromto_convex") || + absl::StrContains(p.path().string(), "cube_skin") || + absl::StrContains(p.path().string(), "cube_3x3x3")) { continue; } // load model std::array error; - mjModel* m = mj_loadXML( - xml.c_str(), nullptr, error.data(), error.size()); + mjSpec* s = + mj_parseXML(xml.c_str(), nullptr, error.data(), error.size()); + ASSERT_THAT(s, NotNull()) + << "Failed to load " << xml.c_str() << ": " << error.data(); + mjModel* m = mj_compile(s, nullptr); ASSERT_THAT(m, NotNull()) << "Failed to load " << xml.c_str() << ": " << error.data(); @@ -1402,32 +1413,33 @@ TEST_F(XMLWriterTest, WriteReadCompare) { ASSERT_THAT(d, testing::NotNull()) << "Failed to create data\n"; // save and load back - mjModel* mtemp = - LoadModelFromString(SaveAndReadXml(m), error.data(), error.size()); + auto abs_path = p.path(); + mjSpec* stemp = mj_parseXMLString(SaveAndReadXml(s).c_str(), 0, + error.data(), error.size()); + mjs_setString(stemp->modelfiledir, + abs_path.remove_filename().string().c_str()); + mjModel* mtemp = mj_compile(stemp, nullptr); - if (!mtemp) { - // if failing because assets are missing, accept the test - ASSERT_THAT(error.data(), HasSubstr("file")) - << error.data() << " from " << xml.c_str(); - } else { - mjtNum tol = 0; + ASSERT_THAT(mtemp, NotNull()) + << error.data() << " from " << xml.c_str(); - // for particularly sensitive models, relax the tolerance - if (absl::StrContains(p.path().string(), "belt.xml") || - absl::StrContains(p.path().string(), "cable.xml")) { - tol = 1e-13; - } + mjtNum tol = 0; - // compare and delete - std::string field = ""; - mjtNum result = CompareModel(m, mtemp, field); - EXPECT_LE(result, tol) - << "Loaded and saved models are different!\n" - << "Affected file " << p.path().string() << '\n' - << "Different field: " << field << '\n'; - mj_deleteModel(mtemp); + // for particularly sensitive models, relax the tolerance + if (absl::StrContains(p.path().string(), "belt.xml") || + absl::StrContains(p.path().string(), "cable.xml")) { + tol = 1e-13; } + // compare and delete + std::string field = ""; + mjtNum result = CompareModel(m, mtemp, field); + EXPECT_LE(result, tol) + << "Loaded and saved models are different!\n" + << "Affected file " << p.path().string() << '\n' + << "Different field: " << field << '\n'; + mj_deleteModel(mtemp); + // check for stack memory leak mj_step(m, d); EXPECT_EQ(d->pstack, 0) << "mjData stack memory leak detected in " << @@ -1452,21 +1464,21 @@ TEST_F(XMLWriterTest, WriteReadCompare) { ASSERT_THAT(mtemp, NotNull()); // compare with 0 tolerance - std::string field = ""; - mjtNum result = CompareModel(m, mtemp, field); + field = ""; + result = CompareModel(m, mtemp, field); EXPECT_EQ(result, 0) << "Loaded and saved binary models are different!\n" << "Affected file " << p.path().string() << '\n' << "Different field: " << field << '\n'; // clean up + mj_deleteSpec(s); + mj_deleteSpec(stemp); + mj_deleteModel(m); mj_deleteModel(mtemp); mj_deleteVFS(vfs); mju_free(vfs); mju_free(buffer); - - // delete model - mj_deleteModel(m); } } } From 3eb31f56cd9645df8e4fa3bdaf7c266f5836a40f Mon Sep 17 00:00:00 2001 From: Levi Burner Date: Tue, 3 Jun 2025 07:00:24 -0700 Subject: [PATCH 191/191] Copybara import of the project: -- c9acc0a6f677951db34f2b607bd60fc18a43f72b by Levi Burner : Fix race condition in Python viewers set_X methods -- b89ab8f7a0f628ee7a02ebfbac2a97f113112076 by Levi Burner : use std::swap to replace some copying -- 02625bed0cffbfc9ea4537f74689f0bbcb8bf44c by Levi Burner : fix whitespace COPYBARA_INTEGRATE_REVIEW=https://github.com/google-deepmind/mujoco/pull/2613 from aftersomemath:simulate-set-race 02625bed0cffbfc9ea4537f74689f0bbcb8bf44c PiperOrigin-RevId: 766646168 Change-Id: I1e1dc16afbfb1e69fb958d54bd1075cc9aed3569 --- python/mujoco/simulate.cc | 92 +++++++++++++++++++++++++++------------ simulate/simulate.cc | 20 ++++++++- simulate/simulate.h | 8 +++- 3 files changed, 91 insertions(+), 29 deletions(-) diff --git a/python/mujoco/simulate.cc b/python/mujoco/simulate.cc index 336ffb71..a62d1b6b 100644 --- a/python/mujoco/simulate.cc +++ b/python/mujoco/simulate.cc @@ -96,7 +96,6 @@ class SimulateWrapper { void Destroy() { if (simulate_) { - ClearImages(); delete simulate_; simulate_ = nullptr; destroyed_.store(1); @@ -140,40 +139,71 @@ class SimulateWrapper { void SetFigures( const std::vector>& viewports_figures) { - // Pairs of [viewport, figure], where viewport corresponds to the location - // of the figure on the viewer window. - std::vector> user_figures; - for (const auto& [viewport, figure] : viewports_figures) { - mjvFigure casted_figure = *figure.cast().get(); - user_figures.push_back(std::make_pair(viewport, casted_figure)); + + // TODO: replace with atomic wait when we migrate to C++20 + while (simulate_ && simulate_->newfigurerequest.load() != 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); } - // Set them all at once to prevent figure flickering. - simulate_->user_figures_ = user_figures; + // Pairs of [viewport, figure], where viewport corresponds to the location + // of the figure on the viewer window. + for (const auto& [viewport, figure] : viewports_figures) { + mjvFigure casted_figure = *figure.cast().get(); + simulate_->user_figures_new_.push_back(std::make_pair(viewport, casted_figure)); + } + + int value = 0; + simulate_->newfigurerequest.compare_exchange_strong(value, 1); } - void ClearFigures() { simulate_->user_figures_.clear(); } + void ClearFigures() { + // TODO: replace with atomic wait when we migrate to C++20 + while (simulate_ && simulate_->newfigurerequest.load() != 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + simulate_->user_figures_new_.clear(); + + int value = 0; + simulate_->newfigurerequest.compare_exchange_strong(value, 1); + } void SetTexts( const std::vector>& texts) { - // Collection of [font, gridpos, text1, text2] tuples for overlay text - std::vector> user_texts; - for (const auto& [font, gridpos, text1, text2] : texts) { - user_texts.push_back(std::make_tuple(font, gridpos, text1, text2)); + // TODO: replace with atomic wait when we migrate to C++20 + while (simulate_ && simulate_->newtextrequest.load() != 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); } - // Set them all at once to prevent text flickering. - simulate_->user_texts_ = user_texts; + // Collection of [font, gridpos, text1, text2] tuples for overlay text + for (const auto& [font, gridpos, text1, text2] : texts) { + simulate_->user_texts_new_.push_back(std::make_tuple(font, gridpos, text1, text2)); + } + + int value = 0; + simulate_->newtextrequest.compare_exchange_strong(value, 1); } - void ClearTexts() { simulate_->user_texts_.clear(); } + void ClearTexts() { + // TODO: replace with atomic wait when we migrate to C++20 + while (simulate_ && simulate_->newtextrequest.load() != 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + simulate_->user_texts_new_.clear(); + + int value = 0; + simulate_->newtextrequest.compare_exchange_strong(value, 1); + } void SetImages( const std::vector> viewports_images ) { - // Clear previous images to prevent memory leaks - ClearImages(); + // TODO: replace with atomic wait when we migrate to C++20 + while (simulate_ && simulate_->newimagerequest.load() != 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } for (const auto& [viewport, image] : viewports_images) { auto buf = image.request(); @@ -192,20 +222,28 @@ class SimulateWrapper { size_t width = buf.shape[1]; size_t size = height * width * 3; - // Make a copy of the image data to prevent flickering - unsigned char* image_copy = new unsigned char[size]; - std::memcpy(image_copy, buf.ptr, size); + // Make a copy of the image data since Python is + // not required to keep it + std::unique_ptr image_copy(new unsigned char[size]()); + std::memcpy(image_copy.get(), buf.ptr, size); - simulate_->user_images_.push_back(std::make_tuple(viewport, image_copy)); + simulate_->user_images_new_.push_back(std::make_tuple(viewport, std::move(image_copy))); } + + int value = 0; + simulate_->newimagerequest.compare_exchange_strong(value, 1); } void ClearImages() { - // Free memory for each image before clearing the vector - for (const auto& [viewport, image_ptr] : simulate_->user_images_) { - delete[] image_ptr; + // TODO: replace with atomic wait when we migrate to C++20 + while (simulate_ && simulate_->newimagerequest.load() != 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); } - simulate_->user_images_.clear(); + + simulate_->user_images_new_.clear(); + + int value = 0; + simulate_->newimagerequest.compare_exchange_strong(value, 1); } private: diff --git a/simulate/simulate.cc b/simulate/simulate.cc index 66087397..b6a1b9e4 100644 --- a/simulate/simulate.cc +++ b/simulate/simulate.cc @@ -2632,18 +2632,36 @@ void Simulate::Render() { } // user figures + if (this->newfigurerequest.load() == 1) { + this->user_figures_.clear(); + std::swap(this->user_figures_, this->user_figures_new_); + int value = 1; + this->newfigurerequest.compare_exchange_strong(value, 0); + } for (auto& [viewport, figure] : this->user_figures_) { ShowFigure(this, viewport, &figure); } // overlay text + if (this->newtextrequest.load() == 1) { + this->user_texts_.clear(); + std::swap(this->user_texts_, this->user_texts_new_); + int value = 1; + this->newtextrequest.compare_exchange_strong(value, 0); + } for (auto& [font, gridpos, text1, text2] : this->user_texts_) { ShowOverlayText(this, rect, font, gridpos, text1, text2); } // user images + if (this->newimagerequest.load() == 1) { + this->user_images_.clear(); + std::swap(this->user_images_, this->user_images_new_); + int value = 1; + this->newimagerequest.compare_exchange_strong(value, 0); + } for (auto& [viewport, image] : this->user_images_) { - ShowImage(this, viewport, image); + ShowImage(this, viewport, image.get()); } // finalize diff --git a/simulate/simulate.h b/simulate/simulate.h index 70b4ddf4..38234e4c 100644 --- a/simulate/simulate.h +++ b/simulate/simulate.h @@ -206,6 +206,9 @@ class Simulate { std::atomic_int droploadrequest = 0; std::atomic_int screenshotrequest = 0; std::atomic_int uiloadrequest = 0; + std::atomic_int newfigurerequest = 0; + std::atomic_int newtextrequest = 0; + std::atomic_int newimagerequest = 0; // loadrequest // 3: display a loading message @@ -263,8 +266,11 @@ class Simulate { mjvScene* user_scn = nullptr; mjtByte user_scn_flags_prev_[mjNRNDFLAG]; std::vector> user_figures_; + std::vector> user_figures_new_; std::vector> user_texts_; - std::vector> user_images_; + std::vector> user_texts_new_; + std::vector>> user_images_; + std::vector>> user_images_new_; // OpenGL rendering and UI int refresh_rate = 60;

Model Editing