From 7062c6f6772909ec573deb43481b588acaa856b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1lint=20Hodossy?= Date: Fri, 17 Nov 2023 14:43:10 +0000 Subject: [PATCH 001/121] Enable mouse spring perturbations when clicking on a geom in scene view --- unity/Editor/Components/MjMouseSpring.cs | 8 ++++++-- unity/Editor/Components/MjShapeComponentEditor.cs | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/unity/Editor/Components/MjMouseSpring.cs b/unity/Editor/Components/MjMouseSpring.cs index b2264727..c502e551 100644 --- a/unity/Editor/Components/MjMouseSpring.cs +++ b/unity/Editor/Components/MjMouseSpring.cs @@ -23,7 +23,7 @@ namespace Mujoco { // and left mouse drag will apply a force on the body. Holding shift down will change the applied // force direction between World XZ plane and Y[camera-up]. - [CustomEditor(typeof(MjBody))] + [CustomEditor(typeof(MjComponent), true)] public class MjMouseSpring : Editor { private bool _lastShiftKeyState = false; @@ -102,7 +102,11 @@ namespace Mujoco { return; } - MjBody body = target as MjBody; + var targetObject = target as MjComponent; + MjBody body = targetObject.GetComponentInParent(); + if(!body) + return; + Vector3 bodyPosition = body != null ? body.transform.position : Vector3.zero; var scene = MjScene.Instance; diff --git a/unity/Editor/Components/MjShapeComponentEditor.cs b/unity/Editor/Components/MjShapeComponentEditor.cs index 41b8ba22..3d0d687c 100644 --- a/unity/Editor/Components/MjShapeComponentEditor.cs +++ b/unity/Editor/Components/MjShapeComponentEditor.cs @@ -22,7 +22,7 @@ namespace Mujoco { [CustomEditor(typeof(MjShapeComponent), true)] [CanEditMultipleObjects] -public class MjShapeComponentEditor : Editor { +public class MjShapeComponentEditor : MjMouseSpring { public override void OnInspectorGUI() { From f99e9abc3c8dde43eb51aa2b59322568de7e23ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1lint=20Hodossy?= Date: Fri, 17 Nov 2023 15:00:19 +0000 Subject: [PATCH 002/121] Remove no longer necessary directive for editor only compilation (solved by assembly definition) --- unity/Editor/Components/MjMouseSpring.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/unity/Editor/Components/MjMouseSpring.cs b/unity/Editor/Components/MjMouseSpring.cs index c502e551..33ead857 100644 --- a/unity/Editor/Components/MjMouseSpring.cs +++ b/unity/Editor/Components/MjMouseSpring.cs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#if UNITY_EDITOR using System; using UnityEditor; using UnityEngine; @@ -191,5 +190,4 @@ namespace Mujoco { } } } -} -#endif +} \ No newline at end of file From dbf44fe2b4f17f68b6c5190b2a2a26af39ce8a79 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Mon, 20 Nov 2023 05:05:42 -0800 Subject: [PATCH 003/121] New SDF objective function. This solves the jittery behavior observed with the gear example in the case of small applied torques (~0.5). The new quadratic option has the form max(A, 0)^2/2 + max(B, 0)^2/2 - min(A, 0)*min(B, 0). This function has a minimum in the intersections of two SDFs A and B, while avoiding the flat areas which would be generated if only the clearance field A+B were employed. See for example [the function resulting from two colliding circles](https://www.wolframalpha.com/input?i=minimize+max%28sqrt%28x%5E2%2By%5E2%29-1%2C0%29%5E2+%2B+max%28sqrt%28%28x-1%29%5E2%2B%28y-1%29%5E2%29-1%2C0%29%5E2+-+2*min%28sqrt%28x%5E2%2By%5E2%29-1%2C0%29*min%28sqrt%28%28x-1%29%5E2%2B%28y-1%29%5E2%29-1%2C0%29) PiperOrigin-RevId: 583992855 Change-Id: I135a1b5931cd136d7d33cc275f8d361a8b7e290c --- doc/XMLreference.rst | 2 +- doc/programming/extension.rst | 2 +- src/engine/engine_collision_sdf.c | 42 +++++++++++++++++++++++++++---- src/engine/engine_collision_sdf.h | 9 ++++--- 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 1a168853..b77547d3 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -1877,7 +1877,7 @@ adjust it properly through the XML. .. _option-sdf_initpoints: :at:`sdf_initpoints`: :at-val:`int, "40"` - Number of starting points used for fining contacts with Signed Distance Field collisions. + Number of starting points used for finding contacts with Signed Distance Field collisions. .. _option-actuatorgroupdisable: diff --git a/doc/programming/extension.rst b/doc/programming/extension.rst index f41c9629..54674089 100644 --- a/doc/programming/extension.rst +++ b/doc/programming/extension.rst @@ -269,7 +269,7 @@ Currently, there are three directories of first-party plugins: `__. The rest of this section will give more detail concerning the collision algorithm and the plugin engine interface. - Collision points are found by minimizing the maximum of the two colliding SDFs via gradient descent. + Collision points are found by minimizing a quadratic form of the two colliding SDFs via gradient descent. Because SDFs are non-convex, multiple starting points are required in order to converge to multiple local minima. The number of starting points is set using :ref:`sdf_initpoints`, and are initialized using the Halton sequence inside the intersection of the axis-aligned bounding boxes. diff --git a/src/engine/engine_collision_sdf.c b/src/engine/engine_collision_sdf.c index 1e557e1a..ed5b8d06 100644 --- a/src/engine/engine_collision_sdf.c +++ b/src/engine/engine_collision_sdf.c @@ -186,6 +186,18 @@ mjtNum mjc_distance(const mjModel* m, const mjData* d, const mjSDF* s, const mjt mju_addTo3(y, s->relpos); return mju_max(geomDistance(m, d, s->plugin[0], s->id[0], x, s->geomtype[0]), geomDistance(m, d, s->plugin[1], s->id[1], y, s->geomtype[1])); + case mjSDFTYPE_MIDSURFACE: + mju_rotVecMat(y, x, s->relmat); + mju_addTo3(y, s->relpos); + return geomDistance(m, d, s->plugin[0], s->id[0], x, s->geomtype[0]) - + geomDistance(m, d, s->plugin[1], s->id[1], y, s->geomtype[1]); + case mjSDFTYPE_QUADRATIC: + mju_rotVecMat(y, x, s->relmat); + mju_addTo3(y, s->relpos); + mjtNum A = geomDistance(m, d, s->plugin[0], s->id[0], x, s->geomtype[0]); + mjtNum B = geomDistance(m, d, s->plugin[1], s->id[1], y, s->geomtype[1]); + return .5 * mju_max(A, 0) * mju_max(A, 0) + + .5 * mju_max(B, 0) * mju_max(B, 0) - mju_min(A, 0) * mju_min(B, 0); default: mjERROR("SDF type not available"); return 0; @@ -197,6 +209,7 @@ void mjc_gradient(const mjModel* m, const mjData* d, const mjSDF* s, mjtNum gradient[3], const mjtNum x[3]) { mjtNum y[3]; const mjtNum* point[2] = {x, y}; + mjtNum grad1[3], grad2[3]; switch (s->type) { case mjSDFTYPE_INTERSECTION: @@ -209,10 +222,9 @@ void mjc_gradient(const mjModel* m, const mjData* d, const mjSDF* s, mju_rotVecMatT(gradient, gradient, s->relmat); } break; - case mjSDFTYPE_AVERAGE: + case mjSDFTYPE_MIDSURFACE: mju_rotVecMat(y, x, s->relmat); mju_addTo3(y, s->relpos); - mjtNum grad1[3], grad2[3]; geomGradient(grad1, m, d, s->plugin[0], s->id[0], x, s->geomtype[0]); mju_normalize3(grad1); geomGradient(grad2, m, d, s->plugin[1], s->id[1], y, s->geomtype[1]); @@ -221,6 +233,24 @@ void mjc_gradient(const mjModel* m, const mjData* d, const mjSDF* s, mju_sub3(gradient, grad1, grad2); mju_normalize3(gradient); break; + case mjSDFTYPE_QUADRATIC: + mju_rotVecMat(y, x, s->relmat); + mju_addTo3(y, s->relpos); + mjtNum A = geomDistance(m, d, s->plugin[0], s->id[0], x, s->geomtype[0]); + mjtNum B = geomDistance(m, d, s->plugin[1], s->id[1], y, s->geomtype[1]); + geomGradient(grad1, m, d, s->plugin[0], s->id[0], x, s->geomtype[0]); + geomGradient(grad2, m, d, s->plugin[1], s->id[1], y, s->geomtype[1]); + mju_rotVecMatT(grad2, grad2, s->relmat); + gradient[0] = grad1[0] * mju_max(A, 0) + grad2[0] * mju_max(B, 0); + gradient[1] = grad1[1] * mju_max(A, 0) + grad2[1] * mju_max(B, 0); + gradient[2] = grad1[2] * mju_max(A, 0) + grad2[2] * mju_max(B, 0); + if (A < 0 && B < 0) { + gradient[0] = - grad1[0] * B - grad2[0] * A; + gradient[1] = - grad1[1] * B - grad2[1] * A; + gradient[2] = - grad1[2] * B - grad2[2] * A; + } + mju_normalize3(gradient); + break; case mjSDFTYPE_SINGLE: geomGradient(gradient, m, d, s->plugin[0], s->id[0], point[0], s->geomtype[0]); break; @@ -716,10 +746,12 @@ int mjc_SDF(const mjModel* m, const mjData* d, mjContact* con, int g1, int g2, m // start counters sdf_ptr[0]->compute(m, (mjData*)d, instance[0], mjPLUGIN_SDF); - // gradient descent - sdf.type = mjSDFTYPE_INTERSECTION; + // gradient descent - we use a quadratic form of the two SDF as objective + sdf.type = mjSDFTYPE_QUADRATIC; dist = stepGradient(x, m, &sdf, (mjData*)d); - sdf.type = mjSDFTYPE_AVERAGE; + + // contact point and normal - we use the midsurface where SDF1=SDF2 as zero level set + sdf.type = mjSDFTYPE_MIDSURFACE; cnt = addContact(contacts, con, x, pos2true, squat2, dist, cnt, m, &sdf, (mjData*)d); // SHOULD NOT OCCUR diff --git a/src/engine/engine_collision_sdf.h b/src/engine/engine_collision_sdf.h index ef9f3f95..00ee5f07 100644 --- a/src/engine/engine_collision_sdf.h +++ b/src/engine/engine_collision_sdf.h @@ -24,10 +24,11 @@ extern "C" { #endif -typedef enum mjtSDFType_ { - mjSDFTYPE_SINGLE = 0, - mjSDFTYPE_INTERSECTION, - mjSDFTYPE_AVERAGE, +typedef enum mjtSDFType_ { // signed distance function (SDF) type + mjSDFTYPE_SINGLE = 0, // single SDF + mjSDFTYPE_INTERSECTION, // max(A, B) + mjSDFTYPE_MIDSURFACE, // A - B + mjSDFTYPE_QUADRATIC, // max(A, 0)^2/2 + max(B, 0)^2/2 + min(A, 0)*min(B, 0) } mjtSDFType; struct mjSDF_ { From 0ea316ed7174e089811b56771024ae9680cad294 Mon Sep 17 00:00:00 2001 From: Viktor Sonesten Date: Sun, 12 Nov 2023 12:04:10 +0100 Subject: [PATCH 004/121] cmake: support system-wide pybind11 Original behavior remains. --- python/mujoco/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/mujoco/CMakeLists.txt b/python/mujoco/CMakeLists.txt index 8c263139..e5613fee 100644 --- a/python/mujoco/CMakeLists.txt +++ b/python/mujoco/CMakeLists.txt @@ -180,9 +180,10 @@ findorfetch( ) # ==================== PYBIND11 ================================================ +option(MUJOCO_PYTHON_USE_SYSTEM_PYBIND11 "Use installed pybind11 version." OFF) findorfetch( USE_SYSTEM_PACKAGE - OFF + MUJOCO_PYTHON_USE_SYSTEM_PYBIND11 PACKAGE_NAME pybind11 LIBRARY_NAME From 8ca51b5318e9dfd8e212c41268b0e0c6e98f6075 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Mon, 20 Nov 2023 06:31:45 -0800 Subject: [PATCH 005/121] Backtracking line search for SDF collisions. Increased tolerance above zero in nutbolt.xml since now the two parts lock otherwise. Measured speedup of 2x in this example. This solves the issue of scale-dependent step sizes. Tested on gears with 10cm diameter and 2cm thickness. PiperOrigin-RevId: 584009449 Change-Id: Ied2f62dbbbc592470b91cf910f3553802a57c752 --- doc/changelog.rst | 10 ++++++++++ model/plugin/sdf/nutbolt.xml | 4 ++-- plugin/sdf/bolt.cc | 2 +- plugin/sdf/bowl.cc | 2 +- plugin/sdf/gear.cc | 2 +- plugin/sdf/nut.cc | 2 +- plugin/sdf/sdflib.cc | 2 +- src/engine/engine_collision_sdf.c | 33 +++++++++++++++++++++++++------ 8 files changed, 44 insertions(+), 13 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index 25d866a4..471e7105 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -2,6 +2,16 @@ Changelog ========= +Upcoming version (not yet released) +----------------------------------- + +General +^^^^^^^ + +- Improved convergence of Signed Distance Function (SDF) collisions by using line search and a new objective function + for the optimization. This allows to decrease the number of initial points needed for finding the contacts and is more + robust for very small or large geom sizes. + Version 3.0.1 (November 15, 2023) --------------------------------- diff --git a/model/plugin/sdf/nutbolt.xml b/model/plugin/sdf/nutbolt.xml index d3d80ff7..51e2b554 100644 --- a/model/plugin/sdf/nutbolt.xml +++ b/model/plugin/sdf/nutbolt.xml @@ -8,7 +8,7 @@ - + @@ -30,7 +30,7 @@ - - @@ -107,39 +104,33 @@ class ForwardTest(parameterized.TestCase): """) - step_jit_fn = jax.jit(forward.step) - mx = mjx.device_put(m) d = mujoco.MjData(m) # give the system a little kick to ensure we have non-identity rotations - d.qvel = np.random.normal(m.nv) * 0.05 - for i in range(100): - # in order to avoid re-jitting, reuse the same mj_data shape - qpos, qvel = d.qpos, d.qvel - d = mujoco.MjData(m) - d.qpos, d.qvel = qpos, qvel - dx = mjx.device_put(d) + d.qvel = np.array([0.2, -0.1]) + mujoco.mj_step(m, d, 10) # let dynamics get state significantly non-zero + mujoco.mj_forward(m, d) - mujoco.mj_step(m, d) - dx = step_jit_fn(mx, dx) + mx = mjx.put_model(m) + dx = jax.jit(mjx.rungekutta4)(mx, mjx.put_data(m, d)) + mujoco.mj_RungeKutta(m, d, 4) - _assert_attr_eq(d, dx, 'qvel', i, 'test_rk4', atol=1e-2) - _assert_attr_eq(d, dx, 'qpos', i, 'test_rk4', atol=1e-2) - _assert_attr_eq(d, dx, 'act', i, 'test_rk4') - _assert_attr_eq(d, dx, 'time', i, 'test_rk4') + _assert_attr_eq(d, dx, 'qvel') + _assert_attr_eq(d, dx, 'qpos') + _assert_attr_eq(d, dx, 'act') + _assert_attr_eq(d, dx, 'time') def test_disable_eulerdamp(self): - m = test_util.load_test_file('ant.xml') - m.opt.disableflags = m.opt.disableflags | DisableBit.EULERDAMP + m = test_util.load_test_file('pendula.xml') + self.assertTrue((m.dof_damping > 0).any()) + m.opt.disableflags = m.opt.disableflags | mjx.DisableBit.EULERDAMP d = mujoco.MjData(m) - mx = mjx.device_put(m) - self.assertTrue((mx.dof_damping > 0).any()) - dx = mjx.device_put(d) - dx = jax.jit(forward.forward)(mx, dx) + d.qvel[:] = 1.0 + d.qacc[:] = 1.0 + mx = mjx.put_model(m) + dx = jax.jit(mjx.euler)(mx, mjx.put_data(m, d)) - dx = dx.replace(qvel=jp.ones_like(dx.qvel), qacc=jp.ones_like(dx.qacc)) - dx = jax.jit(forward._euler)(mx, dx) np.testing.assert_allclose(dx.qvel, 1 + m.opt.timestep) diff --git a/mjx/mujoco/mjx/_src/io.py b/mjx/mujoco/mjx/_src/io.py index 060ffa50..9658ad53 100644 --- a/mjx/mujoco/mjx/_src/io.py +++ b/mjx/mujoco/mjx/_src/io.py @@ -77,6 +77,7 @@ def _put_statistic(s: mujoco.MjStatistic, device=None) -> types.Statistic: def put_model(m: mujoco.MjModel, device=None) -> types.Model: """Puts mujoco.MjModel onto a device, resulting in mjx.Model.""" + if m.ntendon: raise NotImplementedError('tendons are not supported') @@ -150,7 +151,7 @@ def make_data(m: Union[types.Model, mujoco.MjModel]) -> types.Data: d = types.Data( solver_niter=jp.array(0, dtype=jp.int32), time=jp.array(0.0), - qpos=m.qpos0, + qpos=jp.array(m.qpos0), qvel=zero_nv, act=zero_na, qacc_warmstart=zero_nv, diff --git a/mjx/mujoco/mjx/_src/passive.py b/mjx/mujoco/mjx/_src/passive.py index 9b0b41f0..268de126 100644 --- a/mjx/mujoco/mjx/_src/passive.py +++ b/mjx/mujoco/mjx/_src/passive.py @@ -76,7 +76,7 @@ def _inertia_box_fluid_model( def passive(m: Model, d: Data) -> Data: """Adds all passive forces.""" if m.opt.disableflags & DisableBit.PASSIVE: - return d + return d.replace(qfrc_passive=jp.zeros(m.nv)) # joint-level springs def fn(jnt_typs, stiffness, qpos_spring, qpos): diff --git a/mjx/mujoco/mjx/_src/passive_test.py b/mjx/mujoco/mjx/_src/passive_test.py index 4a5026c8..6ff5a596 100644 --- a/mjx/mujoco/mjx/_src/passive_test.py +++ b/mjx/mujoco/mjx/_src/passive_test.py @@ -14,100 +14,65 @@ # ============================================================================== """Tests passive forces.""" -import itertools - from absl.testing import absltest -from absl.testing import parameterized -from etils import epath import jax -import jax.numpy as jp import mujoco from mujoco import mjx +from mujoco.mjx._src import test_util import numpy as np - -def _assert_attr_eq(a, b, attr, step, fname, atol=1e-4, rtol=1e-4): - err_msg = f'mismatch: {attr} at step {step} in {fname}' - a, b = getattr(a, attr), getattr(b, attr) - np.testing.assert_allclose(a, b, err_msg=err_msg, atol=atol, rtol=rtol) +# tolerance for difference between MuJoCo and MJX passive calculations - mostly +# due to float precision +_TOLERANCE = 1e-7 -class PassiveTest(parameterized.TestCase): +def _assert_eq(a, b, name): + tol = _TOLERANCE * 10 # avoid test noise + err_msg = f'mismatch: {name}' + np.testing.assert_allclose(a, b, err_msg=err_msg, atol=tol, rtol=tol) - @parameterized.parameters(enumerate(('ant.xml', 'pendula.xml'))) - def test_stiffness_damping(self, seed, fname): - """Tests stiffness and damping on Ant.""" - np.random.seed(seed) - path = epath.resource_path('mujoco.mjx') / 'test_data' - path /= fname - m = mujoco.MjModel.from_xml_string(path.read_text()) - # set stiffness/damping - m.jnt_stiffness = np.random.uniform(size=m.njnt) - m.dof_damping = np.random.uniform(size=m.nv) +def _assert_attr_eq(a, b, attr): + _assert_eq(getattr(a, attr), getattr(b, attr), attr) + + +class PassiveTest(absltest.TestCase): + + def test_passive(self): + m = test_util.load_test_file('pendula.xml') d = mujoco.MjData(m) - d.qvel = np.random.random(m.nv) # random kick + # give the system a little kick to ensure we have non-identity rotations + d.ctrl = np.array([0.1, -0.1, 0.2, 0.3, -0.4]) + mujoco.mj_step(m, d, 10) # let dynamics get state significantly non-zero + mujoco.mj_forward(m, d) + mx = mjx.put_model(m) - mx = mjx.device_put(m) - dx = mjx.make_data(mx) + dx = jax.jit(mjx.passive)(mx, mjx.put_data(m, d)) + _assert_attr_eq(d, dx, 'qfrc_passive') - passive_jit_fn = jax.jit(mjx.passive) + # test with fluid forces + m.opt.density = 0.01 + mujoco.mj_forward(m, d) + mx = mjx.put_model(m) + dx = jax.jit(mjx.passive)(mx, mjx.put_data(m, d)) + _assert_attr_eq(d, dx, 'qfrc_passive') - for i in range(100): - qpos, qvel = d.qpos.copy(), d.qvel.copy() - mujoco.mj_step(m, d) - dx = passive_jit_fn(mx, dx.replace(qpos=qpos, qvel=qvel)) - _assert_attr_eq(d, dx, 'qfrc_passive', i, fname) + m.opt.viscosity = 0.02 + mujoco.mj_forward(m, d) + mx = mjx.put_model(m) + dx = jax.jit(mjx.passive)(mx, mjx.put_data(m, d)) + _assert_attr_eq(d, dx, 'qfrc_passive') - @parameterized.parameters( - itertools.product(range(3), ('pendula.xml',)) - ) - def test_fluid(self, seed, fname): - np.random.seed(seed) - path = epath.resource_path('mujoco.mjx') / 'test_data' - path /= fname - m = mujoco.MjModel.from_xml_string(path.read_text()) + m.opt.wind = np.array([0.03, 0.04, 0.05]) + mujoco.mj_forward(m, d) + mx = mjx.put_model(m) + dx = jax.jit(mjx.passive)(mx, mjx.put_data(m, d)) + _assert_attr_eq(d, dx, 'qfrc_passive') - # set density/viscosity/wind - m.opt.density = np.random.uniform() - m.opt.viscosity = np.random.uniform() - m.opt.wind = np.random.uniform() - - passive_jit_fn = jax.jit(mjx.passive) - - mx = mjx.device_put(m) - d = mujoco.MjData(m) - d.qvel = np.random.random(m.nv) # random kick - - for i in range(100): - mujoco.mj_step(m, d) - dx = mjx.device_put(d) - mujoco.mj_passive(m, d) - dx = passive_jit_fn(mx, dx) - _assert_attr_eq(d, dx, 'qfrc_passive', i, fname) - - def test_disable_passive(self): - m = mujoco.MjModel.from_xml_string(""" - - - - - - - - - - """) - mx = mjx.device_put(m) - d = mujoco.MjData(m) - dx = mjx.device_put(d) - dx = dx.replace(qvel=jp.ones(mx.nv)) - - passive_jit_fn = jax.jit(mjx.passive) - dx = passive_jit_fn(mx, dx) - np.testing.assert_equal(dx.qfrc_passive, np.zeros(mx.nv)) + # test disable passive + mx = mx.tree_replace({'opt.disableflags': mjx.DisableBit.PASSIVE}) + dx = jax.jit(mjx.passive)(mx, mjx.put_data(m, d)) + np.testing.assert_allclose(dx.qfrc_passive, 0) if __name__ == '__main__': diff --git a/mjx/mujoco/mjx/_src/scan_test.py b/mjx/mujoco/mjx/_src/scan_test.py index fe4448ca..456067d6 100644 --- a/mjx/mujoco/mjx/_src/scan_test.py +++ b/mjx/mujoco/mjx/_src/scan_test.py @@ -193,7 +193,7 @@ class ScanTest(absltest.TestCase): """ - def testscan_actuators(self): + def test_scan_actuators(self): """Tests scanning over actuators.""" m = mujoco.MjModel.from_xml_string(self._MULTI_ACT_XML) m = mjx.device_put(m) diff --git a/mjx/mujoco/mjx/_src/smooth.py b/mjx/mujoco/mjx/_src/smooth.py index 624291d8..ba470bed 100644 --- a/mjx/mujoco/mjx/_src/smooth.py +++ b/mjx/mujoco/mjx/_src/smooth.py @@ -435,40 +435,38 @@ def transmission(m: Model, d: Data) -> Data: if not m.nu: return d - def fn(gear, jnt_typ, m_i, m_j, qpos): + def fn(gear, jnt_typ, m_j, qpos): # handles joint transmissions only if jnt_typ == JointType.FREE: length = jp.zeros(1) moment = gear - m_i = jp.repeat(m_i, 6) m_j = m_j + jp.arange(6) elif jnt_typ == JointType.BALL: - axis, _ = math.quat_to_axis_angle(qpos) - length = jp.dot(axis, gear[:3])[None] + axis, angle = math.quat_to_axis_angle(qpos) + length = jp.dot(axis * angle, gear[:3])[None] moment = gear[:3] - m_i = jp.repeat(m_i, 3) m_j = m_j + jp.arange(3) elif jnt_typ in (JointType.SLIDE, JointType.HINGE): length = qpos * gear[0] moment = gear[:1] - m_i, m_j = m_i[None], m_j[None] + m_j = m_j[None] else: raise RuntimeError(f'unrecognized joint type: {jnt_typ}') - return length, moment, m_i, m_j + moment = jp.zeros((m.nv,)).at[m_j].set(moment) + return length, moment - length, m_val, m_i, m_j = scan.flat( + length, moment = scan.flat( m, fn, - 'ujujq', - 'uvvv', + 'ujjq', + 'uuuu', m.actuator_gear, m.jnt_type, - jp.arange(m.nu), jp.array(m.jnt_dofadr), d.qpos, group_by='u', ) - moment = jp.zeros((m.nu, m.nv)).at[m_i, m_j].set(m_val) length = length.reshape((m.nu,)) + moment = moment.reshape((m.nu, m.nv)) d = d.replace(actuator_length=length, actuator_moment=moment) return d diff --git a/mjx/mujoco/mjx/_src/smooth_test.py b/mjx/mujoco/mjx/_src/smooth_test.py index bd9348bb..203cb654 100644 --- a/mjx/mujoco/mjx/_src/smooth_test.py +++ b/mjx/mujoco/mjx/_src/smooth_test.py @@ -15,122 +15,107 @@ """Tests for smooth 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 test_util -# pylint: disable=g-importing-member -from mujoco.mjx._src.types import DisableBit -# pylint: enable=g-importing-member import numpy as np - -def _assert_eq(a, b, name, step, fname, atol=5e-4, rtol=5e-4): - err_msg = f'mismatch: {name} at step {step} in {fname}' - np.testing.assert_allclose(a, b, err_msg=err_msg, atol=atol, rtol=rtol) +# tolerance for difference between MuJoCo and MJX smooth calculations - mostly +# due to float precision +_TOLERANCE = 5e-5 -def _assert_attr_eq(a, b, attr, step, fname, atol=5e-4, rtol=5e-4): - err_msg = f'mismatch: {attr} at step {step} in {fname}' - a, b = getattr(a, attr), getattr(b, attr) - np.testing.assert_allclose(a, b, err_msg=err_msg, atol=atol, rtol=rtol) +def _assert_eq(a, b, name): + tol = _TOLERANCE * 10 # avoid test noise + err_msg = f'mismatch: {name}' + np.testing.assert_allclose(a, b, err_msg=err_msg, atol=tol, rtol=tol) -class SmoothTest(parameterized.TestCase): +def _assert_attr_eq(a, b, attr): + _assert_eq(getattr(a, attr), getattr(b, attr), attr) - @parameterized.parameters(enumerate(test_util.TEST_FILES)) - def test_smooth(self, seed, fname): - """Tests mujoco mj smooth functions match mujoco_mjx smooth functions.""" - if fname in ('convex.xml', 'equality.xml'): - return - np.random.seed(seed) +class SmoothTest(absltest.TestCase): - m = test_util.load_test_file(fname) + def setUp(self): + super().setUp() + # although we already have generous padding of thresholds, it doesn't hurt + # to also fix the seed to reduce test flakiness + np.random.seed(0) + + def test_smooth(self): + """Tests MJX smooth functions match MuJoCo smooth functions.""" + + m = test_util.load_test_file('pendula.xml') d = mujoco.MjData(m) - - kinematics_jit_fn = jax.jit(mjx.kinematics) - com_pos_jit_fn = jax.jit(mjx.com_pos) - crb_jit_fn = jax.jit(mjx.crb) - factor_m_fn = jax.jit(mjx.factor_m) - com_vel_jit_fn = jax.jit(mjx.com_vel) - rne_jit_fn = jax.jit(mjx.rne) - mul_m_jit_fn = jax.jit(mjx.mul_m) - transmission_jit_fn = jax.jit(mjx.transmission) - - mx = mjx.device_put(m) - dx = mjx.make_data(mx) - # give the system a little kick to ensure we have non-identity rotations d.qvel = np.random.random(m.nv) - for i in range(100): - qpos, qvel = d.qpos.copy(), d.qvel.copy() - mujoco.mj_step(m, d) + mujoco.mj_step(m, d, 10) # let dynamics get state significantly non-zero + mujoco.mj_forward(m, d) + mx = mjx.put_model(m) - # kinematics - dx = kinematics_jit_fn(mx, dx.replace(qpos=qpos, qvel=qvel)) - _assert_attr_eq(d, dx, 'xanchor', i, fname) - _assert_attr_eq(d, dx, 'xaxis', i, fname) - _assert_attr_eq(d, dx, 'xpos', i, fname) - _assert_attr_eq(d, dx, 'xquat', i, fname) - _assert_eq(d.xmat.reshape((-1, 3, 3)), dx.xmat, 'xmat', i, fname) - _assert_attr_eq(d, dx, 'xipos', i, fname) - _assert_eq(d.ximat.reshape((-1, 3, 3)), dx.ximat, 'ximat', i, fname) - _assert_attr_eq(d, dx, 'geom_xpos', i, fname) - _assert_eq( - d.geom_xmat.reshape((-1, 3, 3)), - dx.geom_xmat, - 'geom_xmat', - i, - fname, - ) + # kinematics + dx = jax.jit(mjx.kinematics)(mx, mjx.put_data(m, d)) + _assert_attr_eq(d, dx, 'xanchor') + _assert_attr_eq(d, dx, 'xaxis') + _assert_attr_eq(d, dx, 'xpos') + _assert_attr_eq(d, dx, 'xquat') + _assert_eq(d.xmat.reshape((-1, 3, 3)), dx.xmat, 'xmat') + _assert_attr_eq(d, dx, 'xipos') + _assert_eq(d.ximat.reshape((-1, 3, 3)), dx.ximat, 'ximat') + _assert_attr_eq(d, dx, 'geom_xpos') + _assert_eq(d.geom_xmat.reshape((-1, 3, 3)), dx.geom_xmat, 'geom_xmat') + _assert_attr_eq(d, dx, 'site_xpos') + _assert_eq(d.site_xmat.reshape((-1, 3, 3)), dx.site_xmat, 'site_xmat') + # 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') + # crb + dx = jax.jit(mjx.crb)(mx, mjx.put_data(m, d)) + _assert_attr_eq(d, dx, 'crb') + _assert_attr_eq(d, dx, 'qM') + # factor_m + dx = mjx.put_data(m, d) + dx = jax.jit(mjx.factor_m)(mx, dx, dx.qM) + _assert_attr_eq(d, dx, 'qLD') + _assert_attr_eq(d, dx, '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') + # rne + dx = jax.jit(mjx.rne)(mx, mjx.put_data(m, d)) + _assert_attr_eq(d, dx, 'qfrc_bias') + # transmission + dx = jax.jit(mjx.transmission)(mx, mjx.put_data(m, d)) + _assert_attr_eq(d, dx, 'actuator_length') + _assert_attr_eq(d, dx, 'actuator_moment') - # com_pos - dx = com_pos_jit_fn(mx, dx) - _assert_attr_eq(d, dx, 'subtree_com', i, fname) - _assert_attr_eq(d, dx, 'cinert', i, fname) - _assert_attr_eq(d, dx, 'cdof', i, fname) + def test_mul_m(self): + m = test_util.load_test_file('pendula.xml') + d = mujoco.MjData(m) + # give the system a little kick to ensure we have non-identity rotations + d.qvel = np.random.random(m.nv) + mujoco.mj_step(m, d, 10) # let dynamics get state significantly non-zero + mujoco.mj_forward(m, d) + mx = mjx.put_model(m) + dx = mjx.put_data(m, d) + vec = np.random.random(m.nv) + mjx_vec = jax.jit(mjx.mul_m)(mx, dx, jp.array(vec)) + mj_vec = np.zeros(m.nv) + mujoco.mj_mulM(m, d, mj_vec, vec) + _assert_eq(mj_vec, mjx_vec, 'mul_m') - # crb - dx = crb_jit_fn(mx, dx) - _assert_attr_eq(d, dx, 'crb', i, fname) - _assert_attr_eq(d, dx, 'qM', i, fname) - - # factor_m - dx = factor_m_fn(mx, dx, dx.qM) - _assert_attr_eq(d, dx, 'qLD', i, fname, atol=1e-3) - _assert_attr_eq(d, dx, 'qLDiagInv', i, fname, atol=1e-3) - - # com_vel - dx = com_vel_jit_fn(mx, dx) - _assert_attr_eq(d, dx, 'cvel', i, fname) - _assert_attr_eq(d, dx, 'cdof_dot', i, fname) - - # rne - dx = rne_jit_fn(mx, dx) - _assert_attr_eq(d, dx, 'qfrc_bias', i, fname) - - # mul_m (auxilliary function, not part of smooth step) - vec = np.random.random(m.nv) - mjx_vec = mul_m_jit_fn(mx, dx, jp.array(vec)) - mj_vec = np.zeros(m.nv) - mujoco.mj_mulM(m, d, mj_vec, vec) - _assert_eq(mj_vec, mjx_vec, 'mul_m', i, fname) - - # transmission - dx = transmission_jit_fn(mx, dx) - _assert_attr_eq(d, dx, 'actuator_length', i, fname) - _assert_attr_eq(d, dx, 'actuator_moment', i, fname) - - -class DisableGravityTest(absltest.TestCase): - - def test_disabled(self): + def test_disable_gravity(self): m = mujoco.MjModel.from_xml_string(""" - @@ -139,63 +124,13 @@ class DisableGravityTest(absltest.TestCase): """) - mx = mjx.device_put(m) d = mujoco.MjData(m) - dx = mjx.device_put(d) - - # test with gravity - step_jit_fn = jax.jit(mjx.step) - dx = step_jit_fn(mx, dx) - np.testing.assert_array_almost_equal( - dx.qpos, np.array([0.0, 0.0, -9.81e-4, 1.0, 0.0, 0.0, 0.0]), decimal=7 - ) - - # test with gravity disabled - mx = mx.tree_replace( - {'opt.disableflags': mx.opt.disableflags | DisableBit.GRAVITY} - ) - dx = mjx.device_put(d) - step_jit_fn = jax.jit(mjx.step) - dx = step_jit_fn(mx, dx) - np.testing.assert_equal( - dx.qpos, np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) - ) - - -class SiteTest(absltest.TestCase): - - def test_site(self): - """Tests that site positions and orientations match MuJoCo.""" - m = mujoco.MjModel.from_xml_string(""" - - - - - - - - - - - - - - - - """) - d = mujoco.MjData(m) - - mx = mjx.device_put(m) - dx = mjx.device_put(d) - mujoco.mj_forward(m, d) - dx = mjx.forward(mx, dx) - - np.testing.assert_array_almost_equal(dx.site_xpos, d.site_xpos) - np.testing.assert_array_almost_equal( - dx.site_xmat, d.site_xmat.reshape((-1, 3, 3)) - ) + mx = mjx.put_model(m) + dx = mjx.put_data(m, d) + dx = jax.jit(mjx.rne)(mx, dx) + np.testing.assert_allclose(dx.qfrc_bias, 0) if __name__ == '__main__': absltest.main() diff --git a/mjx/mujoco/mjx/_src/solver_test.py b/mjx/mujoco/mjx/_src/solver_test.py index 6a4f7792..d1677ff9 100644 --- a/mjx/mujoco/mjx/_src/solver_test.py +++ b/mjx/mujoco/mjx/_src/solver_test.py @@ -12,119 +12,64 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== -"""Tests for forward functions.""" +"""Tests for constraint functions.""" from absl.testing import absltest -from absl.testing import parameterized -from etils import epath import jax import mujoco from mujoco import mjx +from mujoco.mjx._src import test_util import numpy as np -def _assert_attr_eq(a, b, attr, step, fname, atol=1e-2, rtol=1e-2): - err_msg = f'mismatch: {attr} at step {step} in {fname}' - a, b = getattr(a, attr), getattr(b, attr) - np.testing.assert_allclose(a, b, err_msg=err_msg, atol=atol, rtol=rtol) +# tolerance for difference between MuJoCo and MJX constraint calculations, +# mostly due to float precision +_TOLERANCE = 5e-5 -class Solver64Test(parameterized.TestCase): - """Tests solvers at 64 bit precision.""" +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) - def setUp(self): - super().setUp() - jax.config.update('jax_enable_x64', True) - def tearDown(self): - super().tearDown() - jax.config.update('jax_enable_x64', False) +def _assert_attr_eq(a, b, attr): + _assert_eq(getattr(a, attr), getattr(b, attr), attr) - @parameterized.parameters(enumerate(('ant.xml', 'humanoid.xml'))) - def test_cg(self, seed, fname): - """Test mjx cg solver matches mujoco cg solver at 64 bit precision.""" - f = epath.resource_path('mujoco.mjx') / 'test_data' / fname - m = mujoco.MjModel.from_xml_string(f.read_text()) + +class SolverTest(absltest.TestCase): + + def test_solver(self): + """Test solver.""" + m = test_util.load_test_file('constraints.xml') d = mujoco.MjData(m) - mx = mjx.device_put(m) + mujoco.mj_step(m, d, 100) # at 100 steps mix of active/inactive constraints + mujoco.mj_forward(m, d) + mx = mjx.put_model(m) - jax.config.update('jax_enable_x64', True) - forward_jit_fn = jax.jit(mjx.forward) + dx = jax.jit(mjx.solve)(mx, mjx.put_data(m, d)) + _assert_attr_eq(d, dx, 'qacc_warmstart') + _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') - # give the system a little kick to ensure we have non-identity rotations - np.random.seed(seed) - d.qvel = 0.01 * np.random.random(m.nv) - - for i in range(100): - # in order to avoid re-jitting, reuse the same mj_data shape - save = d.qpos, d.qvel, d.time, d.qacc_warmstart, d.qacc_smooth - d = mujoco.MjData(m) - d.qpos, d.qvel, d.time, d.qacc_warmstart, d.qacc_smooth = save - dx = mjx.device_put(d) - - mujoco.mj_step(m, d) - dx = forward_jit_fn(mx, dx) - - # at 64 bits the solutions returned by the two solvers are quite close - self.assertLessEqual(dx.solver_niter[0], d.solver_niter[0]) - _assert_attr_eq(d, dx, 'qfrc_constraint', i, fname) - _assert_attr_eq(d, dx, 'qacc', i, fname) - - -class SolverTest(parameterized.TestCase): - - @parameterized.parameters(enumerate(('ant.xml', 'humanoid.xml'))) - def test_cg(self, seed, fname): - """Test mjx cg solver is close to mj at 32 bit precision. - - Args: - seed: int - fname: file to test - - At lower float resolution there's wiggle room in valid forces that satisfy - constraints. So instead let's mainly validate that mjx is finding solutions - with as good cost as mujoco, even if the resulting forces/accelerations - are not quite the same. - """ - f = epath.resource_path('mujoco.mjx') / 'test_data' / fname - m = mujoco.MjModel.from_xml_string(f.read_text()) - d = mujoco.MjData(m) - mx = mjx.device_put(m) - - forward_jit_fn = jax.jit(mjx.forward) - - # give the system a little kick to ensure we have non-identity rotations - np.random.seed(seed) - d.qvel = 0.01 * np.random.random(m.nv) - - for i in range(100): - # in order to avoid re-jitting, reuse the same mj_data shape - save = d.qpos, d.qvel, d.time, d.qacc_warmstart, d.qacc_smooth - d = mujoco.MjData(m) - d.qpos, d.qvel, d.time, d.qacc_warmstart, d.qacc_smooth = save - dx = mjx.device_put(d) - - mujoco.mj_step(m, d) - dx = forward_jit_fn(mx, dx) - - def cost(qacc): - jaref = np.zeros(d.nefc) - mujoco.mj_mulJacVec(m, d, jaref, qacc) - jaref -= d.efc_aref - cost = np.array([0.0]) - mujoco.mj_constraintUpdate(m, d, jaref, cost, 0) - return cost[0] - - cost_mj, cost_mjx = cost(d.qacc), cost(dx.qacc) - - self.assertLessEqual( - cost_mjx, - cost_mj * 1.01, - msg=f'mismatch: {fname} at step {i}, cost too high', - ) - _assert_attr_eq(d, dx, 'qfrc_constraint', i, fname, atol=1e-1, rtol=1e-1) - _assert_attr_eq(d, dx, 'qacc', i, fname, atol=1e-1, rtol=1e-1) + # also test normal CG + m.opt.solver = mujoco.mjtSolver.mjSOL_CG + mujoco.mj_forward(m, d) + dx = jax.jit(mjx.solve)(mx, mjx.put_data(m, d)) + _assert_attr_eq(d, dx, 'qacc_warmstart') + _assert_attr_eq(d, dx, 'qacc') + _assert_attr_eq(d, dx, 'qfrc_constraint') + _assert_eq(d.efc_force, dx.efc_force[nnz], 'efc_force') + # without warmstart, the solution is not as close + m.opt.solver = mujoco.mjtSolver.mjSOL_NEWTON + m.opt.disableflags |= mujoco.mjtDisableBit.mjDSBL_WARMSTART + mujoco.mj_forward(m, d) + mx = mjx.put_model(m) + dx = jax.jit(mjx.solve)(mx, mjx.put_data(m, d)) + _assert_eq(d.efc_force, dx.efc_force[nnz], 'efc_force', tol=2e-2) if __name__ == '__main__': absltest.main() diff --git a/mjx/mujoco/mjx/_src/support_test.py b/mjx/mujoco/mjx/_src/support_test.py index fe88fc84..fb3a5389 100644 --- a/mjx/mujoco/mjx/_src/support_test.py +++ b/mjx/mujoco/mjx/_src/support_test.py @@ -34,8 +34,8 @@ class SupportTest(parameterized.TestCase): m = test_util.load_test_file(fname) d = mujoco.MjData(m) mujoco.mj_step(m, d) - mx = mjx.device_put(m) - dx = mjx.device_put(d) + mx = mjx.put_model(m) + dx = mjx.put_data(m, d) point = np.random.randn(3) body = np.random.choice(m.nbody) jacp, jacr = jax.jit(support.jac)(mx, dx, point, body) @@ -49,11 +49,11 @@ class SupportTest(parameterized.TestCase): """Tests that xfrc_accumulate ouput matches mj_xfrcAccumulate.""" np.random.seed(0) - m = test_util.load_test_file('ant.xml') + m = test_util.load_test_file('pendula.xml') d = mujoco.MjData(m) mujoco.mj_step(m, d) - mx = mjx.device_put(m) - dx = mjx.device_put(d) + mx = mjx.put_model(m) + dx = mjx.put_data(m, d) self.assertFalse((dx.xipos == 0.0).all()) xfrc = np.random.rand(*dx.xfrc_applied.shape) diff --git a/mjx/mujoco/mjx/_src/test_util.py b/mjx/mujoco/mjx/_src/test_util.py index 8d765644..870621f7 100644 --- a/mjx/mujoco/mjx/_src/test_util.py +++ b/mjx/mujoco/mjx/_src/test_util.py @@ -23,10 +23,8 @@ import mujoco import numpy as np TEST_FILES: List[str] = [ - 'ant.xml', + 'constraints.xml', 'convex.xml', - 'equality.xml', - 'humanoid.xml', 'pendula.xml', ] diff --git a/mjx/mujoco/mjx/integration_test/collision_driver_test.py b/mjx/mujoco/mjx/integration_test/collision_driver_test.py index 1e28b053..a9891656 100644 --- a/mjx/mujoco/mjx/integration_test/collision_driver_test.py +++ b/mjx/mujoco/mjx/integration_test/collision_driver_test.py @@ -58,9 +58,9 @@ class CollisionDriverIntegrationTest(parameterized.TestCase): ) m = mujoco.MjModel.from_xml_string(mjcf) - mx = mjx.device_put(m) + mx = mjx.put_model(m) d = mujoco.MjData(m) - dx = mjx.device_put(d) + dx = mjx.put_data(m, d) mujoco.mj_step(m, d) collision_jit_fn = jax.jit(mjx.collision) diff --git a/mjx/mujoco/mjx/integration_test/forward_test.py b/mjx/mujoco/mjx/integration_test/forward_test.py index 6fa913c4..67f20371 100644 --- a/mjx/mujoco/mjx/integration_test/forward_test.py +++ b/mjx/mujoco/mjx/integration_test/forward_test.py @@ -19,7 +19,6 @@ from absl.testing import parameterized import jax import mujoco from mujoco import mjx -from mujoco.mjx._src import forward from mujoco.mjx._src import test_util import numpy as np @@ -46,7 +45,7 @@ class ActuationIntegrationTest(parameterized.TestCase): enable_contact=False, ) m = mujoco.MjModel.from_xml_string(mjcf) - actuation_jit_fn = jax.jit(forward._actuation) + actuation_jit_fn = jax.jit(mjx.fwd_actuation) # init d = mujoco.MjData(m) @@ -57,8 +56,8 @@ class ActuationIntegrationTest(parameterized.TestCase): mujoco.mj_fwdVelocity(m, d) # put on device - mx = mjx.device_put(m) - dx = mjx.device_put(d) + mx = mjx.put_model(m) + dx = mjx.put_data(m, d) mujoco.mj_fwdActuation(m, d) dx = actuation_jit_fn(mx, dx) diff --git a/mjx/mujoco/mjx/integration_test/smooth_test.py b/mjx/mujoco/mjx/integration_test/smooth_test.py index c924e005..d032be79 100644 --- a/mjx/mujoco/mjx/integration_test/smooth_test.py +++ b/mjx/mujoco/mjx/integration_test/smooth_test.py @@ -60,8 +60,8 @@ class TransmissionIntegrationTest(parameterized.TestCase): d.qvel = np.random.random(m.nv) # put on device - mx = mjx.device_put(m) - dx = mjx.device_put(d) + mx = mjx.put_model(m) + dx = mjx.put_data(m, d) mujoco.mj_transmission(m, d) dx = transmission_jit_fn(mx, dx) diff --git a/mjx/mujoco/mjx/test_data/ant.xml b/mjx/mujoco/mjx/test_data/ant.xml deleted file mode 100644 index 7417c3ae..00000000 --- a/mjx/mujoco/mjx/test_data/ant.xml +++ /dev/null @@ -1,82 +0,0 @@ - - - diff --git a/mjx/mujoco/mjx/test_data/constraints.xml b/mjx/mujoco/mjx/test_data/constraints.xml new file mode 100644 index 00000000..52eb95d8 --- /dev/null +++ b/mjx/mujoco/mjx/test_data/constraints.xml @@ -0,0 +1,53 @@ + + + diff --git a/mjx/mujoco/mjx/test_data/equality.xml b/mjx/mujoco/mjx/test_data/equality.xml deleted file mode 100644 index e5c9d184..00000000 --- a/mjx/mujoco/mjx/test_data/equality.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mjx/mujoco/mjx/test_data/humanoid.xml b/mjx/mujoco/mjx/test_data/humanoid.xml deleted file mode 100644 index 2d7158ee..00000000 --- a/mjx/mujoco/mjx/test_data/humanoid.xml +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - - diff --git a/mjx/mujoco/mjx/test_data/pendula.xml b/mjx/mujoco/mjx/test_data/pendula.xml index 2363a3f8..0dc476ab 100644 --- a/mjx/mujoco/mjx/test_data/pendula.xml +++ b/mjx/mujoco/mjx/test_data/pendula.xml @@ -18,6 +18,8 @@ + + @@ -26,45 +28,49 @@ - + + - + + - + + - - + + - + - + - + - + + @@ -72,14 +78,14 @@ - + - + - + @@ -89,14 +95,21 @@ - + - + - + + + + + + + + From 762371c3e4c5ed7204d2533411956c34b498baf5 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Wed, 6 Dec 2023 04:27:34 -0800 Subject: [PATCH 044/121] Add `kv` damping attribute to `position` and `intvelocity` actuator shortcuts. PiperOrigin-RevId: 588377097 Change-Id: I8ded2ff982e52ed8673019979e29b706ef65c743 --- doc/XMLreference.rst | 39 ++- doc/XMLschema.rst | 8 +- doc/changelog.rst | 4 + src/user/user_objects.cc | 2 +- src/xml/xml_native_reader.cc | 51 ++-- test/engine/testdata/actuation/refsite.xml | 16 +- test/user/user_objects_test.cc | 4 +- test/xml/xml_native_reader_test.cc | 268 ++++++++++++++------- 8 files changed, 250 insertions(+), 142 deletions(-) diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index fa963647..2f423abf 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -5317,13 +5317,13 @@ This element does not have custom attributes. It only has common attributes, whi This element creates a position servo. The underlying :el:`general` attributes are set as follows: -========= ======= ========= ======= +========= ======= ========= ========= Attribute Setting Attribute Setting -========= ======= ========= ======= +========= ======= ========= ========= dyntype none dynprm 1 0 0 gaintype fixed gainprm kp 0 0 -biastype affine biasprm 0 -kp 0 -========= ======= ========= ======= +biastype affine biasprm 0 -kp -kv +========= ======= ========= ========= This element has one custom attribute in addition to the common attributes: @@ -5377,6 +5377,11 @@ This element has one custom attribute in addition to the common attributes: :at:`kp`: :at-val:`real, "1"` Position feedback gain. +.. _actuator-position-kv: + +:at:`kv`: :at-val:`real, "0"` + Damping applied by the actuator. + When using this attribute, it is recommended to use the implicitfast or implicit :ref:`integrators`. .. _actuator-velocity: @@ -5385,7 +5390,9 @@ This element has one custom attribute in addition to the common attributes: This element creates a velocity servo. Note that in order create a PD controller, one has to define two actuators: a position servo and a velocity servo. This is because MuJoCo actuators are SISO while a PD controller takes two control -inputs (reference position and reference velocity). The underlying :el:`general` attributes are set as follows: +inputs (reference position and reference velocity). +When using this actuator, it is recommended to use the implicitfast or implicit :ref:`integrators`. +The underlying :el:`general` attributes are set as follows: ========= ======= ========= ======= Attribute Setting Attribute Setting @@ -5456,14 +5463,14 @@ This element creates an integrated-velocity servo. For more information, see the :ref:`Activation clamping ` section of the Modeling chapter. The underlying :el:`general` attributes are set as follows: -========== =========== ========= ======= +========== =========== ========= ========= Attribute Setting Attribute Setting -========== =========== ========= ======= +========== =========== ========= ========= dyntype integrator dynprm 1 0 0 gaintype fixed gainprm kp 0 0 -biastype affine biasprm 0 -kp 0 +biastype affine biasprm 0 -kp -kv actlimited true -========== =========== ========= ======= +========== =========== ========= ========= This element has one custom attribute in addition to the common attributes: @@ -5518,6 +5525,11 @@ This element has one custom attribute in addition to the common attributes: :at:`kp`: :at-val:`real, "1"` Position feedback gain. +.. _actuator-intvelocity-kv: + +:at:`kv`: :at-val:`real, "0"` + Damping applied by the actuator. + When using this attribute, it is recommended to use the implicitfast or implicit :ref:`integrators`. .. _actuator-damper: @@ -5525,8 +5537,9 @@ This element has one custom attribute in addition to the common attributes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This element is an active damper which produces a force proportional to both velocity and control: ``F = - kv * velocity -* control``, where ``kv`` must be nonnegative. :at:`ctrlrange` is required and must also be nonnegative. The underlying -:el:`general` attributes are set as follows: +* control``, where ``kv`` must be nonnegative. :at:`ctrlrange` is required and must also be nonnegative. +When using this actuator, it is recommended to use the implicitfast or implicit :ref:`integrators`. +The underlying :el:`general` attributes are set as follows: =========== ======= ========= ======= Attribute Setting Attribute Setting @@ -7680,6 +7693,8 @@ slidersite, cranksite. .. _default-position-kp: +.. _default-position-kv: + :el-prefix:`default/` |-| **position** (?) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -7736,6 +7751,8 @@ tendon, slidersite, cranksite. .. _default-intvelocity-kp: +.. _default-intvelocity-kv: + :el-prefix:`default/` |-| **intvelocity** (?) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/XMLschema.rst b/doc/XMLschema.rst index a8d0d0b3..702adeb2 100644 --- a/doc/XMLschema.rst +++ b/doc/XMLschema.rst @@ -778,7 +778,7 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | | | | | :ref:`jointinparent` | :ref:`tendon` | :ref:`slidersite` | :ref:`cranksite` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`site` | :ref:`refsite` | :ref:`kp` | | | +| | | | :ref:`site` | :ref:`refsite` | :ref:`kp` | :ref:`kv` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| actuator |br| |_| |L| | | .. table:: | @@ -810,6 +810,8 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | | | | | :ref:`cranksite` | :ref:`site` | :ref:`refsite` | :ref:`kp` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +| | | | :ref:`kv` | | | | | +| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| actuator |br| |_| |L| | | .. table:: | | :ref:`damper | \* | :class: mjcf-attributes | @@ -1440,7 +1442,7 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | | | | | :ref:`gear` | :ref:`cranklength` | :ref:`user` | :ref:`group` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`kp` | | | | | +| | | | :ref:`kp` | :ref:`kv` | | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| default |br| |_| |L| | | .. table:: | @@ -1462,7 +1464,7 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | | | | | :ref:`actrange` | :ref:`gear` | :ref:`cranklength` | :ref:`user` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`group` | :ref:`kp` | | | | +| | | | :ref:`group` | :ref:`kp` | :ref:`kv` | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| default |br| |_| |L| | | .. table:: | diff --git a/doc/changelog.rst b/doc/changelog.rst index 826c1a1d..56904ed8 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -12,6 +12,10 @@ General robust for very small or large geom sizes. - Added :ref:`frame` to MJCF, a :ref:`meta-element` which defines a pure coordinate transformation on its direct children, without requiring a :ref:`body`. +- Added the :at:`kv` attribute to the :ref:`position` and :ref:`intvelocity` + actuators, for specifying actuator-applied damping. This can be used to implement a PD controller with 0 reference + velocity. When using this attribute, it is recommended to use the implicitfast or implicit + :ref:`integrators`. Plugins ^^^^^^^ diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 3d7ef0b8..009ecebc 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -3876,7 +3876,7 @@ void mjCActuator::Compile(void) { throw mjCError(this, "invalid control range for actuator '%s' (id = %d)", name.c_str(), id); } if (actrange[0]>=actrange[1] && actlimited) { - throw mjCError(this, "invalid activation range for actuator '%s' (id = %d)", name.c_str(), id); + throw mjCError(this, "invalid actrange for actuator '%s' (id = %d)", name.c_str(), id); } if (actlimited && dyntype == mjDYN_NONE) { throw mjCError(this, "actrange specified but dyntype is 'none' in actuator '%s' (id = %d)", diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 4121b2b9..57671db9 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -166,16 +166,16 @@ static const char* MJCF[nMJCF][mjXATTRNUM] = { "dyntype", "gaintype", "biastype", "dynprm", "gainprm", "biasprm", "actearly"}, {"motor", "?", "8", "ctrllimited", "forcelimited", "ctrlrange", "forcerange", "gear", "cranklength", "user", "group"}, - {"position", "?", "9", "ctrllimited", "forcelimited", "ctrlrange", "forcerange", + {"position", "?", "10", "ctrllimited", "forcelimited", "ctrlrange", "forcerange", "gear", "cranklength", "user", "group", - "kp"}, + "kp", "kv"}, {"velocity", "?", "9", "ctrllimited", "forcelimited", "ctrlrange", "forcerange", "gear", "cranklength", "user", "group", "kv"}, - {"intvelocity", "?", "10", "ctrllimited", "forcelimited", + {"intvelocity", "?", "11", "ctrllimited", "forcelimited", "ctrlrange", "forcerange", "actrange", "gear", "cranklength", "user", "group", - "kp"}, + "kp", "kv"}, {"damper", "?", "8", "forcelimited", "ctrlrange", "forcerange", "gear", "cranklength", "user", "group", "kv"}, @@ -379,22 +379,22 @@ static const char* MJCF[nMJCF][mjXATTRNUM] = { "ctrllimited", "forcelimited", "ctrlrange", "forcerange", "lengthrange", "gear", "cranklength", "user", "joint", "jointinparent", "tendon", "slidersite", "cranksite", "site", "refsite"}, - {"position", "*", "19", "name", "class", "group", + {"position", "*", "20", "name", "class", "group", "ctrllimited", "forcelimited", "ctrlrange", "forcerange", "lengthrange", "gear", "cranklength", "user", "joint", "jointinparent", "tendon", "slidersite", "cranksite", "site", "refsite", - "kp"}, + "kp", "kv"}, {"velocity", "*", "19", "name", "class", "group", "ctrllimited", "forcelimited", "ctrlrange", "forcerange", "lengthrange", "gear", "cranklength", "user", "joint", "jointinparent", "tendon", "slidersite", "cranksite", "site", "refsite", "kv"}, - {"intvelocity", "*", "20", "name", "class", "group", + {"intvelocity", "*", "21", "name", "class", "group", "ctrllimited", "forcelimited", "ctrlrange", "forcerange", "actrange", "lengthrange", "gear", "cranklength", "user", "joint", "jointinparent", "tendon", "slidersite", "cranksite", "site", "refsite", - "kp"}, + "kp", "kv"}, {"damper", "*", "18", "name", "class", "group", "forcelimited", "ctrlrange", "forcerange", "lengthrange", "gear", "cranklength", "user", @@ -1895,19 +1895,26 @@ void mjXReader::OneActuator(XMLElement* elem, mjCActuator* pact) { pact->biastype = mjBIAS_NONE; } - // position servo - else if (type=="position") { - // clear bias - mjuu_zerovec(pact->biasprm, mjNBIAS); - + // position or integrated velocity servo + else if (type=="position" || type=="intvelocity") { // explicit attributes ReadAttr(elem, "kp", 1, pact->gainprm, text); pact->biasprm[1] = -pact->gainprm[0]; + if (ReadAttr(elem, "kv", 1, pact->biasprm + 2, text)) { + if (pact->biasprm[2] < 0) + throw mjXError(elem, "kv cannot be negative"); + pact->biasprm[2] *= -1; + } + // implied parameters - pact->dyntype = mjDYN_NONE; pact->gaintype = mjGAIN_FIXED; pact->biastype = mjBIAS_AFFINE; + + if (type=="intvelocity") { + pact->dyntype = mjDYN_INTEGRATOR; + pact->actlimited = 1; + } } // velocity servo @@ -1925,22 +1932,6 @@ void mjXReader::OneActuator(XMLElement* elem, mjCActuator* pact) { pact->biastype = mjBIAS_AFFINE; } - // integrated velocity - else if (type=="intvelocity") { - // clear bias - mjuu_zerovec(pact->biasprm, mjNBIAS); - - // explicit attributes - ReadAttr(elem, "kp", 1, pact->gainprm, text); - - // implied parameters - pact->dyntype = mjDYN_INTEGRATOR; - pact->gaintype = mjGAIN_FIXED; - pact->biastype = mjBIAS_AFFINE; - pact->actlimited = 1; - pact->biasprm[1] = -pact->gainprm[0]; - } - // damper else if (type=="damper") { // clear gain diff --git a/test/engine/testdata/actuation/refsite.xml b/test/engine/testdata/actuation/refsite.xml index e4b0a2fa..eda8e284 100644 --- a/test/engine/testdata/actuation/refsite.xml +++ b/test/engine/testdata/actuation/refsite.xml @@ -13,7 +13,7 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/engine/testdata/collision_box/boxbox_bad1.xml b/test/engine/testdata/collision_box/boxbox_bad1.xml new file mode 100644 index 00000000..04f16290 --- /dev/null +++ b/test/engine/testdata/collision_box/boxbox_bad1.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/engine/testdata/collision_box/boxbox_duplicate.xml b/test/engine/testdata/collision_box/boxbox_duplicate.xml new file mode 100644 index 00000000..6582f5cf --- /dev/null +++ b/test/engine/testdata/collision_box/boxbox_duplicate.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + From e7d637f375d809f6a83b7e3e3d5d698151edfd56 Mon Sep 17 00:00:00 2001 From: Baruch Tabanpour Date: Fri, 8 Dec 2023 16:43:31 -0800 Subject: [PATCH 053/121] Fix issue with foot vel in MJX notebook PiperOrigin-RevId: 589276253 Change-Id: I2c8c24e42d037d8c59667364a298f8b669d779b6 --- mjx/tutorial.ipynb | 46 +++++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/mjx/tutorial.ipynb b/mjx/tutorial.ipynb index 16aa9c32..522b0208 100644 --- a/mjx/tutorial.ipynb +++ b/mjx/tutorial.ipynb @@ -923,7 +923,9 @@ " 'physics_steps_per_control_step', physics_steps_per_control_step)\n", " super().__init__(mj_model=mj_model, **kwargs)\n", "\n", - " self.torso_idx = 1\n", + " self.torso_idx = mujoco.mj_name2id(\n", + " mj_model, mujoco.mjtObj.mjOBJ_BODY.value, 'torso'\n", + " )\n", " self._action_scale = action_scale\n", " self._obs_noise = obs_noise\n", " self._reset_horizon = 500\n", @@ -940,6 +942,7 @@ " self.reward_config = get_config()\n", " self.lowers = self._default_ap_pose - jp.array([0.2, 0.8, 0.8] * 4)\n", " self.uppers = self._default_ap_pose + jp.array([0.2, 0.8, 0.8] * 4)\n", + " self._foot_radius = 0.014\n", "\n", " def sample_command(self, rng: jax.Array) -\u003e jax.Array:\n", " lin_vel_x = [-0.6, 1.0] # min max [m/s]\n", @@ -1023,12 +1026,15 @@ " joint_vel = qvel[6:]\n", "\n", " # foot contact data based on z-position\n", - " foot_contact = 0.017 - self._get_feet_pos_vel(x, xd)[0][:, 2]\n", - " contact = foot_contact \u003e -1e-3 # a mm or less off the floor\n", + " foot_contact_pos = (\n", + " self._get_feet_pos_vel(x, xd)[0][:, 2]\n", + " - self._foot_radius\n", + " )\n", + " contact = foot_contact_pos \u003c 1e-3 # a mm or less off the floor\n", " contact_filt_mm = jp.logical_or(contact, state.info['last_contact'])\n", " contact_filt_cm = jp.logical_or(\n", - " foot_contact \u003e -1e-2, state.info['last_contact']\n", - " )\n", + " foot_contact_pos \u003c 3e-2, state.info['last_contact']\n", + " ) # 3cm or less off the floor\n", " first_contact = (state.info['feet_air_time'] \u003e 0) * (contact_filt_mm)\n", " state.info['feet_air_time'] += self.dt\n", "\n", @@ -1100,29 +1106,26 @@ " state.info.update(rng=rng)\n", "\n", " # resetting logic if joint limits are reached or robot is falling\n", - " done = 0.0\n", " up = jp.array([0.0, 0.0, 1.0])\n", - " done = jp.where(jp.dot(math.rotate(up, x.rot[0]), up) \u003c 0, 1.0, done)\n", - " done = jp.where(jp.logical_or(\n", - " jp.any(joint_angles \u003c .98 * self.lowers),\n", - " jp.any(joint_angles \u003e .98 * self.uppers)), 1.0, done)\n", - " done = jp.where(x.pos[self.torso_idx, 2] \u003c 0.18, 1.0, done)\n", + " done = jp.dot(math.rotate(up, x.rot[0]), up) \u003c 0\n", + " done |= jp.any(joint_angles \u003c 0.98 * self.lowers)\n", + " done |= jp.any(joint_angles \u003e 0.98 * self.uppers)\n", + " done |= x.pos[0, 2] \u003c 0.18\n", "\n", " # termination reward\n", - " reward += jp.where(\n", - " (done == 1.0) \u0026 (state.info['step'] \u003c self._reset_horizon),\n", - " self.reward_config.rewards.scales.termination,\n", - " 0.0,\n", + " reward += (\n", + " done * (state.info['step'] \u003c self._reset_horizon) *\n", + " self.reward_config.rewards.scales.termination\n", " )\n", "\n", " # when done, sample new command if more than _reset_horizon timesteps\n", " # achieved\n", " state.info['command'] = jp.where(\n", - " (done == 1.0) \u0026 (state.info['step'] \u003e self._reset_horizon),\n", + " done \u0026 (state.info['step'] \u003e self._reset_horizon),\n", " self.sample_command(cmd_rng), state.info['command'])\n", " # reset the step counter when done\n", " state.info['step'] = jp.where(\n", - " (done == 1.0) | (state.info['step'] \u003e self._reset_horizon), 0,\n", + " done | (state.info['step'] \u003e self._reset_horizon), 0,\n", " state.info['step']\n", " )\n", "\n", @@ -1133,7 +1136,7 @@ "\n", " state = state.replace(\n", " pipeline_state=data, obs=obs + obs_noise, reward=reward,\n", - " done=done)\n", + " done=done * 1.0)\n", " return state\n", "\n", " def _get_obs(self, qpos: jax.Array, x: Transform, xd: Motion,\n", @@ -1233,7 +1236,8 @@ " self, x: Transform, xd: Motion) -\u003e Tuple[jax.Array, jax.Array]:\n", " offset = Transform.create(pos=self._feet_pos)\n", " pos = x.take(self._feet_index).vmap().do(offset).pos\n", - " vel = offset.vmap().do(xd.take(self._feet_index)).vel\n", + " world_offset = Transform.create(pos=pos - x.take(self._feet_index).pos)\n", + " vel = world_offset.vmap().do(xd.take(self._feet_index)).vel\n", " return pos, vel\n", "\n", " def _reward_foot_slip(\n", @@ -1405,8 +1409,8 @@ "private_outputs": true, "provenance": [ { - "file_id": "1brcF4_qCRS2ASc-QQw1rsEwl5IjzGvq2", - "timestamp": 1697763780236 + "file_id": "1QsuS7EJhdPEHxxAu9XwozvA7eb4ZnlAb", + "timestamp": 1701993737024 } ], "toc_visible": true From b22919543ddf096377bce27e4a638b779478ed54 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Sat, 9 Dec 2023 16:05:43 -0800 Subject: [PATCH 054/121] Fix bugs related to saving of values. Before this change: - User-modified values of `mjModel.stat.{meanmass, meaninertia}` would not be saved to XML. - Non-user-modified values of `mjModel.stat.{meansize, extent, center}` would always get saved to XML. After this change values are saved to XML only if provided by the user, either in the XML or by changing values in `mjModel` before saving. PiperOrigin-RevId: 589473351 Change-Id: I4dcf4892ef61c35edf424226e731232d861bad8d --- src/user/user_model.cc | 31 ++++++++++++++++++++++++++---- src/user/user_model.h | 10 +++++++++- test/xml/xml_native_writer_test.cc | 28 ++++++++++++++++++++++++++- 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/user/user_model.cc b/src/user/user_model.cc index 4645644e..c1c0300e 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -114,6 +114,16 @@ mjCModel::mjCModel() { center[0] = mjNAN; center[1] = center[2] = 0; + //------------------------ auto-computed statistics +#ifndef MEMORY_SANITIZER + // initializing as best practice, but want MSAN to catch unintialized use + meaninertia_auto = 0; + meanmass_auto = 0; + meansize_auto = 0; + extent_auto = 0; + center_auto[0] = center_auto[1] = center_auto[2] = 0; +#endif + //------------------------ engine data modelname = "MuJoCo Model"; mj_defaultOption(&option); @@ -3157,6 +3167,13 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) { // actuator lengthrange computation LengthRange(m, d); + // save automatically-computed statistics, to disambiguate when saving + extent_auto = m->stat.extent; + meaninertia_auto = m->stat.meaninertia; + meanmass_auto = m->stat.meanmass; + meansize_auto = m->stat.meansize; + copyvec(center_auto, m->stat.center, 3); + // override model statistics if defined by user if (mjuu_defined(extent)) m->stat.extent = (mjtNum)extent; if (mjuu_defined(meaninertia)) m->stat.meaninertia = (mjtNum)meaninertia; @@ -3233,10 +3250,16 @@ bool mjCModel::CopyBack(const mjModel* m) { option = m->opt; visual = m->vis; - // runtime-modifiable members of mjStatistic - meansize = m->stat.meansize; - extent = m->stat.extent; - mju_copy3(center, m->stat.center); + // runtime-modifiable members of mjStatistic, if different from computed values + if (m->stat.meaninertia != meaninertia_auto) meaninertia = m->stat.meaninertia; + if (m->stat.meanmass != meanmass_auto) meanmass = m->stat.meanmass; + if (m->stat.meansize != meansize_auto) meansize = m->stat.meansize; + if (m->stat.extent != extent_auto) extent = m->stat.extent; + if (m->stat.center[0] != center_auto[0] || + m->stat.center[1] != center_auto[1] || + m->stat.center[2] != center_auto[2]) { + mju_copy3(center, m->stat.center); + } // qpos0, qpos_spring for (int i=0; i cameras; // list of cameras std::vector lights; // list of lights + //------------------------ internal variables + + // statistics, as computed by mj_setConst + double meaninertia_auto; // mean diagonal inertia, as computed by mj_setConst + double meanmass_auto; // mean body mass, as computed by mj_setConst + double meansize_auto; // mean body size, as computed by mj_setConst + double extent_auto; // spatial extent, as computed by mj_setConst + double center_auto[3]; // center of model, as computed by mj_setConst + // map from object names to ids mjListKeyMap ids; - //------------------------ internal variables bool hasImplicitPluginElem; // already encountered an implicit plugin sensor/actuator bool compiled; // already compiled flag (cannot be compiled again) mjCError errInfo; // last error info diff --git a/test/xml/xml_native_writer_test.cc b/test/xml/xml_native_writer_test.cc index 52b21923..f5989279 100644 --- a/test/xml/xml_native_writer_test.cc +++ b/test/xml/xml_native_writer_test.cc @@ -1135,7 +1135,7 @@ using DecompilerTest = MujocoTest; TEST_F(DecompilerTest, SavesStatitics) { static constexpr char xml[] = R"( - + )"; mjModel* model = LoadModelFromString(xml); @@ -1145,10 +1145,36 @@ TEST_F(DecompilerTest, SavesStatitics) { model->stat.center[0] = 9; model->stat.center[1] = 10; model->stat.center[2] = 11; + model->stat.meanmass = 12; + model->stat.meaninertia = 13; std::string saved_xml = SaveAndReadXml(model); EXPECT_THAT(saved_xml, HasSubstr("meansize=\"7\"")); EXPECT_THAT(saved_xml, HasSubstr("extent=\"8\"")); EXPECT_THAT(saved_xml, HasSubstr("center=\"9 10 11\"")); + EXPECT_THAT(saved_xml, HasSubstr("meanmass=\"12\"")); + EXPECT_THAT(saved_xml, HasSubstr("meaninertia=\"13\"")); + mj_deleteModel(model); +} + +TEST_F(DecompilerTest, DoesntSaveInferredStatitics) { + static constexpr char xml[] = R"( + + + + + + + + )"; + mjModel* model = LoadModelFromString(xml); + ASSERT_THAT(model, NotNull()); + std::string saved_xml = SaveAndReadXml(model); + EXPECT_THAT(saved_xml, Not(HasSubstr("meansize"))); + EXPECT_THAT(saved_xml, Not(HasSubstr("meanmass"))); + EXPECT_THAT(saved_xml, Not(HasSubstr("meaninertia"))); + EXPECT_THAT(saved_xml, Not(HasSubstr("center"))); + EXPECT_THAT(saved_xml, Not(HasSubstr("extent"))); + EXPECT_THAT(saved_xml, Not(HasSubstr("statistic"))); mj_deleteModel(model); } From 1885d518ebf80b6fe6c2ba595be06027ce7306da Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Sun, 10 Dec 2023 03:26:49 -0800 Subject: [PATCH 055/121] Use the string `(rot:lin)` in mjdata.h to clarify where 6D motion vectors use the Featherstone convention. Also add clarifying text to the appropriate note in the documentation. Fixes #982. PiperOrigin-RevId: 589556339 Change-Id: I3e7ff185a089715a386694321d4b89d09d481cf9 --- doc/APIreference/APItypes.rst | 6 +++--- doc/includes/references.h | 6 +++--- include/mujoco/mjdata.h | 6 +++--- introspect/structs.py | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/doc/APIreference/APItypes.rst b/doc/APIreference/APItypes.rst index 684e810b..ee8de505 100644 --- a/doc/APIreference/APItypes.rst +++ b/doc/APIreference/APItypes.rst @@ -1219,9 +1219,9 @@ a frame at the center-of-mass of the local kinematic subtree (``mjData.subtree_c This choice increases the precision of kinematic computations for mechanisms that are distant from the global origin. ``cdof``: - These 6D motion vectors describe the instantaneous axis of a degree-of-freedom and are used by all Jacobian functions. - Therefore, the minimal computation required for analytic Jacobians is :ref:`mj_kinematics` followed by - :ref:`mj_comPos`. + These 6D motion vectors (3 rotation, 3 translation) describe the instantaneous axis of a degree-of-freedom and are + used by all Jacobian functions. The minimal computation required for analytic Jacobians is :ref:`mj_kinematics` + followed by :ref:`mj_comPos`. ``cinert``: These 10-vectors describe the inertial properties of a body in the c-frame and are used by the Composite Rigid Body diff --git a/doc/includes/references.h b/doc/includes/references.h index f4aedde9..79302849 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -237,7 +237,7 @@ struct mjData_ { // computed by mj_fwdPosition/mj_comPos mjtNum* subtree_com; // center of mass of each subtree (nbody x 3) - mjtNum* cdof; // com-based motion axis of each dof (nv x 6) + mjtNum* cdof; // com-based motion axis of each dof (rot:lin) (nv x 6) mjtNum* cinert; // com-based body inertia and mass (nbody x 10) // computed by mj_fwdPosition/mj_flex @@ -285,8 +285,8 @@ struct mjData_ { mjtNum* actuator_velocity; // actuator velocities (nu x 1) // computed by mj_fwdVelocity/mj_comVel - mjtNum* cvel; // com-based velocity [3D rot; 3D tran] (nbody x 6) - mjtNum* cdof_dot; // time-derivative of cdof (nv x 6) + mjtNum* cvel; // com-based velocity (rot:lin) (nbody x 6) + mjtNum* cdof_dot; // time-derivative of cdof (rot:lin) (nv x 6) // computed by mj_fwdVelocity/mj_rne (without acceleration) mjtNum* qfrc_bias; // C(qpos,qvel) (nv x 1) diff --git a/include/mujoco/mjdata.h b/include/mujoco/mjdata.h index a3609b8b..1fc6fe19 100644 --- a/include/mujoco/mjdata.h +++ b/include/mujoco/mjdata.h @@ -265,7 +265,7 @@ struct mjData_ { // computed by mj_fwdPosition/mj_comPos mjtNum* subtree_com; // center of mass of each subtree (nbody x 3) - mjtNum* cdof; // com-based motion axis of each dof (nv x 6) + mjtNum* cdof; // com-based motion axis of each dof (rot:lin) (nv x 6) mjtNum* cinert; // com-based body inertia and mass (nbody x 10) // computed by mj_fwdPosition/mj_flex @@ -313,8 +313,8 @@ struct mjData_ { mjtNum* actuator_velocity; // actuator velocities (nu x 1) // computed by mj_fwdVelocity/mj_comVel - mjtNum* cvel; // com-based velocity [3D rot; 3D tran] (nbody x 6) - mjtNum* cdof_dot; // time-derivative of cdof (nv x 6) + mjtNum* cvel; // com-based velocity (rot:lin) (nbody x 6) + mjtNum* cdof_dot; // time-derivative of cdof (rot:lin) (nv x 6) // computed by mj_fwdVelocity/mj_rne (without acceleration) mjtNum* qfrc_bias; // C(qpos,qvel) (nv x 1) diff --git a/introspect/structs.py b/introspect/structs.py index a1fa3159..eea7f6cc 100644 --- a/introspect/structs.py +++ b/introspect/structs.py @@ -4493,7 +4493,7 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=PointerType( inner_type=ValueType(name='mjtNum'), ), - doc='com-based motion axis of each dof (nv x 6)', # pylint: disable=line-too-long + doc='com-based motion axis of each dof (rot:lin) (nv x 6)', # pylint: disable=line-too-long ), StructFieldDecl( name='cinert', @@ -4703,14 +4703,14 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=PointerType( inner_type=ValueType(name='mjtNum'), ), - doc='com-based velocity [3D rot; 3D tran] (nbody x 6)', # pylint: disable=line-too-long + doc='com-based velocity (rot:lin) (nbody x 6)', # pylint: disable=line-too-long ), StructFieldDecl( name='cdof_dot', type=PointerType( inner_type=ValueType(name='mjtNum'), ), - doc='time-derivative of cdof (nv x 6)', # pylint: disable=line-too-long + doc='time-derivative of cdof (rot:lin) (nv x 6)', # pylint: disable=line-too-long ), StructFieldDecl( name='qfrc_bias', From 07e05b4c55c0a179e5a62e4aa7af67ca4e6e8cb5 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 11 Dec 2023 08:41:07 -0800 Subject: [PATCH 056/121] Fix typo. Fixes #1221 PiperOrigin-RevId: 589835888 Change-Id: Iacedbe642709d5fbd43b7f0f1f9daa6447ce6155 --- doc/XMLreference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 2f423abf..6ca8c13b 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -172,7 +172,7 @@ This element does not strictly belong to MJCF. Instead it is a meta-element, use files in a single document object model (DOM) before parsing. The included file must be a valid XML file with a unique top-level element. This top-level element is removed by the parser, and the elements below it are inserted at the location of the :el:`include` element. At least one element must be inserted as a result of this procedure. The -:el:`include` element can be used where ever an XML element is expected in the MJFC file. Nested includes are allowed, +:el:`include` element can be used where ever an XML element is expected in the MJCF file. Nested includes are allowed, however a given XML file can be included at most once in the entire model. After all the included XML files have been assembled into a single DOM, it must correspond to a valid MJCF model. Other than that, it is up to the user to decide how to use includes and how to modularize large files if desired. From aed4254d65d54d61a1d357df62c917ee4ec390a1 Mon Sep 17 00:00:00 2001 From: Nimrod Gileadi Date: Tue, 12 Dec 2023 03:41:29 -0800 Subject: [PATCH 057/121] Fix the table formatting in plugin/actuator/README.md. The line continuation format that was used is not supported on GitHub. PiperOrigin-RevId: 590141362 Change-Id: I0ab0dabfb8977bdf79682476376dda435787eba0 --- plugin/actuator/README.md | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/plugin/actuator/README.md b/plugin/actuator/README.md index 80676921..9139aed1 100644 --- a/plugin/actuator/README.md +++ b/plugin/actuator/README.md @@ -38,16 +38,10 @@ You can use it like: The available options are: -|Attribute|Default |Meaning | -|---------|--------|-------------------------------------------------------------------------------------------------------------------------------------| -|`kp` |0 |**P** gain for the controller. | -|`ki` |0 |**I** gain for the controller. | -: : : : -: : :If nonzero, one activation variable will be added to `mjData.act`, containing the current I term (in units of force). : -|`kd` |0 |**D** gain for the controller. | -|`imax` |Optional|If specified, the force produced by the I term will be clipped to the range `[-imax, -imax]`. | -|`slewmax`|Optional|The maximum rate at which the setpoint for the PID controller can change. | -: : : : -: : :If a bigger change is requested between two timesteps, it will be clipped to the range `[ctrl - slewmax * dt, ctrl + slewmax * dt]` : -: : : : -: : :If specified, one activation variable will be added to `mjData.act` containing the previous value of `ctrl`. : +|Attribute | Default | Meaning | +|----------|---------|---------| +|`kp` | 0 | **P** gain for the controller. | +|`ki` | 0 | **I** gain for the controller.

If nonzero, one activation variable will be added to `mjData.act`, containing the current I term (in units of force). | +|`kd` | 0 | **D** gain for the controller. | +|`imax` | Optional | If specified, the force produced by the I term will be clipped to the range `[-imax, -imax]`. | +|`slewmax` | Optional | The maximum rate at which the setpoint for the PID controller can change.

If a bigger change is requested between two timesteps, it will be clipped to the range `[ctrl - slewmax * dt, ctrl + slewmax * dt]`

If specified, one activation variable will be added to `mjData.act` containing the previous value of `ctrl`. | From 64c33cefcfb6edfcaf424927615621d94ad39d21 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 12 Dec 2023 03:55:19 -0800 Subject: [PATCH 058/121] Correct formula for `solref` direct stiffness. Fixes #732. PiperOrigin-RevId: 590144795 Change-Id: I94def777c7456243509374caecea2a335859f1f2 --- doc/modeling.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/modeling.rst b/doc/modeling.rst index c43b9a04..681534cd 100644 --- a/doc/modeling.rst +++ b/doc/modeling.rst @@ -403,7 +403,7 @@ and the damping ratio is ignored. Equivalently, in the direct format, the :math: .. math:: \begin{aligned} b &= \text{damping} / d_\text{width} \\ - k &= \text{stiffness} / d_\text{width}^2 \\ + k &= \text{stiffness} \cdot d(r) / d_\text{width}^2 \\ \end{aligned} .. tip:: From 28a9e6d9a28afdf303a20415d7946f9362f78cd5 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 12 Dec 2023 05:13:54 -0800 Subject: [PATCH 059/121] Add `mjNOBJECT` value to `mjtObj` enum. PiperOrigin-RevId: 590164716 Change-Id: I500741aa4fb97fe2c4f5d2086188bb2c9fdfd1a0 --- doc/includes/references.h | 4 +++- include/mujoco/mjmodel.h | 4 +++- introspect/enums.py | 1 + src/engine/engine_io.c | 2 ++ unity/Runtime/Bindings/MjBindings.cs | 1 + 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/doc/includes/references.h b/doc/includes/references.h index 79302849..c89a4363 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -570,7 +570,9 @@ typedef enum mjtObj_ { // type of MujoCo object mjOBJ_TEXT, // text mjOBJ_TUPLE, // tuple mjOBJ_KEY, // keyframe - mjOBJ_PLUGIN // plugin instance + mjOBJ_PLUGIN, // plugin instance + + mjNOBJECT // number of object types } mjtObj; typedef enum mjtConstraint_ { // type of constraint mjCNSTR_EQUALITY = 0, // equality constraint diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index 663e191f..49312dd1 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -246,7 +246,9 @@ typedef enum mjtObj_ { // type of MujoCo object mjOBJ_TEXT, // text mjOBJ_TUPLE, // tuple mjOBJ_KEY, // keyframe - mjOBJ_PLUGIN // plugin instance + mjOBJ_PLUGIN, // plugin instance + + mjNOBJECT // number of object types } mjtObj; diff --git a/introspect/enums.py b/introspect/enums.py index bb039f65..4982cf36 100644 --- a/introspect/enums.py +++ b/introspect/enums.py @@ -265,6 +265,7 @@ ENUMS: Mapping[str, EnumDecl] = dict([ ('mjOBJ_TUPLE', 23), ('mjOBJ_KEY', 24), ('mjOBJ_PLUGIN', 25), + ('mjNOBJECT', 26), ]), )), ('mjtConstraint', diff --git a/src/engine/engine_io.c b/src/engine/engine_io.c index 32994e9c..ca55358a 100644 --- a/src/engine/engine_io.c +++ b/src/engine/engine_io.c @@ -1858,6 +1858,8 @@ static int numObjects(const mjModel* m, mjtObj objtype) { return m->nkey; case mjOBJ_PLUGIN: return m->nplugin; + case mjNOBJECT: + return -2; } return -2; } diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index a17a2694..91bf8765 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -301,6 +301,7 @@ public enum mjtObj : int{ mjOBJ_TUPLE = 23, mjOBJ_KEY = 24, mjOBJ_PLUGIN = 25, + mjNOBJECT = 26, } public enum mjtConstraint : int{ mjCNSTR_EQUALITY = 0, From 35f095d7c5a644bf82c827513d87c7fc4afc5308 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 12 Dec 2023 06:42:40 -0800 Subject: [PATCH 060/121] When paused in `simulate`, add index of history scrubber if not 0. PiperOrigin-RevId: 590184966 Change-Id: Id0914c41a1339cf71ebdea02ec486c52c3d37179 --- simulate/simulate.cc | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/simulate/simulate.cc b/simulate/simulate.cc index 0b2a150a..db0821d2 100644 --- a/simulate/simulate.cc +++ b/simulate/simulate.cc @@ -2476,7 +2476,14 @@ void Simulate::Render() { // show pause/loading label if (!this->run || this->loadrequest) { - const char* label = this->loadrequest ? "LOADING..." : "PAUSE"; + char label[30] = {'\0'}; + if (this->loadrequest) { + std::snprintf(label, sizeof(label), "LOADING..."); + } if (this->scrub_index == 0) { + std::snprintf(label, sizeof(label), "PAUSE"); + } else { + std::snprintf(label, sizeof(label), "PAUSE (%d)", this->scrub_index); + } mjr_overlay(mjFONT_BIG, mjGRID_TOP, smallrect, label, nullptr, &this->platform_ui->mjr_context()); } From e3df84ec3fd157d6bb28a206791a4d5f3436d91c Mon Sep 17 00:00:00 2001 From: Nimrod Gileadi Date: Tue, 12 Dec 2023 08:40:20 -0800 Subject: [PATCH 061/121] Include the size limit in error message from mjr_addAux. This is to help debug #1274. PiperOrigin-RevId: 590215990 Change-Id: I356675be84f9cb2892d31d4dc7639adf49fe4a0f --- src/render/render_context.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/render/render_context.c b/src/render/render_context.c index afb1377f..ec349b2a 100644 --- a/src/render/render_context.c +++ b/src/render/render_context.c @@ -1675,8 +1675,17 @@ void mjr_addAux(int index, int width, int height, int samples, mjrContext* con) // check max size int maxSize = 0; glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &maxSize); - if (width > maxSize || height > maxSize) { - mju_error("Auxiliary buffer size exceeds maximum allowed by OpenGL implementation"); + if (width > maxSize) { + mju_error( + "Auxiliary buffer width exceeds maximum allowed by OpenGL " + "implementation: %d > %d", + width, maxSize); + } + if (height > maxSize) { + mju_error( + "Auxiliary buffer height exceeds maximum allowed by OpenGL " + "implementation: %d > %d", + height, maxSize); } // clamp samples request From 53883ce86a7da0f7df7016c009d4ce79eefc283e Mon Sep 17 00:00:00 2001 From: Kevin Zakka Date: Tue, 12 Dec 2023 08:58:49 -0800 Subject: [PATCH 062/121] Add a `close()` method to `mujoco.Renderer` which explicitly frees the gl context. PiperOrigin-RevId: 590221484 Change-Id: I07392f9c53f662a37a57ab23acfef9aa5a98a49b --- python/mujoco/cgl/__init__.py | 19 +++--- python/mujoco/glfw/__init__.py | 2 +- python/mujoco/renderer.py | 37 ++++++++++++ python/mujoco/renderer_test.py | 104 ++++++++++++++++----------------- 4 files changed, 100 insertions(+), 62 deletions(-) diff --git a/python/mujoco/cgl/__init__.py b/python/mujoco/cgl/__init__.py index ba1e36a6..36037116 100644 --- a/python/mujoco/cgl/__init__.py +++ b/python/mujoco/cgl/__init__.py @@ -57,15 +57,18 @@ class GLContext: def free(self): """Frees resources associated with this context.""" - if self._context: - cgl.CGLUnlockContext(self._context) - cgl.CGLSetCurrentContext(None) - cgl.CGLReleaseContext(self._context) - self._context = None + try: + if self._context: + cgl.CGLUnlockContext(self._context) + cgl.CGLSetCurrentContext(None) + cgl.CGLReleaseContext(self._context) + self._context = None - if self._pix: - cgl.CGLReleasePixelFormat(self._pix) - self._context = None + if self._pix: + cgl.CGLReleasePixelFormat(self._pix) + self._pix = None + except Exception: # pylint: disable=broad-exception-caught + pass def __del__(self): self.free() diff --git a/python/mujoco/glfw/__init__.py b/python/mujoco/glfw/__init__.py index a5693bec..0fd8bd9a 100644 --- a/python/mujoco/glfw/__init__.py +++ b/python/mujoco/glfw/__init__.py @@ -35,7 +35,7 @@ class GLContext: if glfw.get_current_context() == self._context: glfw.make_context_current(None) glfw.destroy_window(self._context) - self._context = None + self._context = None def __del__(self): self.free() diff --git a/python/mujoco/renderer.py b/python/mujoco/renderer.py index f34c8243..dfb387c6 100644 --- a/python/mujoco/renderer.py +++ b/python/mujoco/renderer.py @@ -136,6 +136,9 @@ the clause: A new numpy array holding the pixels with shape `(H, W)` or `(H, W, 3)`, depending on the value of `self._depth_rendering` unless `out is None`, in which case a reference to `out` is returned. + + Raises: + RuntimeError: if this method is called after the close method. """ original_flags = self._scene.flags.copy() @@ -145,6 +148,8 @@ the clause: self._scene.flags[_enums.mjtRndFlag.mjRND_SEGMENT] = True self._scene.flags[_enums.mjtRndFlag.mjRND_IDCOLOR] = True + if self._gl_context is None: + raise RuntimeError('render cannot be called after close.') self._gl_context.make_current() if self._depth_rendering: @@ -288,3 +293,35 @@ the clause: camera, _enums.mjtCatBit.mjCAT_ALL.value, self._scene, ) + + def close(self) -> None: + """Frees the resources used by the renderer. + + This method can be used directly: + + ```python + renderer = Renderer(...) + # Use renderer. + renderer.close() + ``` + + or via a context manager: + + ```python + with Renderer(...) as renderer: + # Use renderer. + ``` + """ + if self._gl_context: + self._gl_context.free() + self._gl_context = None + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + del exc_type, exc_value, traceback # Unused. + self.close() + + def __del__(self) -> None: + self.close() diff --git a/python/mujoco/renderer_test.py b/python/mujoco/renderer_test.py index 8aa44a97..e8022006 100644 --- a/python/mujoco/renderer_test.py +++ b/python/mujoco/renderer_test.py @@ -33,10 +33,10 @@ class MuJoCoRendererTest(parameterized.TestCase): """ model = mujoco.MjModel.from_xml_string(xml) data = mujoco.MjData(model) - renderer = mujoco.Renderer(model, 50, 50) - mujoco.mj_forward(model, data) - with self.assertRaisesRegex(ValueError, r'camera "b" does not exist'): - renderer.update_scene(data, 'b') + with mujoco.Renderer(model, 50, 50) as renderer: + mujoco.mj_forward(model, data) + with self.assertRaisesRegex(ValueError, r'camera "b" does not exist'): + renderer.update_scene(data, 'b') def test_renderer_camera_under_range(self): xml = """ @@ -48,10 +48,10 @@ class MuJoCoRendererTest(parameterized.TestCase): """ model = mujoco.MjModel.from_xml_string(xml) data = mujoco.MjData(model) - renderer = mujoco.Renderer(model, 50, 50) - mujoco.mj_forward(model, data) - with self.assertRaisesRegex(ValueError, '-2 is out of range'): - renderer.update_scene(data, -2) + with mujoco.Renderer(model, 50, 50) as renderer: + mujoco.mj_forward(model, data) + with self.assertRaisesRegex(ValueError, '-2 is out of range'): + renderer.update_scene(data, -2) def test_renderer_camera_over_range(self): xml = """ @@ -63,10 +63,10 @@ class MuJoCoRendererTest(parameterized.TestCase): """ model = mujoco.MjModel.from_xml_string(xml) data = mujoco.MjData(model) - renderer = mujoco.Renderer(model, 50, 50) - mujoco.mj_forward(model, data) - with self.assertRaisesRegex(ValueError, '1 is out of range'): - renderer.update_scene(data, 1) + with mujoco.Renderer(model, 50, 50) as renderer: + mujoco.mj_forward(model, data) + with self.assertRaisesRegex(ValueError, '1 is out of range'): + renderer.update_scene(data, 1) def test_renderer_renders_scene(self): xml = """ @@ -79,19 +79,19 @@ class MuJoCoRendererTest(parameterized.TestCase): """ model = mujoco.MjModel.from_xml_string(xml) data = mujoco.MjData(model) - renderer = mujoco.Renderer(model, 50, 50) - mujoco.mj_forward(model, data) - renderer.update_scene(data, 'closeup') + with mujoco.Renderer(model, 50, 50) as renderer: + mujoco.mj_forward(model, data) + renderer.update_scene(data, 'closeup') - pixels = renderer.render().flatten() - not_all_black = False + pixels = renderer.render().flatten() + not_all_black = False - # Pixels should all be a neutral color. - for pixel in pixels: - if pixel > 0: - not_all_black = True - break - self.assertTrue(not_all_black) + # Pixels should all be a neutral color. + for pixel in pixels: + if pixel > 0: + not_all_black = True + break + self.assertTrue(not_all_black) def test_renderer_output_without_out(self): xml = """ @@ -105,25 +105,25 @@ class MuJoCoRendererTest(parameterized.TestCase): model = mujoco.MjModel.from_xml_string(xml) data = mujoco.MjData(model) mujoco.mj_forward(model, data) - renderer = mujoco.Renderer(model, 50, 50) - renderer.update_scene(data, 'closeup') - pixels = [renderer.render()] - - colors = ( - (1.0, 0.0, 0.0, 1.0), - (0.0, 1.0, 0.0, 1.0), - (0.0, 0.0, 1.0, 1.0), - ) - - for i, color in enumerate(colors): - model.geom_rgba[0, :] = color - mujoco.mj_forward(model, data) + with mujoco.Renderer(model, 50, 50) as renderer: renderer.update_scene(data, 'closeup') - pixels.append(renderer.render()) - self.assertIsNot(pixels[-2], pixels[-1]) + pixels = [renderer.render()] - # Pixels should change over steps. - self.assertFalse((pixels[i + 1] == pixels[i]).all()) + colors = ( + (1.0, 0.0, 0.0, 1.0), + (0.0, 1.0, 0.0, 1.0), + (0.0, 0.0, 1.0, 1.0), + ) + + for i, color in enumerate(colors): + model.geom_rgba[0, :] = color + mujoco.mj_forward(model, data) + renderer.update_scene(data, 'closeup') + pixels.append(renderer.render()) + self.assertIsNot(pixels[-2], pixels[-1]) + + # Pixels should change over steps. + self.assertFalse((pixels[i + 1] == pixels[i]).all()) def test_renderer_output_with_out(self): xml = """ @@ -139,23 +139,21 @@ class MuJoCoRendererTest(parameterized.TestCase): model = mujoco.MjModel.from_xml_string(xml) data = mujoco.MjData(model) mujoco.mj_forward(model, data) - renderer = mujoco.Renderer(model, *render_size) - renderer.update_scene(data, 'closeup') + with mujoco.Renderer(model, *render_size) as renderer: + renderer.update_scene(data, 'closeup') - self.assertTrue(np.all(render_out == 0)) + self.assertTrue(np.all(render_out == 0)) - pixels = renderer.render(out=render_out) + pixels = renderer.render(out=render_out) - # Pixels should always refer to the same `render_out` array. - self.assertIs(pixels, render_out) - self.assertFalse(np.all(render_out == 0)) + # Pixels should always refer to the same `render_out` array. + self.assertIs(pixels, render_out) + self.assertFalse(np.all(render_out == 0)) - failing_render_size = (10, 10) - self.assertNotEqual(failing_render_size, render_size) - with self.assertRaises(ValueError): - pixels = renderer.render( - out=np.zeros((*failing_render_size, 3), np.uint8) - ) + failing_render_size = (10, 10) + self.assertNotEqual(failing_render_size, render_size) + with self.assertRaises(ValueError): + renderer.render(out=np.zeros((*failing_render_size, 3), np.uint8)) if __name__ == '__main__': From fcebdf4d0962b9b2cc5071c52cbd7a775ea084d0 Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Tue, 12 Dec 2023 10:36:48 -0800 Subject: [PATCH 063/121] Update MuJoCo to v3.1.0 for release. PiperOrigin-RevId: 590256664 Change-Id: I42ad788d609dde0ff00b13a2e456b2a8d3d9e952 --- 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 c9723a29..bfc599f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,7 +28,7 @@ set(MSVC_INCREMENTAL_DEFAULT ON) project( mujoco - VERSION 3.0.2 + VERSION 3.1.0 DESCRIPTION "MuJoCo Physics Simulator" HOMEPAGE_URL "https://mujoco.org" ) diff --git a/dist/mujoco.rc b/dist/mujoco.rc index e52e3530..484597a9 100644 --- a/dist/mujoco.rc +++ b/dist/mujoco.rc @@ -1,6 +1,6 @@ 1 VERSIONINFO -FILEVERSION 3,0,2,0 -PRODUCTVERSION 3,0,2,0 +FILEVERSION 3,1,0,0 +PRODUCTVERSION 3,1,0,0 FILEOS 0x4 FILETYPE 0x1 { @@ -9,9 +9,9 @@ FILETYPE 0x1 BLOCK "040904b0" { VALUE "ProductName", "MuJoCo" - VALUE "ProductVersion", "3.0.2" + VALUE "ProductVersion", "3.1.0" VALUE "FileDescription", "MuJoCo" - VALUE "FileVersion", "3.0.2" + VALUE "FileVersion", "3.1.0" VALUE "InternalName", "mujoco.dll" VALUE "OriginalFilename", "mujoco.dll" VALUE "CompanyName", "Google DeepMind" diff --git a/dist/simulate.rc b/dist/simulate.rc index 4835c979..ae73084e 100644 --- a/dist/simulate.rc +++ b/dist/simulate.rc @@ -1,8 +1,8 @@ MUJOCO ICON "mujoco.ico" 1 VERSIONINFO -FILEVERSION 3,0,2,0 -PRODUCTVERSION 3,0,2,0 +FILEVERSION 3,1,0,0 +PRODUCTVERSION 3,1,0,0 FILEOS 0x4 FILETYPE 0x1 { @@ -11,9 +11,9 @@ FILETYPE 0x1 BLOCK "040904b0" { VALUE "ProductName", "MuJoCo" - VALUE "ProductVersion", "3.0.2" + VALUE "ProductVersion", "3.1.0" VALUE "FileDescription", "MuJoCo" - VALUE "FileVersion", "3.0.2" + VALUE "FileVersion", "3.1.0" 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 33c5401c..3101f9b2 100644 --- a/doc/APIreference/APIglobals.rst +++ b/doc/APIreference/APIglobals.rst @@ -522,7 +522,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`` - - 302 + - 310 - 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 457c2cd7..f3a97af7 100644 --- a/doc/unity.rst +++ b/doc/unity.rst @@ -30,14 +30,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.0.2.dylib`` (it can be +``/Applications/MuJoCo.app/Contents/Frameworks/mujoco.framework/Versions/Current/libmujoco.3.1.0.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.0.2/lib/libmujoco.so.3.0.2`` and rename it as ``libmujoco.so``. +``~/.mujoco/mujoco-3.1.0/lib/libmujoco.so.3.1.0`` and rename it as ``libmujoco.so``. Windows _______ diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index 5f32cbc7..2935d5a8 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -24,7 +24,7 @@ extern "C" { #endif // header version; should match the library version as returned by mj_version() -#define mjVERSION_HEADER 302 +#define mjVERSION_HEADER 310 // needed to define size_t, fabs and log10 #include diff --git a/mjx/pyproject.toml b/mjx/pyproject.toml index a385e9af..d615108d 100644 --- a/mjx/pyproject.toml +++ b/mjx/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name="mujoco-mjx" -version = "3.0.2" +version = "3.1.0" authors = [ {name = "Google DeepMind", email = "mujoco@deepmind.com"}, ] @@ -31,13 +31,13 @@ dependencies = [ "etils[epath]", "jax", "jaxlib", - "mujoco>=3.0.2.dev0", + "mujoco>=3.1.0.dev0", "scipy", "trimesh", ] [project.urls] Homepage = "https://github.com/google-deepmind/mujoco/tree/main/mjx" -Documentation = "https://mujoco.readthedocs.io/en/3.0.2" +Documentation = "https://mujoco.readthedocs.io/en/3.1.0" Repository = "https://github.com/google-deepmind/mujoco/tree/main/mjx" -Changelog = "https://mujoco.readthedocs.io/en/3.0.2/changelog.html" +Changelog = "https://mujoco.readthedocs.io/en/3.1.0/changelog.html" diff --git a/python/mujoco/CMakeLists.txt b/python/mujoco/CMakeLists.txt index 256adb10..b4dc3f28 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.0.2.dylib + ${MUJOCO_FRAMEWORK}/mujoco.framework/Versions/A/libmujoco.3.1.0.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.0.2 HINTS ${MUJOCO_LIBRARY_DIR} REQUIRED + MUJOCO_LIBRARY mujoco mujoco.3.1.0 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 6c6d68b2..18a27c2f 100644 --- a/python/mujoco/mjpython/Info.plist +++ b/python/mujoco/mjpython/Info.plist @@ -7,13 +7,13 @@ CFBundleIdentifier org.mujoco.mjpython CFBundleVersion - 3.0.2 + 3.1.0 CFBundleGetInfoString - 3.0.2 + 3.1.0 CFBundleLongVersionString - 3.0.2 + 3.1.0 CFBundleShortVersionString - 3.0.2 + 3.1.0 CFBundleExecutable mjpython CFBundleIconFile diff --git a/python/pyproject.toml b/python/pyproject.toml index 81b7da8e..6f934c1d 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mujoco" -version = "3.0.2" +version = "3.1.0" authors = [ {name = "Google DeepMind", email = "mujoco@deepmind.com"}, ] @@ -36,9 +36,9 @@ dynamic = ["readme", "scripts"] [project.urls] Homepage = "https://github.com/google-deepmind/mujoco" -Documentation = "https://mujoco.readthedocs.io/en/3.0.2" +Documentation = "https://mujoco.readthedocs.io/en/3.1.0" Repository = "https://github.com/google-deepmind/mujoco" -Changelog = "https://mujoco.readthedocs.io/en/3.0.2/changelog.html" +Changelog = "https://mujoco.readthedocs.io/en/3.1.0/changelog.html" [tool.setuptools] include-package-data = false diff --git a/sample/CMakeLists.txt b/sample/CMakeLists.txt index 3dceb85c..0e989480 100644 --- a/sample/CMakeLists.txt +++ b/sample/CMakeLists.txt @@ -24,7 +24,7 @@ set(MSVC_INCREMENTAL_DEFAULT ON) project( mujoco_samples - VERSION 3.0.2 + VERSION 3.1.0 DESCRIPTION "MuJoCo samples binaries" HOMEPAGE_URL "https://mujoco.org" ) diff --git a/simulate/CMakeLists.txt b/simulate/CMakeLists.txt index 464fc4c5..e16584e0 100644 --- a/simulate/CMakeLists.txt +++ b/simulate/CMakeLists.txt @@ -29,7 +29,7 @@ set(MUJOCO_DEP_VERSION_lodepng project( mujoco_simulate - VERSION 3.0.2 + VERSION 3.1.0 DESCRIPTION "MuJoCo simulate binaries" HOMEPAGE_URL "https://mujoco.org" ) diff --git a/src/engine/engine_support.c b/src/engine/engine_support.c index b223a3e2..f887d1ca 100644 --- a/src/engine/engine_support.c +++ b/src/engine/engine_support.c @@ -38,8 +38,8 @@ //-------------------------- Constants ------------------------------------------------------------- - #define mjVERSION 302 -#define mjVERSIONSTRING "3.0.2" + #define mjVERSION 310 +#define mjVERSIONSTRING "3.1.0" // names of disable flags const char* mjDISABLESTRING[mjNDISABLE] = { diff --git a/unity/Editor/Bindings/MujocoBinaryRetriever.cs b/unity/Editor/Bindings/MujocoBinaryRetriever.cs index f670431b..8ed2ac7a 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.0.2.dylib", + "/mujoco.framework/Versions/Current/libmujoco.3.1.0.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.0.2/lib/libmujoco.so.3.0.2", + "/.mujoco/mujoco-3.1.0/lib/libmujoco.so.3.1.0", mujocoPath + "/libmujoco.so"); AssetDatabase.Refresh(); } diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index 91bf8765..6f797926 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -108,7 +108,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 = 302; +public const int mjVERSION_HEADER = 310; // ------------------------------------Enums------------------------------------ diff --git a/unity/package.json b/unity/package.json index 9c093316..f8f658d7 100644 --- a/unity/package.json +++ b/unity/package.json @@ -1,7 +1,7 @@ { "name": "org.mujoco", "displayName": "MuJoCo", - "version": "3.0.2", + "version": "3.1.0", "description": "MuJoCo importer and runtime plug-in", "dependencies": {}, "author": { From 89d15c48e8b2f41b4f909767dd0ab569edabbf35 Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Tue, 12 Dec 2023 10:41:28 -0800 Subject: [PATCH 064/121] Update version number in third_party/mujoco/doc/changelog.rst for v3.1.0 PiperOrigin-RevId: 590258202 Change-Id: I1220eb48e7c3871fdf785cf74819aca12d0c75c0 --- doc/changelog.rst | 52 +++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index 56904ed8..5a90b5f6 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -2,48 +2,48 @@ Changelog ========= -Upcoming version (not yet released) +Version 3.1.0 (December 12, 2023) ----------------------------------- General ^^^^^^^ -- Improved convergence of Signed Distance Function (SDF) collisions by using line search and a new objective function - for the optimization. This allows to decrease the number of initial points needed for finding the contacts and is more - robust for very small or large geom sizes. -- Added :ref:`frame` to MJCF, a :ref:`meta-element` which defines a pure coordinate transformation - on its direct children, without requiring a :ref:`body`. -- Added the :at:`kv` attribute to the :ref:`position` and :ref:`intvelocity` - actuators, for specifying actuator-applied damping. This can be used to implement a PD controller with 0 reference - velocity. When using this attribute, it is recommended to use the implicitfast or implicit - :ref:`integrators`. +1. Improved convergence of Signed Distance Function (SDF) collisions by using line search and a new objective function + for the optimization. This allows to decrease the number of initial points needed for finding the contacts and is more + robust for very small or large geom sizes. +2. Added :ref:`frame` to MJCF, a :ref:`meta-element` which defines a pure coordinate transformation + on its direct children, without requiring a :ref:`body`. +3. Added the :at:`kv` attribute to the :ref:`position` and :ref:`intvelocity` + actuators, for specifying actuator-applied damping. This can be used to implement a PD controller with 0 reference + velocity. When using this attribute, it is recommended to use the implicitfast or implicit + :ref:`integrators`. Plugins ^^^^^^^ -- Allow actuator plugins to use activation variables in ``mjData.act`` as their internal state, rather than - ``mjData.plugin_state``. Actuator plugins can now specify :ref:`callbacks` that compute activation - variables, and they can be used with built-in :ref:`dyntype` actuator dynamics. +4. Allow actuator plugins to use activation variables in ``mjData.act`` as their internal state, rather than + ``mjData.plugin_state``. Actuator plugins can now specify :ref:`callbacks` that compute activation + variables, and they can be used with built-in :ref:`dyntype` actuator dynamics. -- Added the `pid `__ actuator plugin, a - configurable PID controller that implements the Integral term, which is not available with native MuJoCo actuators. +5. Added the `pid `__ actuator plugin, a + configurable PID controller that implements the Integral term, which is not available with native MuJoCo actuators. MJX ^^^ -- Added ``site_xpos`` and ``site_xmat`` to MJX. -- Added ``put_data``, ``put_model``, ``get_data`` to replace ``device_put`` and ``device_get_into``, which will be - deprecated. These new functions correctly translate fields that are the result of intermediate calculations such as - ``efc_J``. +6. Added ``site_xpos`` and ``site_xmat`` to MJX. +7. Added ``put_data``, ``put_model``, ``get_data`` to replace ``device_put`` and ``device_get_into``, which will be + deprecated. These new functions correctly translate fields that are the result of intermediate calculations such as + ``efc_J``. Bug fixes ^^^^^^^^^ -- Fix bug in Cartesian actuation with movable refsite, as when using body-centric Cartesian actuators on a quadruped. - Before this fix such actuators could lead to non-conservation of momentum. -- Fix bug that prevented using flex with the :ref:`passive viewer`. -- Fix bug that prevented the use of elasticity plugins in combination with pinned flex vertices. -- Release Python wheels targeting macOS 10.16 to support x86_64 systems where SYSTEM_VERSION_COMPAT is set. The minimum - supported version is still 11.0, but we release these wheels to fix compatibility for those users. See - :github:issue:`1213`. +8. Fix bug in Cartesian actuation with movable refsite, as when using body-centric Cartesian actuators on a quadruped. + Before this fix such actuators could lead to non-conservation of momentum. +9. Fix bug that prevented using flex with the :ref:`passive viewer`. +10. Fix bug that prevented the use of elasticity plugins in combination with pinned flex vertices. +11. Release Python wheels targeting macOS 10.16 to support x86_64 systems where SYSTEM_VERSION_COMPAT is set. The minimum + supported version is still 11.0, but we release these wheels to fix compatibility for those users. See + :github:issue:`1213`. Version 3.0.1 (November 15, 2023) --------------------------------- From 0df1c9c733ea0cc90ba392ce21050914f53832ef Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Tue, 12 Dec 2023 13:16:54 -0800 Subject: [PATCH 065/121] Updated version number in cmakelist, mujcodepencies.cmake for v 3.1.0 PiperOrigin-RevId: 590312529 Change-Id: Ie4fb2a76f88d5032c53365995a2bb7b691485e96 --- cmake/MujocoDependencies.cmake | 4 ++-- python/mujoco/CMakeLists.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmake/MujocoDependencies.cmake b/cmake/MujocoDependencies.cmake index 87bb13a7..86c860a2 100644 --- a/cmake/MujocoDependencies.cmake +++ b/cmake/MujocoDependencies.cmake @@ -39,7 +39,7 @@ set(MUJOCO_DEP_VERSION_qhull CACHE STRING "Version of `qhull` to be fetched." ) set(MUJOCO_DEP_VERSION_Eigen3 - aa6964bf3a34fd607837dd8123bc42465185c4f8 + 454f89af9d6f3525b1df5f9ef9c86df58bf2d4d3 CACHE STRING "Version of `Eigen3` to be fetched." ) @@ -54,7 +54,7 @@ set(MUJOCO_DEP_VERSION_gtest ) set(MUJOCO_DEP_VERSION_benchmark - 344117638c8ff7e239044fd0fa7085839fc03021 # v1.8.3 + e45585a4b8e75c28479fa4107182c28172799640 # v1.8.3 CACHE STRING "Version of `benchmark` to be fetched." ) diff --git a/python/mujoco/CMakeLists.txt b/python/mujoco/CMakeLists.txt index b4dc3f28..1575f33b 100644 --- a/python/mujoco/CMakeLists.txt +++ b/python/mujoco/CMakeLists.txt @@ -173,7 +173,7 @@ findorfetch( GIT_REPO https://gitlab.com/libeigen/eigen GIT_TAG - aa6964bf3a34fd607837dd8123bc42465185c4f8 + 54f89af9d6f3525b1df5f9ef9c86df58bf2d4d3 TARGETS Eigen3::Eigen EXCLUDE_FROM_ALL From 0d37670698b49e78594c635b7f54682ce51ec95d Mon Sep 17 00:00:00 2001 From: Baruch Tabanpour Date: Tue, 12 Dec 2023 14:21:49 -0800 Subject: [PATCH 066/121] Fix eigen3 commit hash. PiperOrigin-RevId: 590335535 Change-Id: I90819d420e5ef54dfc5b202ce0407e1ea833a8a4 --- python/mujoco/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/mujoco/CMakeLists.txt b/python/mujoco/CMakeLists.txt index 1575f33b..c9ed748e 100644 --- a/python/mujoco/CMakeLists.txt +++ b/python/mujoco/CMakeLists.txt @@ -173,7 +173,7 @@ findorfetch( GIT_REPO https://gitlab.com/libeigen/eigen GIT_TAG - 54f89af9d6f3525b1df5f9ef9c86df58bf2d4d3 + 454f89af9d6f3525b1df5f9ef9c86df58bf2d4d3 TARGETS Eigen3::Eigen EXCLUDE_FROM_ALL From b82c382c53d866b391fde85914ee172634270ef5 Mon Sep 17 00:00:00 2001 From: Tyler Lindberg Date: Tue, 12 Dec 2023 16:28:36 -0800 Subject: [PATCH 067/121] Use positional arguments with jp.where --- mjx/tutorial.ipynb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mjx/tutorial.ipynb b/mjx/tutorial.ipynb index 522b0208..f27461c6 100644 --- a/mjx/tutorial.ipynb +++ b/mjx/tutorial.ipynb @@ -416,10 +416,8 @@ " forward_reward = self._forward_reward_weight * velocity[0]\n", "\n", " min_z, max_z = self._healthy_z_range\n", - " is_healthy = jp.where(data.qpos[2] \u003c min_z, x=0.0, y=1.0)\n", - " is_healthy = jp.where(\n", - " data.qpos[2] \u003e max_z, x=0.0, y=is_healthy\n", - " )\n", + " is_healthy = jp.where(data.qpos[2] \u003c min_z, 0.0, 1.0)\n", + " is_healthy = jp.where(data.qpos[2] \u003e max_z, 0.0, is_healthy)\n", " if self._terminate_when_unhealthy:\n", " healthy_reward = self._healthy_reward\n", " else:\n", From 5b2c98f8f1fa7b0ddbce34ab7c5382bf52f41901 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Wed, 13 Dec 2023 13:12:29 -0800 Subject: [PATCH 068/121] When saving XMLs, don't round floats that are bigger than INT_MAX. Fixes #1278 PiperOrigin-RevId: 590692020 Change-Id: Icd11d20f47bc4163eb4378f4cb031b8fd9d13612 --- src/xml/xml_util.cc | 4 +++- test/xml/xml_native_writer_test.cc | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/xml/xml_util.cc b/src/xml/xml_util.cc index 0d1a7f75..d7a09afc 100644 --- a/src/xml/xml_util.cc +++ b/src/xml/xml_util.cc @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -1004,7 +1005,8 @@ void mjXUtil::WriteAttr(XMLElement* elem, string name, int n, const T* data, con } // append number - if (isint(data[i])) { + double doubledata = static_cast(data[i]); + if (doubledata < INT_MAX && doubledata > -INT_MAX && isint(data[i])) { stream << Round(data[i]); } else { stream << data[i]; diff --git a/test/xml/xml_native_writer_test.cc b/test/xml/xml_native_writer_test.cc index f5989279..4b5b7f9e 100644 --- a/test/xml/xml_native_writer_test.cc +++ b/test/xml/xml_native_writer_test.cc @@ -1178,5 +1178,29 @@ TEST_F(DecompilerTest, DoesntSaveInferredStatitics) { mj_deleteModel(model); } +TEST_F(DecompilerTest, VeryLargeNumbers) { + static constexpr char xml[] = R"( + + + + + + + + + + + )"; + std::array error; + mjModel* model = LoadModelFromString(xml, error.data(), error.size()); + ASSERT_THAT(model, NotNull()) << error.data(); + std::string saved_xml = SaveAndReadXml(model); + // note, focal is float and loses precision 16777217 -> 16777216 + EXPECT_THAT(saved_xml, HasSubstr("focal=\"16777216 1\"")); + EXPECT_THAT(saved_xml, HasSubstr("pos=\"1e+20 0 0\"")); + EXPECT_THAT(saved_xml, HasSubstr("range=\"-1e+10 1e+10\"")); + mj_deleteModel(model); +} + } // namespace } // namespace mujoco From 1c3455363179c86ec03f4f01ba47d085f3b4a3db Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Wed, 13 Dec 2023 13:13:12 -0800 Subject: [PATCH 069/121] Add shortcuts for power function in impedance computation. PiperOrigin-RevId: 590692245 Change-Id: I1a49475c9a8299f4845e4ec33c9a7ce21a9c5d2b --- src/engine/engine_core_constraint.c | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/engine/engine_core_constraint.c b/src/engine/engine_core_constraint.c index d1cb4a6c..fbb8ff9a 100644 --- a/src/engine/engine_core_constraint.c +++ b/src/engine/engine_core_constraint.c @@ -1325,6 +1325,19 @@ static void getposdim(const mjModel* m, const mjData* d, int i, mjtNum* pos, int +// return a to the power of b, quick return for powers 1 and 2 +// solimp[4] == 2 is the default, so these branches are common +static mjtNum power(mjtNum a, mjtNum b) { + if (b == 1) { + return a; + } else if (b == 2) { + return a*a; + } + return mju_pow(a, b); +} + + + // compute impedance and derivative for one constraint static void getimpedance(const mjtNum* solimp, mjtNum pos, mjtNum margin, mjtNum* imp, mjtNum* impP) { @@ -1359,16 +1372,16 @@ static void getimpedance(const mjtNum* solimp, mjtNum pos, mjtNum margin, // y(x) = a*x^p if x<=midpoint else if (x <= solimp[3]) { - mjtNum a = 1/mju_pow(solimp[3], solimp[4]-1); - y = a*mju_pow(x, solimp[4]); - yP = solimp[4] * a*mju_pow(x, solimp[4]-1); + mjtNum a = 1/power(solimp[3], solimp[4]-1); + y = a*power(x, solimp[4]); + yP = solimp[4] * a*power(x, solimp[4]-1); } // y(x) = 1-b*(1-x)^p is x>midpoint else { - mjtNum b = 1/mju_pow(1-solimp[3], solimp[4]-1); - y = 1-b*mju_pow(1-x, solimp[4]); - yP = solimp[4] * b*mju_pow(1-x, solimp[4]-1); + mjtNum b = 1/power(1-solimp[3], solimp[4]-1); + y = 1-b*power(1-x, solimp[4]); + yP = solimp[4] * b*power(1-x, solimp[4]-1); } // scale From 537d649e95d5de559786ae55a0f3bb83cffd1426 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Wed, 13 Dec 2023 13:21:43 -0800 Subject: [PATCH 070/121] Improvements to transmission documentation. PiperOrigin-RevId: 590694542 Change-Id: I9dc25655116c9300061095c2a2b701530bbbbeca --- doc/computation/index.rst | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/doc/computation/index.rst b/doc/computation/index.rst index 8afdb7cf..788cdd14 100644 --- a/doc/computation/index.rst +++ b/doc/computation/index.rst @@ -300,19 +300,22 @@ is attached; the possible attachment object types are :at:`joint`, :at:`tendon`, Slider-cranks can also be modeled explicitly by creating MuJoCo bodies and coupling them with equality constraints to the rest of the system, but that would be less efficient. -:at:`site` - :at:`site` transmission (without a :at:`refsite`, see below) and :at:`body` transmission targets have a fixed zero - length :math:`l_i(q) = 0`. They can therefore not be used to maintain a desired length, but can be used to apply - forces. Site transmissions correspond to applying a Cartsian force/torque at the site, and are useful for modeling - jets and propellors. :el:`body` transmissions correspond to applying forces at contact points belonging to a body, in +:at:`body` + :el:`body` transmission corresponds to applying forces at contact points belonging to a body, in order to model vacuum grippers and biomechanical adhesive appendages. For more information about adhesion, see the - :ref:`adhesion` actuator documentation. + :ref:`adhesion` actuator documentation. These transmission targets have a fixed zero length + :math:`l_i(q) = 0`. - If a :at:`site` transmission target is defined with the optional :at:`refsite` attribute, forces and torques are - applied in the frame of the reference site rather than the site's own frame. If a reference site is defined then - the length of the actuator is nonzero and corresponds to the pose difference of the two sites. This length can then - be controlled with a :el:`position` actuator, enabling Cartesian end-effector control. See the - :ref:`refsite` documentation for more details. +:at:`site` + Site transmissions correspond to applying a Cartsian force/torque in the frame of a site, and are useful for + modeling jets and propellors. When a :at:`refsite` is not defined (see below), these targets have a fixed zero + length :math:`l_i(q) = 0`. + + If a :at:`site` transmission is defined with the optional :at:`refsite` attribute, forces and torques are applied in + the frame of the reference site rather than the site's own frame. If a reference site is defined, the length of the + actuator is nonzero and corresponds to the pose difference of the two sites, projected onto a chosen direction in the + reference frame. This length can then be controlled with a :el:`position` actuator, allowing for Cartesian + end-effector control. See the :ref:`refsite` documentation for more details. .. _geActivation: From c2c76195dd5c706e4427915b0aa374df4f13e36e Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Wed, 13 Dec 2023 14:25:10 -0800 Subject: [PATCH 071/121] Updating Mujoco Release version number to v3.1.1 PiperOrigin-RevId: 590712172 Change-Id: Ibf32322222212c1bf098e75bbb2e145344c57b93 --- 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 bfc599f8..40931b19 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,7 +28,7 @@ set(MSVC_INCREMENTAL_DEFAULT ON) project( mujoco - VERSION 3.1.0 + VERSION 3.1.1 DESCRIPTION "MuJoCo Physics Simulator" HOMEPAGE_URL "https://mujoco.org" ) diff --git a/dist/mujoco.rc b/dist/mujoco.rc index 484597a9..6a7fb828 100644 --- a/dist/mujoco.rc +++ b/dist/mujoco.rc @@ -1,6 +1,6 @@ 1 VERSIONINFO -FILEVERSION 3,1,0,0 -PRODUCTVERSION 3,1,0,0 +FILEVERSION 3,1,1,0 +PRODUCTVERSION 3,1,1,0 FILEOS 0x4 FILETYPE 0x1 { @@ -9,9 +9,9 @@ FILETYPE 0x1 BLOCK "040904b0" { VALUE "ProductName", "MuJoCo" - VALUE "ProductVersion", "3.1.0" + VALUE "ProductVersion", "3.1.1" VALUE "FileDescription", "MuJoCo" - VALUE "FileVersion", "3.1.0" + VALUE "FileVersion", "3.1.1" VALUE "InternalName", "mujoco.dll" VALUE "OriginalFilename", "mujoco.dll" VALUE "CompanyName", "Google DeepMind" diff --git a/dist/simulate.rc b/dist/simulate.rc index ae73084e..14627b0b 100644 --- a/dist/simulate.rc +++ b/dist/simulate.rc @@ -1,8 +1,8 @@ MUJOCO ICON "mujoco.ico" 1 VERSIONINFO -FILEVERSION 3,1,0,0 -PRODUCTVERSION 3,1,0,0 +FILEVERSION 3,1,1,0 +PRODUCTVERSION 3,1,1,0 FILEOS 0x4 FILETYPE 0x1 { @@ -11,9 +11,9 @@ FILETYPE 0x1 BLOCK "040904b0" { VALUE "ProductName", "MuJoCo" - VALUE "ProductVersion", "3.1.0" + VALUE "ProductVersion", "3.1.1" VALUE "FileDescription", "MuJoCo" - VALUE "FileVersion", "3.1.0" + VALUE "FileVersion", "3.1.1" 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 3101f9b2..c43f3145 100644 --- a/doc/APIreference/APIglobals.rst +++ b/doc/APIreference/APIglobals.rst @@ -522,7 +522,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`` - - 310 + - 311 - 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 f3a97af7..0a608df9 100644 --- a/doc/unity.rst +++ b/doc/unity.rst @@ -30,14 +30,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.1.0.dylib`` (it can be +``/Applications/MuJoCo.app/Contents/Frameworks/mujoco.framework/Versions/Current/libmujoco.3.1.1.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.1.0/lib/libmujoco.so.3.1.0`` and rename it as ``libmujoco.so``. +``~/.mujoco/mujoco-3.1.1/lib/libmujoco.so.3.1.1`` and rename it as ``libmujoco.so``. Windows _______ diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index 2935d5a8..ea861353 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -24,7 +24,7 @@ extern "C" { #endif // header version; should match the library version as returned by mj_version() -#define mjVERSION_HEADER 310 +#define mjVERSION_HEADER 311 // needed to define size_t, fabs and log10 #include diff --git a/mjx/pyproject.toml b/mjx/pyproject.toml index d615108d..a558c2ed 100644 --- a/mjx/pyproject.toml +++ b/mjx/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name="mujoco-mjx" -version = "3.1.0" +version = "3.1.1" authors = [ {name = "Google DeepMind", email = "mujoco@deepmind.com"}, ] @@ -31,13 +31,13 @@ dependencies = [ "etils[epath]", "jax", "jaxlib", - "mujoco>=3.1.0.dev0", + "mujoco>=3.1.1.dev0", "scipy", "trimesh", ] [project.urls] Homepage = "https://github.com/google-deepmind/mujoco/tree/main/mjx" -Documentation = "https://mujoco.readthedocs.io/en/3.1.0" +Documentation = "https://mujoco.readthedocs.io/en/3.1.1" Repository = "https://github.com/google-deepmind/mujoco/tree/main/mjx" -Changelog = "https://mujoco.readthedocs.io/en/3.1.0/changelog.html" +Changelog = "https://mujoco.readthedocs.io/en/3.1.1/changelog.html" diff --git a/python/mujoco/CMakeLists.txt b/python/mujoco/CMakeLists.txt index c9ed748e..e782497d 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.1.0.dylib + ${MUJOCO_FRAMEWORK}/mujoco.framework/Versions/A/libmujoco.3.1.1.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.1.0 HINTS ${MUJOCO_LIBRARY_DIR} REQUIRED + MUJOCO_LIBRARY mujoco mujoco.3.1.1 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 18a27c2f..7b2dfa5e 100644 --- a/python/mujoco/mjpython/Info.plist +++ b/python/mujoco/mjpython/Info.plist @@ -7,13 +7,13 @@ CFBundleIdentifier org.mujoco.mjpython CFBundleVersion - 3.1.0 + 3.1.1 CFBundleGetInfoString - 3.1.0 + 3.1.1 CFBundleLongVersionString - 3.1.0 + 3.1.1 CFBundleShortVersionString - 3.1.0 + 3.1.1 CFBundleExecutable mjpython CFBundleIconFile diff --git a/python/pyproject.toml b/python/pyproject.toml index 6f934c1d..ab298dc4 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mujoco" -version = "3.1.0" +version = "3.1.1" authors = [ {name = "Google DeepMind", email = "mujoco@deepmind.com"}, ] @@ -36,9 +36,9 @@ dynamic = ["readme", "scripts"] [project.urls] Homepage = "https://github.com/google-deepmind/mujoco" -Documentation = "https://mujoco.readthedocs.io/en/3.1.0" +Documentation = "https://mujoco.readthedocs.io/en/3.1.1" Repository = "https://github.com/google-deepmind/mujoco" -Changelog = "https://mujoco.readthedocs.io/en/3.1.0/changelog.html" +Changelog = "https://mujoco.readthedocs.io/en/3.1.1/changelog.html" [tool.setuptools] include-package-data = false diff --git a/sample/CMakeLists.txt b/sample/CMakeLists.txt index 0e989480..0d2c2c7f 100644 --- a/sample/CMakeLists.txt +++ b/sample/CMakeLists.txt @@ -24,7 +24,7 @@ set(MSVC_INCREMENTAL_DEFAULT ON) project( mujoco_samples - VERSION 3.1.0 + VERSION 3.1.1 DESCRIPTION "MuJoCo samples binaries" HOMEPAGE_URL "https://mujoco.org" ) diff --git a/simulate/CMakeLists.txt b/simulate/CMakeLists.txt index e16584e0..fb416535 100644 --- a/simulate/CMakeLists.txt +++ b/simulate/CMakeLists.txt @@ -29,7 +29,7 @@ set(MUJOCO_DEP_VERSION_lodepng project( mujoco_simulate - VERSION 3.1.0 + VERSION 3.1.1 DESCRIPTION "MuJoCo simulate binaries" HOMEPAGE_URL "https://mujoco.org" ) diff --git a/src/engine/engine_support.c b/src/engine/engine_support.c index f887d1ca..819dc693 100644 --- a/src/engine/engine_support.c +++ b/src/engine/engine_support.c @@ -38,8 +38,8 @@ //-------------------------- Constants ------------------------------------------------------------- - #define mjVERSION 310 -#define mjVERSIONSTRING "3.1.0" + #define mjVERSION 311 +#define mjVERSIONSTRING "3.1.1" // names of disable flags const char* mjDISABLESTRING[mjNDISABLE] = { diff --git a/unity/Editor/Bindings/MujocoBinaryRetriever.cs b/unity/Editor/Bindings/MujocoBinaryRetriever.cs index 8ed2ac7a..3e9b9bfc 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.1.0.dylib", + "/mujoco.framework/Versions/Current/libmujoco.3.1.1.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.1.0/lib/libmujoco.so.3.1.0", + "/.mujoco/mujoco-3.1.1/lib/libmujoco.so.3.1.1", mujocoPath + "/libmujoco.so"); AssetDatabase.Refresh(); } diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index 6f797926..c4969e46 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -108,7 +108,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 = 310; +public const int mjVERSION_HEADER = 311; // ------------------------------------Enums------------------------------------ diff --git a/unity/package.json b/unity/package.json index f8f658d7..a6f629b9 100644 --- a/unity/package.json +++ b/unity/package.json @@ -1,7 +1,7 @@ { "name": "org.mujoco", "displayName": "MuJoCo", - "version": "3.1.0", + "version": "3.1.1", "description": "MuJoCo importer and runtime plug-in", "dependencies": {}, "author": { From da211ddf29400ffc7bbd981d21a7325cc113d4f8 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Wed, 13 Dec 2023 16:05:19 -0800 Subject: [PATCH 072/121] Improve description of the collision detection pipeline. PiperOrigin-RevId: 590742091 Change-Id: Idfcc37cece21dcf46401a147a43aa8ec4eeb05aa --- doc/computation/index.rst | 49 ++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/doc/computation/index.rst b/doc/computation/index.rst index 788cdd14..190be5ac 100644 --- a/doc/computation/index.rst +++ b/doc/computation/index.rst @@ -307,9 +307,9 @@ is attached; the possible attachment object types are :at:`joint`, :at:`tendon`, :math:`l_i(q) = 0`. :at:`site` - Site transmissions correspond to applying a Cartsian force/torque in the frame of a site, and are useful for - modeling jets and propellors. When a :at:`refsite` is not defined (see below), these targets have a fixed zero - length :math:`l_i(q) = 0`. + Site transmissions correspond to applying a Cartsian force/torque in the frame of a site. When a :at:`refsite` is not + defined (see below), these targets have a fixed zero length :math:`l_i(q) = 0` and are useful for modeling jets and + propellors: forces and torques which are fixed to the site frame. If a :at:`site` transmission is defined with the optional :at:`refsite` attribute, forces and torques are applied in the frame of the reference site rather than the site's own frame. If a reference site is defined, the length of the @@ -1458,48 +1458,45 @@ others can be pruned quickly without a detailed check. MuJoCo has flexible mecha checked in detail. The decision process involves two stages: generation and filtering. Generation - First we generate a list of candidate geom pairs in one of two ways: "pair" or "dynamic". The user can also specify - "all" which merges both sources (and is the default). This is done via the setting ``mjModel.opt.collision``. "Pair" - refers to an explicit list of geom pairs defined with the :ref:`pair ` element in MJCF. It gives the - user full control, however it is a static mechanism (independent of the spatial arrangement of the geoms at runtime) - and can be tedious for large models. It is normally used to supplement the output of the "dynamic" mechanism. Dynamic - generation works with bodies rather than geoms; when a body pair is included this means that all geoms attached to - one body can collide with all geoms attached to the other body. + First we generate a list of candidate geom pairs by merging from two sources: pairs of bodies that might contain + colliding geoms and the explicit list of geom pairs defined with the :ref:`pair ` element in MJCF. The body pairs are generated via broad-phase collision detection based on a modified sweep-and-prune algorithm. The modification is that the axis for sorting is chosen as the principal eigenvector of the covariance matrix of all geom - centers - which maximizes the spread. Then, for each body pair, a mid-phase collision detection using a static - bounding volume hierarchy (a BVH binary tree) of axis-aligned bounding boxes (AABB) is performed. Each body is - equipped with an AABB tree of its geoms, aligned with the body inertial or geom frames for all inner or leaf nodes, + centers -- which maximizes the spread. Then, for each body pair, mid-phase collision detection is performed using a + static bounding volume hierarchy (a BVH binary tree) of axis-aligned bounding boxes (AABB). Each body is equipped + with an AABB tree of its geoms, aligned with the body inertial or geom frames for all inner or leaf nodes, respectively. - Finally, the user can explicitly exclude certain body pairs using the :ref:`exclude ` element - in MJCF. Exclusion is applied when "dynamic" or "all" are selected, but not when "pair" is selected. At the end of - this step we have a list of geoms pairs that is typically much smaller than :math:`n (n-1)/2`, but can still be - pruned further before detailed collision checking. + Finally, the user can explicitly exclude certain body pairs using the :ref:`exclude ` element in + MJCF. At the end of this step we have a list of geoms pairs that is typically much smaller than :math:`n (n-1)/2`, + but can still be pruned further before detailed collision checking. Filtering Next we apply four filters to the list generated in the previous step. Filters 1 and 2 are applied to all geom pairs. - Filters 3 and 4 are applied only to pairs generated by the "dynamic" mechanism, thereby allowing the user to bypass + Filters 3 and 4 are applied only to pairs generated by the body-pair mechanism, thereby allowing the user to bypass those filters by specifying geom pairs explicitly. - #. The types of the two geoms must correspond to a collision function that is capable of performing the detailed + 1. The types of the two geoms must correspond to a collision function that is capable of performing the detailed check. This is usually the case but there are exceptions (for example plane-plane collisions are not supported), and furthermore the user may override the default table of collision functions with NULL pointers, effectively disabling collisions between certain geom types. - #. A bounding sphere test is applied, taking into account the contact margin. If one of the geoms in the pair is a + 2. A bounding sphere test is applied, taking into account the contact margin. If one of the geoms in the pair is a plane, this becomes a plane-sphere test. - #. The two geoms cannot belong to the same body. Furthermore, they cannot belong to a parent and a child body, unless + 3. The two geoms cannot belong to the same body. Furthermore, they cannot belong to a parent and a child body, unless the parent is the world body. The motivation is to avoid permanent contacts within bodies and joints. Note that if several bodies are welded together in the sense that there are no joints between them, they are treated as a single body for the purposes of this test. The parent-filter test can be disabled by the user, while the same-body test cannot be disabled. - #. The two geoms must be "compatible" in the following sense. Each geom has integer parameters ``contype`` and + 4. The two geoms must be "compatible" in the following sense. Each geom has integer parameters ``contype`` and ``conaffinity``. The boolean expression below must be true for the test to pass: - ``(contype1 & conaffinity2) || (contype2 & conaffinity1)`` This requires the ``contype`` of one geom and the - ``conaffinity`` of the other geom to have a common bit set to 1. This is a powerful mechanism borrowed from the - Open Dynamics Engine. The default setting for all geoms is ``contype = conaffinity = 1`` which always passes the - test, so the user can ignore this mechanism if it is confusing at first. + + ``(contype1 & conaffinity2) || (contype2 & conaffinity1)`` + + This requires the ``contype`` of one geom and the ``conaffinity`` of the other geom to have a common bit set to 1. + This is a powerful mechanism borrowed from Open Dynamics Engine. The default setting for all geoms is + ``contype = conaffinity = 1`` which always passes the test, so the user can ignore this mechanism if it is + confusing at first. .. _coChecking: From 0915d69c3f5610ca22be7997e2328cc1261fefe9 Mon Sep 17 00:00:00 2001 From: Erik Frey Date: Wed, 13 Dec 2023 16:33:44 -0800 Subject: [PATCH 073/121] Correctly transform site_xmat in MJX get_data/put_data. PiperOrigin-RevId: 590750273 Change-Id: I836e1f5d20725ed9b93bf86fe3e8e61954a56b47 --- mjx/mujoco/mjx/_src/io.py | 6 +++--- mjx/mujoco/mjx/_src/io_test.py | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/mjx/mujoco/mjx/_src/io.py b/mjx/mujoco/mjx/_src/io.py index 9658ad53..9396d7f3 100644 --- a/mjx/mujoco/mjx/_src/io.py +++ b/mjx/mujoco/mjx/_src/io.py @@ -217,7 +217,7 @@ def _get_contact( value = value.reshape((-1, 9)) getattr(c, field.name)[:] = value - ncon = con_id.shape[0] + ncon = cx.dist.shape[0] c.efc_address[:] = np.arange(efc_start, efc_start + ncon * 4, 4)[con_id] @@ -257,7 +257,7 @@ def get_data( value = getattr(dx_i, field.name) - if field.name in ('xmat', 'ximat', 'geom_xmat'): + if field.name in ('xmat', 'ximat', 'geom_xmat', 'site_xmat'): value = value.reshape((-1, 9)) if field.name in ('efc_frictionloss', 'efc_D', 'efc_aref', 'efc_force'): @@ -318,7 +318,7 @@ def put_data(m: mujoco.MjModel, d: mujoco.MjData, device=None) -> types.Data: if f.type is jax.Array } - for fname in ('xmat', 'ximat', 'geom_xmat'): + for fname in ('xmat', 'ximat', 'geom_xmat', 'site_xmat'): fields[fname] = fields[fname].reshape((-1, 3, 3)) # pad efc fields: MuJoCo efc arrays are sparse for inactive constraints. diff --git a/mjx/mujoco/mjx/_src/io_test.py b/mjx/mujoco/mjx/_src/io_test.py index 4c7b0065..a16a8780 100644 --- a/mjx/mujoco/mjx/_src/io_test.py +++ b/mjx/mujoco/mjx/_src/io_test.py @@ -71,6 +71,7 @@ _MULTIPLE_CONSTRAINTS = """ + @@ -318,9 +319,11 @@ class IoTest(parameterized.TestCase): self.assertEqual(dx.xmat.shape, (3, 3, 3)) self.assertEqual(dx.ximat.shape, (3, 3, 3)) self.assertEqual(dx.geom_xmat.shape, (3, 3, 3)) + self.assertEqual(dx.site_xmat.shape, (1, 3, 3)) np.testing.assert_allclose(dx.xmat.reshape((3, 9)), d.xmat) np.testing.assert_allclose(dx.ximat.reshape((3, 9)), d.ximat) np.testing.assert_allclose(dx.geom_xmat.reshape((3, 9)), d.geom_xmat) + np.testing.assert_allclose(dx.site_xmat.reshape((1, 9)), d.site_xmat) # efc_ are also shape transformed and padded self.assertEqual(dx.efc_J.shape, (21, 8)) # nefc, nv @@ -369,13 +372,15 @@ class IoTest(parameterized.TestCase): self.assertEqual(d_2.contact.frame.shape, (1, 9)) np.testing.assert_allclose(d_2.contact.frame, d.contact.frame) - # xmat, ximat, geom_xmat are all shape transformed + # xmat, ximat, geom_xmat, site_xmat are all shape transformed self.assertEqual(d_2.xmat.shape, (3, 9)) self.assertEqual(d_2.ximat.shape, (3, 9)) self.assertEqual(d_2.geom_xmat.shape, (3, 9)) + self.assertEqual(d_2.site_xmat.shape, (1, 9)) np.testing.assert_allclose(d_2.xmat, d.xmat) np.testing.assert_allclose(d_2.ximat, d.ximat) np.testing.assert_allclose(d_2.geom_xmat, d.geom_xmat) + np.testing.assert_allclose(d_2.site_xmat, d.site_xmat) # efc_* are also shape transformed and filtered self.assertEqual(d_2.efc_J.shape, (64,)) # nefc * nv From 9f6be3135ecc077700ab69d60f0826bee43f24a9 Mon Sep 17 00:00:00 2001 From: Kyle Bayes Date: Thu, 14 Dec 2023 11:11:00 -0800 Subject: [PATCH 074/121] Modify mjListKeyMap to be a fixed-size array, and add internal object_lists variable to mjCModel to remove redundant code. PiperOrigin-RevId: 590997348 Change-Id: I5f4d7de605f44d40a75eb1e9ceec208325fa3be5 --- src/user/user_model.cc | 234 ++++++++--------------------------- src/user/user_model.h | 6 +- test/user/user_model_test.cc | 27 +++- 3 files changed, 80 insertions(+), 187 deletions(-) diff --git a/src/user/user_model.cc b/src/user/user_model.cc index c1c0300e..a4f5a2dd 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -147,7 +147,6 @@ mjCModel::mjCModel() { nuser_sensor = -1; //------------------------ private variables - ids.clear(); cameras.clear(); lights.clear(); flexes.clear(); @@ -183,6 +182,35 @@ mjCModel::mjCModel() { world->name = "world"; world->def = defaults[0]; bodies.push_back(world); + + for (int i = 0; i < mjNOBJECT; ++i) { + object_lists[i] = nullptr; + } + + object_lists[mjOBJ_BODY] = (std::vector*) &bodies; + object_lists[mjOBJ_XBODY] = (std::vector*) &bodies; + object_lists[mjOBJ_JOINT] = (std::vector*) &joints; + object_lists[mjOBJ_GEOM] = (std::vector*) &geoms; + object_lists[mjOBJ_SITE] = (std::vector*) &sites; + object_lists[mjOBJ_CAMERA] = (std::vector*) &cameras; + object_lists[mjOBJ_LIGHT] = (std::vector*) &lights; + object_lists[mjOBJ_FLEX] = (std::vector*) &flexes; + object_lists[mjOBJ_MESH] = (std::vector*) &meshes; + object_lists[mjOBJ_SKIN] = (std::vector*) &skins; + object_lists[mjOBJ_HFIELD] = (std::vector*) &hfields; + object_lists[mjOBJ_TEXTURE] = (std::vector*) &textures; + object_lists[mjOBJ_MATERIAL] = (std::vector*) &materials; + object_lists[mjOBJ_PAIR] = (std::vector*) &pairs; + object_lists[mjOBJ_EXCLUDE] = (std::vector*) &excludes; + object_lists[mjOBJ_EQUALITY] = (std::vector*) &equalities; + object_lists[mjOBJ_TENDON] = (std::vector*) &tendons; + object_lists[mjOBJ_ACTUATOR] = (std::vector*) &actuators; + object_lists[mjOBJ_SENSOR] = (std::vector*) &sensors; + object_lists[mjOBJ_NUMERIC] = (std::vector*) &numerics; + object_lists[mjOBJ_TEXT] = (std::vector*) &texts; + object_lists[mjOBJ_TUPLE] = (std::vector*) &tuples; + object_lists[mjOBJ_KEY] = (std::vector*) &keys; + object_lists[mjOBJ_PLUGIN] = (std::vector*) &plugins; } @@ -213,7 +241,6 @@ mjCModel::~mjCModel() { for (int i=0; isize(); } // get pointer to specified object mjCBase* mjCModel::GetObject(mjtObj type, int id) { - if (id>=0 && id= NumObjects(type)) { + return nullptr; } - - return 0; + return (*object_lists[type])[id]; } @@ -666,55 +595,10 @@ static T* findobject(std::string_view name, const vector& list, const mjKeyM // find object in global lists given string type and name mjCBase* mjCModel::FindObject(mjtObj type, string name) { - switch (type) { - case mjOBJ_BODY: - case mjOBJ_XBODY: - return findobject(name, bodies, ids["body"]); - case mjOBJ_JOINT: - return findobject(name, joints, ids["joint"]); - case mjOBJ_GEOM: - return findobject(name, geoms, ids["geom"]); - case mjOBJ_SITE: - return findobject(name, sites, ids["site"]); - case mjOBJ_CAMERA: - return findobject(name, cameras, ids["camera"]); - case mjOBJ_LIGHT: - return findobject(name, lights, ids["light"]); - case mjOBJ_FLEX: - return findobject(name, flexes, ids["flex"]); - case mjOBJ_MESH: - return findobject(name, meshes, ids["mesh"]); - case mjOBJ_SKIN: - return findobject(name, skins, ids["skin"]); - case mjOBJ_HFIELD: - return findobject(name, hfields, ids["hfield"]); - case mjOBJ_TEXTURE: - return findobject(name, textures, ids["texture"]); - case mjOBJ_MATERIAL: - return findobject(name, materials, ids["material"]); - case mjOBJ_PAIR: - return findobject(name, pairs, ids["pair"]); - case mjOBJ_EXCLUDE: - return findobject(name, excludes, ids["exclude"]); - case mjOBJ_EQUALITY: - return findobject(name, equalities, ids["equality"]); - case mjOBJ_TENDON: - return findobject(name, tendons, ids["tendon"]); - case mjOBJ_ACTUATOR: - return findobject(name, actuators, ids["actuator"]); - case mjOBJ_SENSOR: - return findobject(name, sensors, ids["sensor"]); - case mjOBJ_NUMERIC: - return findobject(name, numerics, ids["numeric"]); - case mjOBJ_TEXT: - return findobject(name, texts, ids["text"]); - case mjOBJ_TUPLE: - return findobject(name, tuples, ids["tuple"]); - case mjOBJ_PLUGIN: - return findobject(name, plugins, ids["plugin"]); - default: - return 0; + if (!object_lists[type]) { + return nullptr; } + return findobject(name, *object_lists[type], ids[type]); } @@ -2649,37 +2533,37 @@ static void reassignid(vector& list) { // set ids, check for repeated names template static void processlist(mjListKeyMap& ids, vector& list, - string defname, bool checkrepeat = true) { + mjtObj type, bool checkrepeat = true) { // loop over list elements - for (int i=0; i<(int)list.size(); i++) { + for (size_t i=0; i < list.size(); i++) { // check for incompatible id setting; SHOULD NOT OCCUR if (list[i]->id!=-1 && list[i]->id!=i) { - throw mjCError(list[i], "incompatible id in %s array, position %d", defname.c_str(), i); + throw mjCError(list[i], "incompatible id in %s array, position %d", mju_type2Str(type), i); } // id equals position in array list[i]->id = i; // add to ids map - ids[defname][list[i]->name] = i; + ids[type][list[i]->name] = i; } // check for repeated names if (checkrepeat) { // created vectors with all names vector allnames; - for (int i=0; i<(int)list.size(); i++) { + for (size_t i=0; i < list.size(); i++) { if (!list[i]->name.empty()) { allnames.push_back(list[i]->name); } } // sort and check for duplicates - if (allnames.size()>1) { + if (allnames.size() > 1) { std::sort(allnames.begin(), allnames.end()); auto adjacent = std::adjacent_find(allnames.begin(), allnames.end()); - if (adjacent!=allnames.end()) { - string msg = "repeated name '" + *adjacent + "' in " + defname; + if (adjacent != allnames.end()) { + string msg = "repeated name '" + *adjacent + "' in " + mju_type2Str(type); throw mjCError(NULL, msg.c_str()); } } @@ -2813,29 +2697,11 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) { CheckEmptyNames(); // set object ids, check for repeated names - processlist(ids, bodies, "body"); - processlist(ids, joints, "joint"); - processlist(ids, geoms, "geom"); - processlist(ids, sites, "site"); - processlist(ids, cameras, "camera"); - processlist(ids, lights, "light"); - processlist(ids, flexes, "flex"); - processlist(ids, meshes, "mesh"); - processlist(ids, skins, "skin"); - processlist(ids, hfields, "hfield"); - processlist(ids, textures, "texture"); - processlist(ids, materials, "material"); - processlist(ids, pairs, "pair"); - processlist(ids, excludes, "exclude"); - processlist(ids, equalities, "equality"); - processlist(ids, tendons, "tendon"); - processlist(ids, actuators, "actuator"); - processlist(ids, sensors, "sensor"); - processlist(ids, numerics, "numeric"); - processlist(ids, texts, "text"); - processlist(ids, tuples, "tuple"); - processlist(ids, keys, "key"); - processlist(ids, plugins, "plugin"); + for (int i = 0; i < mjNOBJECT; i++) { + if (i != mjOBJ_XBODY && object_lists[i]) { + processlist(ids, *object_lists[i], (mjtObj) i); + } + } // convert names into indices IndexAssets(); diff --git a/src/user/user_model.h b/src/user/user_model.h index 85ae8ec9..222cbccf 100644 --- a/src/user/user_model.h +++ b/src/user/user_model.h @@ -32,9 +32,8 @@ typedef enum _mjtInertiaFromGeom { mjINERTIAFROMGEOM_AUTO // use only if inertial element is missing } mjtInertiaFromGeom; -// TODO: convert mjListKeyMap to mjKeyMap[mjNOBJECT] by adding mjNOBJECT to mjtObj typedef std::map > mjKeyMap; -typedef std::map > mjListKeyMap; +typedef std::array mjListKeyMap; @@ -294,6 +293,9 @@ class mjCModel { //------------------------ internal variables + // array of pointers to each object list (enumerated by type) + std::array*, mjNOBJECT> object_lists; + // statistics, as computed by mj_setConst double meaninertia_auto; // mean diagonal inertia, as computed by mj_setConst double meanmass_auto; // mean body mass, as computed by mj_setConst diff --git a/test/user/user_model_test.cc b/test/user/user_model_test.cc index db1059ef..2079b1d1 100644 --- a/test/user/user_model_test.cc +++ b/test/user/user_model_test.cc @@ -30,16 +30,41 @@ namespace { using ::testing::DoubleNear; using ::testing::ElementsAre; +using ::testing::HasSubstr; +using ::testing::IsNull; using ::testing::NotNull; -using UserDataTest = MujocoTest; static std::vector GetRow(const mjtNum* array, int ncolumn, int row) { return std::vector(array + ncolumn * row, array + ncolumn * (row + 1)); } +// ----------------------------- test mjCModel -------------------------------- + +using UserCModelTest = MujocoTest; + +TEST_F(UserCModelTest, RepeatedNames) { + static constexpr char xml[] = R"( + + + + + + + + + )"; + + std::array error; + mjModel* model = LoadModelFromString(xml, error.data(), error.size()); + EXPECT_THAT(model, IsNull()); + EXPECT_THAT(error.data(), HasSubstr("repeated name 'geom1' in geom")); +} + // ------------- test automatic inference of nuser_xxx ------------------------- +using UserDataTest = MujocoTest; + TEST_F(UserDataTest, AutoNUserBody) { static constexpr char xml[] = R"( From f2a967348c64197b9a29fb9e1c7dd9879984e10b Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Thu, 14 Dec 2023 13:29:22 -0800 Subject: [PATCH 075/121] Fix bug introduced in 7942fe957ea046750f936ac002d49425e31d9dab That change removed some spurious contacts returned by mjc_BoxBox, but also removed some desirable contacts that occur during very deep penetration of two boxes (when one box is completely inside another box). This is now fixed. PiperOrigin-RevId: 591036252 Change-Id: I84b51f2179fd6fe29618a4e908e86934c6fc6941 --- src/engine/engine_collision_driver.c | 18 ++++---- src/engine/engine_util_misc.c | 40 ++++++++++++++---- src/engine/engine_util_misc.h | 6 ++- test/engine/engine_collision_box_test.cc | 42 ++++++++++++++----- .../testdata/collision_box/boxbox_deep.xml | 11 +++++ 5 files changed, 90 insertions(+), 27 deletions(-) create mode 100644 test/engine/testdata/collision_box/boxbox_deep.xml diff --git a/src/engine/engine_collision_driver.c b/src/engine/engine_collision_driver.c index d42a4f2e..f6132b74 100644 --- a/src/engine/engine_collision_driver.c +++ b/src/engine/engine_collision_driver.c @@ -1444,9 +1444,6 @@ static void mj_makeCapsule(const mjModel* m, mjData* d, int f, const int vid[2], // test two geoms for collision, apply filters, add to contact list void mj_collideGeoms(const mjModel* m, mjData* d, int g1, int g2) { - // relative distance (1%) outside of which box-box contacts are removed - static mjtNum kBoxRemoveMargin = 1.01; - TM_START; int num, type1, type2, condim; @@ -1547,11 +1544,16 @@ void mj_collideGeoms(const mjModel* m, mjData* d, int g1, int g2) { // box sizes with margin mjtNum sz1[3] = {size1[0] + margin, size1[1] + margin, size1[2] + margin}; mjtNum sz2[3] = {size2[0] + margin, size2[1] + margin, size2[2] + margin}; - mju_scl3(sz1, sz1, kBoxRemoveMargin); - mju_scl3(sz2, sz2, kBoxRemoveMargin); - // mark as bad if outside box - if (mju_outsideBox(con[i].pos, pos1, mat1, sz1) || - mju_outsideBox(con[i].pos, pos2, mat2, sz2)) { + + // relative distance from surface (1%) outside of which box-box contacts are removed + static mjtNum kRemoveRatio = 1.01; + + // is the contact outside: 1, inside: -1, within the removal width: 0 + int out1 = mju_outsideBox(con[i].pos, pos1, mat1, sz1, kRemoveRatio); + int out2 = mju_outsideBox(con[i].pos, pos2, mat2, sz2, kRemoveRatio); + + // mark as bad if outside one box and not inside the other box + if ((out1 == 1 && out2 != -1) || (out2 == 1 && out1 != -1)) { con[i].dim = -1; } } diff --git a/src/engine/engine_util_misc.c b/src/engine/engine_util_misc.c index 29e2e0f8..37eca9e4 100644 --- a/src/engine/engine_util_misc.c +++ b/src/engine/engine_util_misc.c @@ -837,21 +837,47 @@ mjtNum mju_springDamper(mjtNum pos0, mjtNum vel0, mjtNum k, mjtNum b, mjtNum t) -// return 1 if point is outside box given by pos, mat, size +// return 1 if point is outside box given by pos, mat, size * inflate +// return -1 if point is inside box given by pos, mat, size / inflate +// return 0 if point is between the inflated and deflated boxes int mju_outsideBox(const mjtNum point[3], const mjtNum pos[3], const mjtNum mat[9], - const mjtNum size[3]) { + const mjtNum size[3], mjtNum inflate) { + // check inflation coefficient + if (inflate < 1) { + mjERROR("inflation coefficient must be >= 1") + } + // vector from pos to point, projected to box frame mjtNum vec[3] = {point[0]-pos[0], point[1]-pos[1], point[2]-pos[2]}; mju_rotVecMatT(vec, vec, mat); - // outside - if (vec[0] > size[0] || vec[0] < -size[0] || - vec[1] > size[1] || vec[1] < -size[1] || - vec[2] > size[2] || vec[2] < -size[2]) { + // big: inflated box + mjtNum big[3] = {size[0], size[1], size[2]}; + if (inflate > 1) { + mju_scl3(big, big, inflate); + } + + // check if outside big box + if (vec[0] > big[0] || vec[0] < -big[0] || + vec[1] > big[1] || vec[1] < -big[1] || + vec[2] > big[2] || vec[2] < -big[2]) { return 1; } - // inside + // quick return if no inflation + if (inflate == 1) { + return -1; + } + + // check if inside small (deflated) box + mjtNum small[3] = {size[0]/inflate, size[1]/inflate, size[2]/inflate}; + if (vec[0] < small[0] && vec[0] > -small[0] && + vec[1] < small[1] && vec[1] > -small[1] && + vec[2] < small[2] && vec[2] > -small[2]) { + return -1; + } + + // within margin between small and big box return 0; } diff --git a/src/engine/engine_util_misc.h b/src/engine/engine_util_misc.h index c103aba1..cb317258 100644 --- a/src/engine/engine_util_misc.h +++ b/src/engine/engine_util_misc.h @@ -78,9 +78,11 @@ MJAPI void mju_decodePyramid(mjtNum* force, const mjtNum* pyramid, // integrate spring-damper analytically, return pos(dt) MJAPI mjtNum mju_springDamper(mjtNum pos0, mjtNum vel0, mjtNum Kp, mjtNum Kv, mjtNum dt); -// return 1 if point is outside box given by pos, mat, size +// return 1 if point is outside box given by pos, mat, size * inflate +// return -1 if point is inside box given by pos, mat, size / inflate +// return 0 if point is between the inflated and deflated boxes MJAPI int mju_outsideBox(const mjtNum point[3], const mjtNum pos[3], const mjtNum mat[9], - const mjtNum size[3]); + const mjtNum size[3], mjtNum inflate); // print matrix MJAPI void mju_printMat(const mjtNum* mat, int nr, int nc); diff --git a/test/engine/engine_collision_box_test.cc b/test/engine/engine_collision_box_test.cc index 0247335d..5477189d 100644 --- a/test/engine/engine_collision_box_test.cc +++ b/test/engine/engine_collision_box_test.cc @@ -94,7 +94,7 @@ TEST_F(MjCollisionBoxTest, BadContacts) { } // expect some contacts to have been removed - EXPECT_LT(nmatched, num); + EXPECT_LT(nmatched, num) << local_path; // get box info const mjtNum* pos1 = data->geom_xpos + 3 * g1; @@ -103,23 +103,27 @@ TEST_F(MjCollisionBoxTest, BadContacts) { const mjtNum* pos2 = data->geom_xpos + 3 * g2; const mjtNum* mat2 = data->geom_xmat + 9 * g2; const mjtNum* size2 = model->geom_size + 3 * g2; + mjtNum margin = mju_max(model->geom_margin[g1], model->geom_margin[g2]); // loop over raw contacts, find removed for (int i = 0; i < num; i++) { if (!match_raw[i]) { // === check if outside - // get margin and adjusted sizes - const mjtNum kBoxRemoveMargin = 1.01; - mjtNum sz1[3], sz2[3]; - mju_scl3(sz1, size1, kBoxRemoveMargin); - mju_scl3(sz2, size2, kBoxRemoveMargin); + mjtNum sz1[3] = {size1[0]+margin, size1[1]+margin, size1[2]+margin}; + mjtNum sz2[3] = {size2[0]+margin, size2[1]+margin, size2[2]+margin}; - // is contact outside one of the boxes - bool outside = mju_outsideBox(con_raw[i].pos, pos1, mat1, sz1) || - mju_outsideBox(con_raw[i].pos, pos2, mat2, sz2); + // relative distance (1%) outside of which contacts are removed + static mjtNum kRatio = 1.01; - // expect that removed contact was either outside + // is the contact outside: 1, inside: -1, within the removal width: 0 + int out1 = mju_outsideBox(con_raw[i].pos, pos1, mat1, sz1, kRatio); + int out2 = mju_outsideBox(con_raw[i].pos, pos2, mat2, sz2, kRatio); + + // mark as bad if outside one box and not inside the other box + bool outside = (out1 == 1 && out2 != -1) || (out2 == 1 && out1 != -1); + + // expect that removed contact was outside EXPECT_TRUE(outside); } } @@ -222,5 +226,23 @@ TEST_F(MjCollisionBoxTest, DuplicateContacts) { } +static const char* const kDeepFilePath = + "engine/testdata/collision_box/boxbox_deep.xml"; + +TEST_F(MjCollisionBoxTest, DeepPenetration) { + const std::string xml_path = GetTestDataFilePath(kDeepFilePath); + mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, 0, 0); + ASSERT_THAT(model, NotNull()); + mjData* data = mj_makeData(model); + mj_forward(model, data); + + // expect 4 contact + EXPECT_EQ(data->ncon ,4); + + mj_deleteData(data); + mj_deleteModel(model); +} + + } // namespace } // namespace mujoco diff --git a/test/engine/testdata/collision_box/boxbox_deep.xml b/test/engine/testdata/collision_box/boxbox_deep.xml new file mode 100644 index 00000000..65f14e5f --- /dev/null +++ b/test/engine/testdata/collision_box/boxbox_deep.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + From 38fa756a20b8edc218bea622f21c50a333ab02cb Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Fri, 15 Dec 2023 05:35:34 -0800 Subject: [PATCH 076/121] Add `mjGEOM_LINEBOX`, used in visualization of collision trees. PiperOrigin-RevId: 591224734 Change-Id: I09ad8c4b711021ff4dd5548ba5381d25d5a3d7f8 --- doc/includes/references.h | 3 +- include/mujoco/mjmodel.h | 3 +- introspect/enums.py | 9 +-- introspect/enums_test.py | 2 +- python/mujoco/bindings_test.py | 2 +- src/engine/engine_vis_visualize.c | 96 ++++++++++------------------ src/render/render_gl3.c | 35 ++++++++++ unity/Runtime/Bindings/MjBindings.cs | 9 +-- 8 files changed, 84 insertions(+), 75 deletions(-) diff --git a/doc/includes/references.h b/doc/includes/references.h index c89a4363..7224e3a3 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -459,10 +459,11 @@ typedef enum mjtGeom_ { // type of geometric shape mjGEOM_ARROW1, // arrow without wedges mjGEOM_ARROW2, // arrow in both directions mjGEOM_LINE, // line + mjGEOM_LINEBOX, // box with line edges mjGEOM_FLEX, // flex mjGEOM_SKIN, // skin mjGEOM_LABEL, // text label - mjGEOM_TRIANGLE, // triangle connecting a frame + mjGEOM_TRIANGLE, // triangle mjGEOM_NONE = 1001 // missing geom type } mjtGeom; diff --git a/include/mujoco/mjmodel.h b/include/mujoco/mjmodel.h index 49312dd1..d3417683 100644 --- a/include/mujoco/mjmodel.h +++ b/include/mujoco/mjmodel.h @@ -109,10 +109,11 @@ typedef enum mjtGeom_ { // type of geometric shape mjGEOM_ARROW1, // arrow without wedges mjGEOM_ARROW2, // arrow in both directions mjGEOM_LINE, // line + mjGEOM_LINEBOX, // box with line edges mjGEOM_FLEX, // flex mjGEOM_SKIN, // skin mjGEOM_LABEL, // text label - mjGEOM_TRIANGLE, // triangle connecting a frame + mjGEOM_TRIANGLE, // triangle mjGEOM_NONE = 1001 // missing geom type } mjtGeom; diff --git a/introspect/enums.py b/introspect/enums.py index 4982cf36..8502dbdb 100644 --- a/introspect/enums.py +++ b/introspect/enums.py @@ -90,10 +90,11 @@ ENUMS: Mapping[str, EnumDecl] = dict([ ('mjGEOM_ARROW1', 101), ('mjGEOM_ARROW2', 102), ('mjGEOM_LINE', 103), - ('mjGEOM_FLEX', 104), - ('mjGEOM_SKIN', 105), - ('mjGEOM_LABEL', 106), - ('mjGEOM_TRIANGLE', 107), + ('mjGEOM_LINEBOX', 104), + ('mjGEOM_FLEX', 105), + ('mjGEOM_SKIN', 106), + ('mjGEOM_LABEL', 107), + ('mjGEOM_TRIANGLE', 108), ('mjGEOM_NONE', 1001), ]), )), diff --git a/introspect/enums_test.py b/introspect/enums_test.py index b4144420..3580d4c1 100644 --- a/introspect/enums_test.py +++ b/introspect/enums_test.py @@ -61,7 +61,7 @@ class EnumsTest(absltest.TestCase): self.assertEqual(enum_decl.values['mjGEOM_ARROW'], 100) self.assertEqual(enum_decl.values['mjGEOM_ARROW1'], 101) self.assertEqual(enum_decl.values['mjGEOM_ARROW2'], 102) - self.assertEqual(enum_decl.values['mjGEOM_TRIANGLE'], 107) + self.assertEqual(enum_decl.values['mjGEOM_TRIANGLE'], 108) # Skip a few... self.assertEqual(enum_decl.values['mjGEOM_NONE'], 1001) diff --git a/python/mujoco/bindings_test.py b/python/mujoco/bindings_test.py index 1222569c..3d2e0ac6 100644 --- a/python/mujoco/bindings_test.py +++ b/python/mujoco/bindings_test.py @@ -840,7 +840,7 @@ Euler integrator, semi-implicit in velocity. self.assertEqual(mujoco.mjtGeom.mjGEOM_ARROW, 100) self.assertEqual(mujoco.mjtGeom.mjGEOM_ARROW1, 101) self.assertEqual(mujoco.mjtGeom.mjGEOM_ARROW2, 102) - self.assertEqual(mujoco.mjtGeom.mjGEOM_TRIANGLE, 107) + self.assertEqual(mujoco.mjtGeom.mjGEOM_TRIANGLE, 108) self.assertEqual(mujoco.mjtGeom.mjGEOM_NONE, 1001) def test_enum_from_int(self): diff --git a/src/engine/engine_vis_visualize.c b/src/engine/engine_vis_visualize.c index 393bd509..d2e2de20 100644 --- a/src/engine/engine_vis_visualize.c +++ b/src/engine/engine_vis_visualize.c @@ -509,56 +509,6 @@ static int bodycategory(const mjModel* m, int bodyid) { -// draw bounding box -static void drawBoundingBox(mjData* d, mjvScene* scn, - const mjtNum aabb[6], const mjtNum xpos[3], - const mjtNum xmat[9], const float rgba[4]) { - mjtNum x[3]; - mjtNum dist[3][3]; - mjvGeom* thisgeom; - int category = mjCAT_DECOR; - int objtype = mjOBJ_UNKNOWN; - int i = -1; - - if (xmat != NULL) { - mju_rotVecMat(x, aabb, xmat); - mju_addTo3(x, xpos); - for (int j=0; j < 3; j++) { - for (int k=0; k < 3; k++) { - dist[k][j] = aabb[k+3] * xmat[3*j+k]; - } - } - } else { - mju_copy3(x, aabb); - mju_addTo3(x, xpos); - for (int j=0; j < 3; j++) { - mju_zero3(dist[j]); - dist[j][j] = aabb[j+3]; - } - } - - int split[3] = {1, 2, 4}; - for (int v=0; v < 8; v++) { - mjtNum from[3] = {x[0], x[1], x[2]}; - for (int k=0; k < 3; k++) { - mju_addToScl3(from, dist[k], v&split[k] ? 1 : -1); - } - - mjtNum to[3]; - for (int k=0; k < 3; k++) { - mju_addScl3(to, from, dist[k], 2); - if (!(v&split[k])) { - START - mjv_connector(thisgeom, mjGEOM_LINE, 2, from, to); - f2f(thisgeom->rgba, rgba, 4); - FINISH - } - } - } -} - - - // computes the camera frustum static void getFrustum(float zver[2], float zhor[2], float znear, const float K[4], const float sensorsize[2]) { @@ -683,6 +633,8 @@ void mjv_addGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, } // body BVH + category = mjCAT_DECOR; + objtype = mjOBJ_UNKNOWN; if (vopt->flags[mjVIS_BODYBVH]) { float rgba[] = {1, 0, 0, 1}; for (int i = 0; i < m->nbvhstatic; i++) { @@ -708,20 +660,30 @@ void mjv_addGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, break; } - // compute transformation - mjtNum *aabb = isleaf ? m->geom_aabb + 6*geomid : m->bvh_aabb + 6*i; - + // get xpos, xmat, size const mjtNum* xpos = isleaf ? d->geom_xpos + 3 * geomid : d->xipos + 3 * bodyid; const mjtNum* xmat = isleaf ? d->geom_xmat + 9 * geomid : d->ximat + 9 * bodyid; + const mjtNum *size = isleaf ? m->geom_aabb + 6*geomid + 3 : m->bvh_aabb + 6*i + 3; + + // offset xpos with aabb center (not always at frame origin) + const mjtNum *center = isleaf ? m->geom_aabb + 6*geomid : m->bvh_aabb + 6*i; + mjtNum pos[3]; + mju_rotVecMat(pos, center, xmat); + mju_addTo3(pos, xpos); rgba[0] = d->bvh_active[i] ? 1 : 0; rgba[1] = d->bvh_active[i] ? 0 : 1; - drawBoundingBox(d, scn, aabb, xpos, xmat, rgba); + START + mjv_initGeom(thisgeom, mjGEOM_LINEBOX, size, pos, xmat, rgba); + FINISH + } } // flex BVH + category = mjCAT_DECOR; + objtype = mjOBJ_UNKNOWN; if (vopt->flags[mjVIS_FLEXBVH]) { float rgba[] = {1, 0, 0, 0.1}; for (int f=0; f < m->nflex; f++) { @@ -741,10 +703,8 @@ void mjv_addGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, rgba[0] = d->bvh_active[i] ? 1 : 0; rgba[1] = d->bvh_active[i] ? 0 : 1; - // b/304453879 : add LINEBOX geom for bounding box visualization - START - mjv_initGeom(thisgeom, mjGEOM_BOX, aabb+3, aabb, NULL, rgba); + mjv_initGeom(thisgeom, mjGEOM_LINEBOX, aabb+3, aabb, NULL, rgba); FINISH } } @@ -752,6 +712,8 @@ void mjv_addGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, } // mesh BVH + category = mjCAT_DECOR; + objtype = mjOBJ_UNKNOWN; if (vopt->flags[mjVIS_MESHBVH]) { float rgba[] = {1, 0, 0, 1}; for (int geomid = 0; geomid < m->ngeom; geomid++) { @@ -770,11 +732,6 @@ void mjv_addGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, } } - // compute transformation - const mjtNum *aabb = m->bvh_aabb + 6*i; - const mjtNum* xpos = d->geom_xpos + 3 * geomid; - const mjtNum* xmat = d->geom_xmat + 9 * geomid; - if (!d->bvh_active[i]) { continue; } @@ -782,7 +739,20 @@ void mjv_addGeoms(const mjModel* m, mjData* d, const mjvOption* vopt, rgba[0] = d->bvh_active[i] ? 1 : 0; rgba[1] = d->bvh_active[i] ? 0 : 1; - drawBoundingBox(d, scn, aabb, xpos, xmat, rgba); + // get xpos, xmat, size + const mjtNum* xpos = d->geom_xpos + 3 * geomid; + const mjtNum* xmat = d->geom_xmat + 9 * geomid; + const mjtNum *size = m->bvh_aabb + 6*i + 3; + + // offset xpos with aabb center (not always at geom origin) + const mjtNum *center = m->bvh_aabb + 6*i; + mjtNum pos[3]; + mju_rotVecMat(pos, center, xmat); + mju_addTo3(pos, xpos); + + START + mjv_initGeom(thisgeom, mjGEOM_LINEBOX, size, pos, xmat, rgba); + FINISH } } } diff --git a/src/render/render_gl3.c b/src/render/render_gl3.c index 4e108c0d..6535874a 100644 --- a/src/render/render_gl3.c +++ b/src/render/render_gl3.c @@ -404,6 +404,41 @@ static void renderGeom(const mjvGeom* geom, int mode, const float* headpos, } break; + case mjGEOM_LINEBOX: // box with line edges + glLineWidth(1.5*con->lineWidth); + lighting = glIsEnabled(GL_LIGHTING); + glDisable(GL_LIGHTING); + // bottom face + glBegin(GL_LINE_LOOP); + glVertex3f(-size[0], -size[1], -size[2]); + glVertex3f( size[0], -size[1], -size[2]); + glVertex3f( size[0], size[1], -size[2]); + glVertex3f(-size[0], size[1], -size[2]); + glEnd(); + // top face + glBegin(GL_LINE_LOOP); + glVertex3f(-size[0], -size[1], size[2]); + glVertex3f( size[0], -size[1], size[2]); + glVertex3f( size[0], size[1], size[2]); + glVertex3f(-size[0], size[1], size[2]); + glEnd(); + // vertical edges + glBegin(GL_LINES); + glVertex3f(-size[0], -size[1], -size[2]); + glVertex3f(-size[0], -size[1], size[2]); + glVertex3f( size[0], -size[1], -size[2]); + glVertex3f( size[0], -size[1], size[2]); + glVertex3f( size[0], size[1], -size[2]); + glVertex3f( size[0], size[1], size[2]); + glVertex3f(-size[0], size[1], -size[2]); + glVertex3f(-size[0], size[1], size[2]); + glEnd(); + glLineWidth(con->lineWidth); + if (lighting) { + glEnable(GL_LIGHTING); + } + break; + case mjGEOM_TRIANGLE: // triangle glBegin(GL_TRIANGLES); glVertex3f(0, 0, 0); diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index c4969e46..d773cf87 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -191,10 +191,11 @@ public enum mjtGeom : int{ mjGEOM_ARROW1 = 101, mjGEOM_ARROW2 = 102, mjGEOM_LINE = 103, - mjGEOM_FLEX = 104, - mjGEOM_SKIN = 105, - mjGEOM_LABEL = 106, - mjGEOM_TRIANGLE = 107, + mjGEOM_LINEBOX = 104, + mjGEOM_FLEX = 105, + mjGEOM_SKIN = 106, + mjGEOM_LABEL = 107, + mjGEOM_TRIANGLE = 108, mjGEOM_NONE = 1001, } public enum mjtCamLight : int{ From dc0d0c59d40bf768282a47bdc465c83c2b7fb5f1 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Fri, 15 Dec 2023 08:11:26 -0800 Subject: [PATCH 077/121] Fix bug in simulate where the "LOADING..." label was not properly showing. PiperOrigin-RevId: 591257979 Change-Id: Id09754b1e5a87decfb68b9fe305388aa8f0f5ed3 --- simulate/simulate.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simulate/simulate.cc b/simulate/simulate.cc index db0821d2..abe5a23b 100644 --- a/simulate/simulate.cc +++ b/simulate/simulate.cc @@ -2479,7 +2479,7 @@ void Simulate::Render() { char label[30] = {'\0'}; if (this->loadrequest) { std::snprintf(label, sizeof(label), "LOADING..."); - } if (this->scrub_index == 0) { + } else if (this->scrub_index == 0) { std::snprintf(label, sizeof(label), "PAUSE"); } else { std::snprintf(label, sizeof(label), "PAUSE (%d)", this->scrub_index); From 8ecb16abfa16beb3a92f1840bfb31c0bf7d11cae Mon Sep 17 00:00:00 2001 From: Nimrod Gileadi Date: Mon, 18 Dec 2023 03:33:05 -0800 Subject: [PATCH 078/121] Don't run particle.xml in testspeed_test under ASAN. Reduce the number of steps for a couple of other slow tests. PiperOrigin-RevId: 591844569 Change-Id: If722567b91163f1f3c451302f9543a43d05872a1 --- test/sample/testspeed_test.sh | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/test/sample/testspeed_test.sh b/test/sample/testspeed_test.sh index 942c2a98..f48342bc 100755 --- a/test/sample/testspeed_test.sh +++ b/test/sample/testspeed_test.sh @@ -26,8 +26,17 @@ test_model() { echo "Testing $model" >&2 local iterations=10 - if [[ "$model" == */composite/particle.xml && ${TESTSPEED_ASAN:-0} != 0 ]]; then - iterations=2 + # for particularly slow models, only run 2 steps under ASAN, or skip. + if [[ ${TESTSPEED_ASAN:-0} != 0 ]]; then + if [[ "$model" == */composite/particle.xml ]]; then + # this test can take several minutes under ASAN + return 0 + fi + if [[ "$model" == */benchmark/testdata/humanoid200.xml + || "$model" == */engine/testdata/collision_convex/stacked_boxes.xml + ]]; then + iterations=2 + fi fi # run testspeed, writing its output to stderr. From 7cb7c87f70a8dc6718283c3fec90f8b28497debf Mon Sep 17 00:00:00 2001 From: Nimrod Gileadi Date: Mon, 18 Dec 2023 07:32:20 -0800 Subject: [PATCH 079/121] Fix a few issues with the passive viewer. 1. Create an arena for the mjData instance used by the passive viewer visualization. When using the passive viewer, stuff gets copied from the real mjData into a minimal struct. That struct didn't have a stack, and now visualization for Flex does stack allocs. 2. Add missing a missing field in scene state for flex visualization. 3. Fix a memory leak where mjvScene wasn't released on exit in the passive viewer. 4. Add some locks in places where the render thread and Simulate::Sync collide. This fixes #1280. PiperOrigin-RevId: 591891676 Change-Id: I592f286cab9719c8d9af84e42f9756ea1f9f1971 --- doc/includes/references.h | 3 +++ include/mujoco/mjvisualize.h | 3 +++ include/mujoco/mjxmacro.h | 2 +- introspect/structs.py | 19 +++++++++++++++++++ simulate/simulate.cc | 9 ++++++--- src/engine/engine_vis_state.c | 16 ++++++++++++++++ test/engine/CMakeLists.txt | 7 ++++++- test/engine/engine_vis_state_test.cc | 8 ++++---- unity/Runtime/Bindings/MjBindings.cs | 3 +++ 9 files changed, 61 insertions(+), 9 deletions(-) diff --git a/doc/includes/references.h b/doc/includes/references.h index 7224e3a3..1c788150 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -2170,6 +2170,7 @@ struct mjvSceneState_ { int nnames; int npaths; int nsensordata; + int narena; mjOption opt; mjVisual vis; @@ -2383,6 +2384,7 @@ struct mjvSceneState_ { mjtNum* ten_length; mjtNum* wrap_xpos; + mjtNum* bvh_aabb_dyn; mjtByte* bvh_active; int* island_dofadr; int* island_dofind; @@ -2394,6 +2396,7 @@ struct mjvSceneState_ { mjContact* contact; mjtNum* efc_force; + void* arena; } data; }; typedef struct mjvSceneState_ mjvSceneState; diff --git a/include/mujoco/mjvisualize.h b/include/mujoco/mjvisualize.h index d693df9c..dc8a4bc6 100644 --- a/include/mujoco/mjvisualize.h +++ b/include/mujoco/mjvisualize.h @@ -436,6 +436,7 @@ struct mjvSceneState_ { int nnames; int npaths; int nsensordata; + int narena; mjOption opt; mjVisual vis; @@ -649,6 +650,7 @@ struct mjvSceneState_ { mjtNum* ten_length; mjtNum* wrap_xpos; + mjtNum* bvh_aabb_dyn; mjtByte* bvh_active; int* island_dofadr; int* island_dofind; @@ -660,6 +662,7 @@ struct mjvSceneState_ { mjContact* contact; mjtNum* efc_force; + void* arena; } data; }; typedef struct mjvSceneState_ mjvSceneState; diff --git a/include/mujoco/mjxmacro.h b/include/mujoco/mjxmacro.h index f4088314..86f61e0c 100644 --- a/include/mujoco/mjxmacro.h +++ b/include/mujoco/mjxmacro.h @@ -614,7 +614,7 @@ X ( mjtNum, qLD, nM, 1 ) \ X ( mjtNum, qLDiagInv, nv, 1 ) \ X ( mjtNum, qLDiagSqrtInv, nv, 1 ) \ - X ( mjtNum, bvh_aabb_dyn, nbvhdynamic, 6 ) \ + XMJV( mjtNum, bvh_aabb_dyn, nbvhdynamic, 6 ) \ XMJV( mjtByte, bvh_active, nbvh, 1 ) \ X ( mjtNum, flexedge_velocity, nflexedge, 1 ) \ X ( mjtNum, ten_velocity, ntendon, 1 ) \ diff --git a/introspect/structs.py b/introspect/structs.py index eea7f6cc..43e7cdc9 100644 --- a/introspect/structs.py +++ b/introspect/structs.py @@ -6334,6 +6334,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='int'), doc='', ), + StructFieldDecl( + name='narena', + type=ValueType(name='int'), + doc='', + ), StructFieldDecl( name='opt', type=ValueType(name='mjOption'), @@ -7596,6 +7601,13 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), doc='', ), + StructFieldDecl( + name='bvh_aabb_dyn', + type=PointerType( + inner_type=ValueType(name='mjtNum'), + ), + doc='', + ), StructFieldDecl( name='bvh_active', type=PointerType( @@ -7659,6 +7671,13 @@ STRUCTS: Mapping[str, StructDecl] = dict([ ), doc='', ), + StructFieldDecl( + name='arena', + type=PointerType( + inner_type=ValueType(name='void'), + ), + doc='', + ), ), ), doc='', diff --git a/simulate/simulate.cc b/simulate/simulate.cc index abe5a23b..226e290d 100644 --- a/simulate/simulate.cc +++ b/simulate/simulate.cc @@ -1817,6 +1817,9 @@ void Simulate::Sync() { if (!m_) { return; } + if (this->exitrequest.load()) { + return; + } bool update_profiler = this->profiler && (this->pause_update || this->run); bool update_sensor = this->sensor && (this->pause_update || this->run); @@ -2714,9 +2717,9 @@ void Simulate::RenderLoop() { } } - if (!is_passive_){ - mjv_freeScene(&this->scn); - } else { + const MutexLock lock(this->mtx); + mjv_freeScene(&this->scn); + if (is_passive_) { mjv_freeSceneState(&scnstate_); } diff --git a/src/engine/engine_vis_state.c b/src/engine/engine_vis_state.c index 5ef3c274..55d35497 100644 --- a/src/engine/engine_vis_state.c +++ b/src/engine/engine_vis_state.c @@ -86,6 +86,11 @@ void mjv_makeSceneState(const mjModel* m, const mjData* d, mjvSceneState* scnsta #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); @@ -118,6 +123,10 @@ void mjv_makeSceneState(const mjModel* m, const mjData* d, mjvSceneState* scnsta #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); @@ -177,6 +186,7 @@ void mjv_assignFromSceneState(const mjvSceneState* scnstate, mjModel* m, mjData* 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; @@ -194,10 +204,16 @@ void mjv_assignFromSceneState(const mjvSceneState* scnstate, mjModel* m, 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; diff --git a/test/engine/CMakeLists.txt b/test/engine/CMakeLists.txt index fb77205d..727cf955 100644 --- a/test/engine/CMakeLists.txt +++ b/test/engine/CMakeLists.txt @@ -99,5 +99,10 @@ target_link_libraries(engine_util_spatial_test fixture gmock) mujoco_test(engine_vfs_test) target_link_libraries(engine_vfs_test fixture gmock) -mujoco_test(engine_vis_state_test) +mujoco_test( + engine_vis_state_test + PROPERTIES + ENVIRONMENT + "MUJOCO_PLUGIN_DIR=$" +) target_link_libraries(engine_vis_state_test fixture gmock) diff --git a/test/engine/engine_vis_state_test.cc b/test/engine/engine_vis_state_test.cc index 48d48efd..6fefde47 100644 --- a/test/engine/engine_vis_state_test.cc +++ b/test/engine/engine_vis_state_test.cc @@ -35,17 +35,17 @@ 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 kModelPath = - "testdata/model.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}) { + {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()); + ASSERT_THAT(model, NotNull()) << "Failed to load model from " << path; mjData* data = mj_makeData(model); while (data->time < 2) { diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index d773cf87..213cba7a 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -6095,6 +6095,7 @@ public unsafe struct model { public int nnames; public int npaths; public int nsensordata; + public int narena; public mjOption_ opt; public mjVisual_ vis; public mjStatistic_ stat; @@ -6287,6 +6288,7 @@ public unsafe struct data { 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* island_dofind; @@ -6296,6 +6298,7 @@ public unsafe struct data { public double* flexvert_xpos; public mjContact_* contact; public double* efc_force; + public void* arena; } [StructLayout(LayoutKind.Sequential)] From d39ed1d3c8890f30037cc454a0f415f506cf907f Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Mon, 18 Dec 2023 10:04:25 -0800 Subject: [PATCH 080/121] Update changelog. PiperOrigin-RevId: 591932297 Change-Id: I9131400fee6839e9311baf28a069fc1e7237ee86 --- doc/changelog.rst | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index 5a90b5f6..016d028b 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -2,9 +2,23 @@ Changelog ========= -Version 3.1.0 (December 12, 2023) + +Upcoming version (not yet released) ----------------------------------- +Bug fixes +^^^^^^^^^ +- Fixed a bug (introduced in 3.1.0) where box-box collisions produced no contacts is one box was deeply embedded in the + other. +- Fixed a bug in :ref:`simulate` where the "LOADING..." message was not showing correctly. +- Fixed a crash in the Python :ref:`passive viewer`, when used with models containing Flex objects. +- Fixed a bug in MJX where ``site_xmat`` was ignored in ``get_data`` and ``put_data`` +- Fixed a bug in MJX where ``efc_address`` was sometimes incorrectly calculated in ``get_data``. + + +Version 3.1.0 (December 12, 2023) +--------------------------------- + General ^^^^^^^ 1. Improved convergence of Signed Distance Function (SDF) collisions by using line search and a new objective function @@ -39,7 +53,7 @@ Bug fixes ^^^^^^^^^ 8. Fix bug in Cartesian actuation with movable refsite, as when using body-centric Cartesian actuators on a quadruped. Before this fix such actuators could lead to non-conservation of momentum. -9. Fix bug that prevented using flex with the :ref:`passive viewer`. +9. Fix bug that prevented using flex with :ref:`simulate`. 10. Fix bug that prevented the use of elasticity plugins in combination with pinned flex vertices. 11. Release Python wheels targeting macOS 10.16 to support x86_64 systems where SYSTEM_VERSION_COMPAT is set. The minimum supported version is still 11.0, but we release these wheels to fix compatibility for those users. See From 3ed81f864c75957b507a9f9453d112f2cb9d243f Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Mon, 18 Dec 2023 11:34:33 -0800 Subject: [PATCH 081/121] Update version number in third_party/mujoco/doc/changelog.rst for v3.1.1 PiperOrigin-RevId: 591962782 Change-Id: I7c498e1339013e9207ef8c21e793c1d80f55d16d --- doc/changelog.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index 016d028b..c4970235 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -3,17 +3,17 @@ Changelog ========= -Upcoming version (not yet released) +Version 3.1.1 (December 18, 2023) ----------------------------------- Bug fixes ^^^^^^^^^ -- Fixed a bug (introduced in 3.1.0) where box-box collisions produced no contacts is one box was deeply embedded in the +1. Fixed a bug (introduced in 3.1.0) where box-box collisions produced no contacts is one box was deeply embedded in the other. -- Fixed a bug in :ref:`simulate` where the "LOADING..." message was not showing correctly. -- Fixed a crash in the Python :ref:`passive viewer`, when used with models containing Flex objects. -- Fixed a bug in MJX where ``site_xmat`` was ignored in ``get_data`` and ``put_data`` -- Fixed a bug in MJX where ``efc_address`` was sometimes incorrectly calculated in ``get_data``. +2. Fixed a bug in :ref:`simulate` where the "LOADING..." message was not showing correctly. +3. Fixed a crash in the Python :ref:`passive viewer`, when used with models containing Flex objects. +4. Fixed a bug in MJX where ``site_xmat`` was ignored in ``get_data`` and ``put_data`` +5. Fixed a bug in MJX where ``efc_address`` was sometimes incorrectly calculated in ``get_data``. Version 3.1.0 (December 12, 2023) From 1c31676e2d37d3d7808d1511318ce6afce3dc91b Mon Sep 17 00:00:00 2001 From: Baruch Tabanpour Date: Mon, 18 Dec 2023 14:56:12 -0800 Subject: [PATCH 082/121] Fix formatting error. PiperOrigin-RevId: 592018049 Change-Id: If0b59d18c380997c4c94795e275b56d2f6293c22 --- doc/changelog.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index c4970235..b70c7e9e 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -8,8 +8,7 @@ Version 3.1.1 (December 18, 2023) Bug fixes ^^^^^^^^^ -1. Fixed a bug (introduced in 3.1.0) where box-box collisions produced no contacts is one box was deeply embedded in the - other. +1. Fixed a bug (introduced in 3.1.0) where box-box collisions produced no contacts if one box was deeply embedded in the other. 2. Fixed a bug in :ref:`simulate` where the "LOADING..." message was not showing correctly. 3. Fixed a crash in the Python :ref:`passive viewer`, when used with models containing Flex objects. 4. Fixed a bug in MJX where ``site_xmat`` was ignored in ``get_data`` and ``put_data`` From 7fb448613fbcd205f742628c353b3f20223b3c88 Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Mon, 18 Dec 2023 15:02:35 -0800 Subject: [PATCH 083/121] Post release version update: Update to V3.1.2 PiperOrigin-RevId: 592019826 Change-Id: I64fd7da628bf3312ede4970b9b10ef5d0cc75b45 --- 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 40931b19..57b9ddd3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,7 +28,7 @@ set(MSVC_INCREMENTAL_DEFAULT ON) project( mujoco - VERSION 3.1.1 + VERSION 3.1.2 DESCRIPTION "MuJoCo Physics Simulator" HOMEPAGE_URL "https://mujoco.org" ) diff --git a/dist/mujoco.rc b/dist/mujoco.rc index 6a7fb828..2c2d58c6 100644 --- a/dist/mujoco.rc +++ b/dist/mujoco.rc @@ -1,6 +1,6 @@ 1 VERSIONINFO -FILEVERSION 3,1,1,0 -PRODUCTVERSION 3,1,1,0 +FILEVERSION 3,1,2,0 +PRODUCTVERSION 3,1,2,0 FILEOS 0x4 FILETYPE 0x1 { @@ -9,9 +9,9 @@ FILETYPE 0x1 BLOCK "040904b0" { VALUE "ProductName", "MuJoCo" - VALUE "ProductVersion", "3.1.1" + VALUE "ProductVersion", "3.1.2" VALUE "FileDescription", "MuJoCo" - VALUE "FileVersion", "3.1.1" + VALUE "FileVersion", "3.1.2" VALUE "InternalName", "mujoco.dll" VALUE "OriginalFilename", "mujoco.dll" VALUE "CompanyName", "Google DeepMind" diff --git a/dist/simulate.rc b/dist/simulate.rc index 14627b0b..255cb59c 100644 --- a/dist/simulate.rc +++ b/dist/simulate.rc @@ -1,8 +1,8 @@ MUJOCO ICON "mujoco.ico" 1 VERSIONINFO -FILEVERSION 3,1,1,0 -PRODUCTVERSION 3,1,1,0 +FILEVERSION 3,1,2,0 +PRODUCTVERSION 3,1,2,0 FILEOS 0x4 FILETYPE 0x1 { @@ -11,9 +11,9 @@ FILETYPE 0x1 BLOCK "040904b0" { VALUE "ProductName", "MuJoCo" - VALUE "ProductVersion", "3.1.1" + VALUE "ProductVersion", "3.1.2" VALUE "FileDescription", "MuJoCo" - VALUE "FileVersion", "3.1.1" + VALUE "FileVersion", "3.1.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 c43f3145..483383c4 100644 --- a/doc/APIreference/APIglobals.rst +++ b/doc/APIreference/APIglobals.rst @@ -522,7 +522,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`` - - 311 + - 312 - 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 0a608df9..4e443f40 100644 --- a/doc/unity.rst +++ b/doc/unity.rst @@ -30,14 +30,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.1.1.dylib`` (it can be +``/Applications/MuJoCo.app/Contents/Frameworks/mujoco.framework/Versions/Current/libmujoco.3.1.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.1.1/lib/libmujoco.so.3.1.1`` and rename it as ``libmujoco.so``. +``~/.mujoco/mujoco-3.1.2/lib/libmujoco.so.3.1.2`` and rename it as ``libmujoco.so``. Windows _______ diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index ea861353..5ab8a363 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -24,7 +24,7 @@ extern "C" { #endif // header version; should match the library version as returned by mj_version() -#define mjVERSION_HEADER 311 +#define mjVERSION_HEADER 312 // needed to define size_t, fabs and log10 #include diff --git a/mjx/pyproject.toml b/mjx/pyproject.toml index a558c2ed..b0672ead 100644 --- a/mjx/pyproject.toml +++ b/mjx/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name="mujoco-mjx" -version = "3.1.1" +version = "3.1.2" authors = [ {name = "Google DeepMind", email = "mujoco@deepmind.com"}, ] @@ -31,13 +31,13 @@ dependencies = [ "etils[epath]", "jax", "jaxlib", - "mujoco>=3.1.1.dev0", + "mujoco>=3.1.2.dev0", "scipy", "trimesh", ] [project.urls] Homepage = "https://github.com/google-deepmind/mujoco/tree/main/mjx" -Documentation = "https://mujoco.readthedocs.io/en/3.1.1" +Documentation = "https://mujoco.readthedocs.io/en/3.1.2" Repository = "https://github.com/google-deepmind/mujoco/tree/main/mjx" -Changelog = "https://mujoco.readthedocs.io/en/3.1.1/changelog.html" +Changelog = "https://mujoco.readthedocs.io/en/3.1.2/changelog.html" diff --git a/python/mujoco/CMakeLists.txt b/python/mujoco/CMakeLists.txt index e782497d..ed90bd7d 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.1.1.dylib + ${MUJOCO_FRAMEWORK}/mujoco.framework/Versions/A/libmujoco.3.1.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.1.1 HINTS ${MUJOCO_LIBRARY_DIR} REQUIRED + MUJOCO_LIBRARY mujoco mujoco.3.1.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 7b2dfa5e..e4cb8947 100644 --- a/python/mujoco/mjpython/Info.plist +++ b/python/mujoco/mjpython/Info.plist @@ -7,13 +7,13 @@ CFBundleIdentifier org.mujoco.mjpython CFBundleVersion - 3.1.1 + 3.1.2 CFBundleGetInfoString - 3.1.1 + 3.1.2 CFBundleLongVersionString - 3.1.1 + 3.1.2 CFBundleShortVersionString - 3.1.1 + 3.1.2 CFBundleExecutable mjpython CFBundleIconFile diff --git a/python/pyproject.toml b/python/pyproject.toml index ab298dc4..e8a31471 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mujoco" -version = "3.1.1" +version = "3.1.2" authors = [ {name = "Google DeepMind", email = "mujoco@deepmind.com"}, ] @@ -36,9 +36,9 @@ dynamic = ["readme", "scripts"] [project.urls] Homepage = "https://github.com/google-deepmind/mujoco" -Documentation = "https://mujoco.readthedocs.io/en/3.1.1" +Documentation = "https://mujoco.readthedocs.io/en/3.1.2" Repository = "https://github.com/google-deepmind/mujoco" -Changelog = "https://mujoco.readthedocs.io/en/3.1.1/changelog.html" +Changelog = "https://mujoco.readthedocs.io/en/3.1.2/changelog.html" [tool.setuptools] include-package-data = false diff --git a/sample/CMakeLists.txt b/sample/CMakeLists.txt index 0d2c2c7f..3df0c6ce 100644 --- a/sample/CMakeLists.txt +++ b/sample/CMakeLists.txt @@ -24,7 +24,7 @@ set(MSVC_INCREMENTAL_DEFAULT ON) project( mujoco_samples - VERSION 3.1.1 + VERSION 3.1.2 DESCRIPTION "MuJoCo samples binaries" HOMEPAGE_URL "https://mujoco.org" ) diff --git a/simulate/CMakeLists.txt b/simulate/CMakeLists.txt index fb416535..670aa6e2 100644 --- a/simulate/CMakeLists.txt +++ b/simulate/CMakeLists.txt @@ -29,7 +29,7 @@ set(MUJOCO_DEP_VERSION_lodepng project( mujoco_simulate - VERSION 3.1.1 + VERSION 3.1.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 819dc693..dfa91d0a 100644 --- a/src/engine/engine_support.c +++ b/src/engine/engine_support.c @@ -38,8 +38,8 @@ //-------------------------- Constants ------------------------------------------------------------- - #define mjVERSION 311 -#define mjVERSIONSTRING "3.1.1" + #define mjVERSION 312 +#define mjVERSIONSTRING "3.1.2" // names of disable flags const char* mjDISABLESTRING[mjNDISABLE] = { diff --git a/unity/Editor/Bindings/MujocoBinaryRetriever.cs b/unity/Editor/Bindings/MujocoBinaryRetriever.cs index 3e9b9bfc..2d6e9ea0 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.1.1.dylib", + "/mujoco.framework/Versions/Current/libmujoco.3.1.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.1.1/lib/libmujoco.so.3.1.1", + "/.mujoco/mujoco-3.1.2/lib/libmujoco.so.3.1.2", mujocoPath + "/libmujoco.so"); AssetDatabase.Refresh(); } diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index 213cba7a..fd2c33bf 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -108,7 +108,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 = 311; +public const int mjVERSION_HEADER = 312; // ------------------------------------Enums------------------------------------ diff --git a/unity/package.json b/unity/package.json index a6f629b9..cff13baf 100644 --- a/unity/package.json +++ b/unity/package.json @@ -1,7 +1,7 @@ { "name": "org.mujoco", "displayName": "MuJoCo", - "version": "3.1.1", + "version": "3.1.2", "description": "MuJoCo importer and runtime plug-in", "dependencies": {}, "author": { From 80f50c943c4fc500f8a7ea5bd8705836588260b4 Mon Sep 17 00:00:00 2001 From: Baruch Tabanpour Date: Mon, 18 Dec 2023 15:27:16 -0800 Subject: [PATCH 084/121] Add filterexact to MJX. PiperOrigin-RevId: 592026712 Change-Id: Ia73d67f36db5a4a1786a2532b5daa9e9a36f714c --- doc/changelog.rst | 8 ++++++ doc/mjx.rst | 8 +++--- mjx/mujoco/mjx/_src/forward.py | 41 +++++++++++++++++++++-------- mjx/mujoco/mjx/_src/forward_test.py | 40 ++++++++++++++++++++++++++++ mjx/mujoco/mjx/_src/test_util.py | 12 +++++++++ mjx/mujoco/mjx/_src/types.py | 10 +++---- 6 files changed, 99 insertions(+), 20 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index b70c7e9e..ad621fb4 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -2,6 +2,14 @@ Changelog ========= +Upcoming version (not yet released) +----------------------------------- + +MJX +^^^ + +1. Add :ref:`dyntype` ``filterexact``. + Version 3.1.1 (December 18, 2023) ----------------------------------- diff --git a/doc/mjx.rst b/doc/mjx.rst index 92a709b6..cc052182 100644 --- a/doc/mjx.rst +++ b/doc/mjx.rst @@ -183,7 +183,7 @@ The following features are **fully supported** in MJX: * - :ref:`Transmission ` - ``TRN_JOINT`` * - :ref:`Actuator Dynamics ` - - ``NONE``, ``INTEGRATOR``, ``FILTER`` + - ``NONE``, ``INTEGRATOR``, ``FILTER``, ``FILTEREXACT`` * - :ref:`Actuator Gain ` - ``FIXED``, ``AFFINE`` * - :ref:`Actuator Bias ` @@ -218,7 +218,7 @@ The following features are **in development** and coming soon: * - Dynamics - :ref:`Inverse ` * - :ref:`Transmission ` - - ``TRN_TENDON`` + - ``TRN_SITE``, ``TRN_TENDON`` * - :ref:`Actuator Dynamics ` - ``MUSCLE`` * - :ref:`Actuator Gain ` @@ -257,9 +257,9 @@ The following features are **unsupported**: * - Category - Feature * - :ref:`Transmission ` - - ``TRN_JOINTINPARENT``, ``TRN_SLIDERCRANK``, ``TRN_SITE``, ``TRN_BODY`` + - ``TRN_JOINTINPARENT``, ``TRN_SLIDERCRANK``, ``TRN_BODY`` * - :ref:`Actuator Dynamics ` - - ``FILTEREXACT``, ``USER`` + - ``USER`` * - :ref:`Actuator Gain ` - ``USER`` * - :ref:`Actuator Bias ` diff --git a/mjx/mujoco/mjx/_src/forward.py b/mjx/mujoco/mjx/_src/forward.py index 827c8f31..1ff268e7 100644 --- a/mjx/mujoco/mjx/_src/forward.py +++ b/mjx/mujoco/mjx/_src/forward.py @@ -107,7 +107,7 @@ def fwd_actuation(m: Model, d: Data) -> Data: act_dot = jp.array(0.0) elif dyn_typ == DynType.INTEGRATOR: act_dot = ctrl - elif dyn_typ == DynType.FILTER: + elif dyn_typ in (DynType.FILTER, DynType.FILTEREXACT): act_dot = (ctrl - act) / jp.clip(dyn_prm[0], mujoco.mjMINVAL) else: raise NotImplementedError(f'dyntype {dyn_typ.name} not implemented.') @@ -228,6 +228,34 @@ def _integrate_pos( return jp.concatenate(qs) if qs else jp.empty((0,)) +def _next_activation(m: Model, d: Data, act_dot: jax.Array) -> jax.Array: + """Returns the next act given the current act_dot, after clamping.""" + act = d.act + + if not m.na: + return act + + actrange = jp.where( + m.actuator_actlimited[:, None], + m.actuator_actrange, + jp.array([-jp.inf, jp.inf]), + ) + + def fn(dyntype, dynprm, act, act_dot, actrange): + if dyntype == DynType.FILTEREXACT: + tau = jp.clip(dynprm[0], a_min=mujoco.mjMINVAL) + act = act + act_dot * tau * (1 - jp.exp(-m.opt.timestep / tau)) + else: + act = act + act_dot * m.opt.timestep + act = jp.clip(act, actrange[0], actrange[1]) + return act + + args = (m.actuator_dyntype, m.actuator_dynprm, act, act_dot, actrange) + act = scan.flat(m, fn, 'uuaau', 'a', *args, group_by='u') + + return act.reshape(m.na) + + @named_scope def _advance( m: Model, @@ -237,16 +265,7 @@ def _advance( qvel: Optional[jax.Array] = None, ) -> Data: """Advance state and time given activation derivatives and acceleration.""" - act = d.act - if m.na: - act = d.act + act_dot * m.opt.timestep - actrange = jp.where( - m.actuator_actlimited[:, None], - m.actuator_actrange, - jp.array([-jp.inf, jp.inf]), - ) - fn = lambda act, actrange: jp.clip(act, actrange[0], actrange[1]) - act = scan.flat(m, fn, 'au', 'a', act, actrange, group_by='u') + act = _next_activation(m, d, act_dot) # advance velocities d = d.replace(qvel=d.qvel + qacc * m.opt.timestep) diff --git a/mjx/mujoco/mjx/_src/forward_test.py b/mjx/mujoco/mjx/_src/forward_test.py index fbe28c9d..cc767ae8 100644 --- a/mjx/mujoco/mjx/_src/forward_test.py +++ b/mjx/mujoco/mjx/_src/forward_test.py @@ -134,5 +134,45 @@ class ForwardTest(absltest.TestCase): np.testing.assert_allclose(dx.qvel, 1 + m.opt.timestep) +class ActuatorTest(absltest.TestCase): + _DYN_XML = """ + + + + + + + + + + + + + + + + + + + """ + + def test_dyntype(self): + m = mujoco.MjModel.from_xml_string(self._DYN_XML) + d = mujoco.MjData(m) + d.ctrl = np.array([1.5, 1.5, 1.5, 1.5]) + d.act = np.array([0.5, 0.5, 0.5]) + + mx = mjx.put_model(m) + dx = mjx.put_data(m, d) + + mujoco.mj_fwdActuation(m, d) + dx = jax.jit(mjx.fwd_actuation)(mx, dx) + _assert_attr_eq(d, dx, 'act_dot') + + mujoco.mj_Euler(m, d) + dx = jax.jit(mjx.euler)(mx, dx) + _assert_attr_eq(d, dx, 'act') + + if __name__ == '__main__': absltest.main() diff --git a/mjx/mujoco/mjx/_src/test_util.py b/mjx/mujoco/mjx/_src/test_util.py index 870621f7..85cdb5dc 100644 --- a/mjx/mujoco/mjx/_src/test_util.py +++ b/mjx/mujoco/mjx/_src/test_util.py @@ -29,6 +29,8 @@ TEST_FILES: List[str] = [ ] _ACTUATOR_TYPES = ['motor', 'velocity', 'position', 'general', 'intvelocity'] +_DYN_TYPES = ['none', 'integrator', 'filter', 'filterexact'] +_DYN_PRMS = ['0.189', '2.1'] _JOINT_TYPES = ['free', 'hinge', 'slide', 'ball'] _JOINT_AXES = ['1 0 0', '0 1 0', '0 0 1'] _FRICTIONS = ['1.2 0.003 0.0002', '0.2 0.0001 0.0005'] @@ -124,6 +126,8 @@ def _make_geom( def _make_actuator(actuator_type: str, joint: str) -> Dict[str, str]: """Returns attributes for an actuator.""" attr = {'joint': joint} + + # set actuator type if actuator_type == 'motor': attr['gear'] = np.random.choice(_GEARS) elif actuator_type == 'position': @@ -139,10 +143,18 @@ def _make_actuator(actuator_type: str, joint: str) -> Dict[str, str]: elif actuator_type == 'velocity': attr['kv'] = np.random.choice(_KV_VEL) + # set dyntype + if actuator_type == 'general': + attr['dyntype'] = np.random.choice(_DYN_TYPES) + if attr['dyntype'] != 'none': + attr['dynprm'] = np.random.choice(_DYN_PRMS) + + # ctrlrange if p(50) and actuator_type != 'intvelocity': lb, ub = -np.random.uniform(), np.random.uniform() attr['ctrlrange'] = f'{lb:.2f} {ub:.2f}' + # forcerange if p(50): lb, ub = -np.random.uniform(), np.random.uniform() attr['forcerange'] = f'{lb*10:.2f} {ub*10:.2f}' diff --git a/mjx/mujoco/mjx/_src/types.py b/mjx/mujoco/mjx/_src/types.py index 19b37630..9ccbe29a 100644 --- a/mjx/mujoco/mjx/_src/types.py +++ b/mjx/mujoco/mjx/_src/types.py @@ -20,10 +20,7 @@ from typing import Sequence import jax import jax.numpy as jp import mujoco -# pylint: disable=g-importing-member -from mujoco.mjx._src import dataclasses -from mujoco.mjx._src.dataclasses import PyTreeNode -# pylint: enable=g-importing-member +from mujoco.mjx._src.dataclasses import PyTreeNode # pylint: disable=g-importing-member import numpy as np @@ -167,11 +164,14 @@ class DynType(enum.IntEnum): Attributes: NONE: no internal dynamics; ctrl specifies force INTEGRATOR: integrator: da/dt = u + FILTER: linear filter: da/dt = (u-a) / tau + FILTEREXACT: linear filter: da/dt = (u-a) / tau, with exact integration """ NONE = mujoco.mjtDyn.mjDYN_NONE INTEGRATOR = mujoco.mjtDyn.mjDYN_INTEGRATOR FILTER = mujoco.mjtDyn.mjDYN_FILTER - # unsupported: FILTEREXACT, MUSCLE, USER + FILTEREXACT = mujoco.mjtDyn.mjDYN_FILTEREXACT + # unsupported: MUSCLE, USER class GainType(enum.IntEnum): From 77b4132c47fe652df27d014884b39b410301fb1c Mon Sep 17 00:00:00 2001 From: Baruch Tabanpour Date: Tue, 19 Dec 2023 16:18:01 -0800 Subject: [PATCH 085/121] Add site transmission to MJX. PiperOrigin-RevId: 592373042 Change-Id: I54d941f4ee6f74404fe2aea7253f5b426d097e23 --- doc/changelog.rst | 1 + doc/mjx.rst | 4 +-- mjx/mujoco/mjx/_src/device_test.py | 6 ---- mjx/mujoco/mjx/_src/io.py | 4 +++ mjx/mujoco/mjx/_src/io_test.py | 41 +++++++++++++++++----- mjx/mujoco/mjx/_src/scan.py | 40 +++++++++++++++++----- mjx/mujoco/mjx/_src/smooth.py | 55 ++++++++++++++++++++---------- mjx/mujoco/mjx/_src/smooth_test.py | 35 +++++++++++++++++++ mjx/mujoco/mjx/_src/test_util.py | 33 +++++++++++++----- mjx/mujoco/mjx/_src/types.py | 4 ++- 10 files changed, 171 insertions(+), 52 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index ad621fb4..52592511 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -9,6 +9,7 @@ MJX ^^^ 1. Add :ref:`dyntype` ``filterexact``. +2. Add :at:`site` transmission. Version 3.1.1 (December 18, 2023) diff --git a/doc/mjx.rst b/doc/mjx.rst index cc052182..c9263fa9 100644 --- a/doc/mjx.rst +++ b/doc/mjx.rst @@ -181,7 +181,7 @@ The following features are **fully supported** in MJX: * - :ref:`Joint ` - ``FREE``, ``BALL``, ``SLIDE``, ``HINGE`` * - :ref:`Transmission ` - - ``TRN_JOINT`` + - ``TRN_JOINT``, ``TRN_SITE`` * - :ref:`Actuator Dynamics ` - ``NONE``, ``INTEGRATOR``, ``FILTER``, ``FILTEREXACT`` * - :ref:`Actuator Gain ` @@ -218,7 +218,7 @@ The following features are **in development** and coming soon: * - Dynamics - :ref:`Inverse ` * - :ref:`Transmission ` - - ``TRN_SITE``, ``TRN_TENDON`` + - ``TRN_TENDON`` * - :ref:`Actuator Dynamics ` - ``MUSCLE`` * - :ref:`Actuator Gain ` diff --git a/mjx/mujoco/mjx/_src/device_test.py b/mjx/mujoco/mjx/_src/device_test.py index 9f518d91..b4b9a0cd 100644 --- a/mjx/mujoco/mjx/_src/device_test.py +++ b/mjx/mujoco/mjx/_src/device_test.py @@ -129,12 +129,6 @@ class ValidateInputTest(absltest.TestCase): with self.assertRaises(NotImplementedError): mjx.device_put(m) - def test_trn(self): - m = test_util.load_test_file('pendula.xml') - m.actuator_trntype[0] = mujoco.mjtTrn.mjTRN_SITE - with self.assertRaises(NotImplementedError): - mjx.device_put(m) - def test_dyn(self): m = test_util.load_test_file('pendula.xml') m.actuator_dyntype[0] = mujoco.mjtDyn.mjDYN_MUSCLE diff --git a/mjx/mujoco/mjx/_src/io.py b/mjx/mujoco/mjx/_src/io.py index 9396d7f3..ab255cfa 100644 --- a/mjx/mujoco/mjx/_src/io.py +++ b/mjx/mujoco/mjx/_src/io.py @@ -103,6 +103,10 @@ def put_model(m: mujoco.MjModel, device=None) -> types.Model: f'{[mj_type(m) for m in missing]} not supported' ) + # TODO: implement reference sites. + if any(m.actuator_trnid[:, 1] != -1): + raise NotImplementedError('refsite is not supported') + opt = _put_option(m.opt, device=device) stat = _put_statistic(m.stat, device=device) diff --git a/mjx/mujoco/mjx/_src/io_test.py b/mjx/mujoco/mjx/_src/io_test.py index a16a8780..08dec57a 100644 --- a/mjx/mujoco/mjx/_src/io_test.py +++ b/mjx/mujoco/mjx/_src/io_test.py @@ -82,7 +82,8 @@ _MULTIPLE_CONSTRAINTS = """ """ -class IoTest(parameterized.TestCase): +class ModelIOTest(parameterized.TestCase): + """IO tests for mjx.Model.""" def test_put_model(self): m = mujoco.MjModel.from_xml_string(_MULTIPLE_CONVEX_OBJECTS) @@ -133,7 +134,7 @@ class IoTest(parameterized.TestCase): ) self.assertTrue(m.opt.has_fluid_params) - def test_put_model_implicit_not_implemented(self): + def test_implicit_not_implemented(self): """Test that MJX guards against models with unimplemented features.""" with self.assertRaises(NotImplementedError): @@ -143,7 +144,7 @@ class IoTest(parameterized.TestCase): ) ) - def test_put_model_cone_not_implemented(self): + def test_cone_not_implemented(self): with self.assertRaises(NotImplementedError): mjx.put_model( mujoco.MjModel.from_xml_string( @@ -151,7 +152,7 @@ class IoTest(parameterized.TestCase): ) ) - def test_put_model_pgs_not_implemented(self): + def test_pgs_not_implemented(self): with self.assertRaises(NotImplementedError): mjx.put_model( mujoco.MjModel.from_xml_string( @@ -159,7 +160,7 @@ class IoTest(parameterized.TestCase): ) ) - def test_put_model_site_actuator_not_implemented(self): + def test_site_actuator_not_implemented(self): with self.assertRaises(NotImplementedError): mjx.put_model(mujoco.MjModel.from_xml_string(""" @@ -174,7 +175,7 @@ class IoTest(parameterized.TestCase): """)) - def test_put_model_tendon_not_implemented(self): + def test_tendon_not_implemented(self): with self.assertRaises(NotImplementedError): mjx.put_model(mujoco.MjModel.from_xml_string(""" @@ -191,7 +192,7 @@ class IoTest(parameterized.TestCase): """)) - def test_put_model_condim_not_implemented(self): + def test_condim_not_implemented(self): with self.assertRaises(NotImplementedError): mjx.put_model(mujoco.MjModel.from_xml_string(""" @@ -207,7 +208,7 @@ class IoTest(parameterized.TestCase): """)) - def test_put_model_cylinder_not_implemented(self): + def test_cylinder_not_implemented(self): with self.assertRaises(NotImplementedError): mjx.put_model(mujoco.MjModel.from_xml_string(""" @@ -223,6 +224,29 @@ class IoTest(parameterized.TestCase): """)) + def test_refsite_not_implemented(self): + """Tests that site transmissions with refsites are not implemented.""" + with self.assertRaises(NotImplementedError): + mjx.put_model(mujoco.MjModel.from_xml_string(""" + + + + + + + + + + + + + + """)) + + +class DataIOTest(parameterized.TestCase): + """IO tests for mjx.Data.""" + def test_make_data(self): """Test that make_data returns the correct shapes.""" @@ -407,5 +431,6 @@ class IoTest(parameterized.TestCase): self.assertEqual(ds[0].ncon, 1) self.assertEqual(ds[1].ncon, 0) + if __name__ == '__main__': absltest.main() diff --git a/mjx/mujoco/mjx/_src/scan.py b/mjx/mujoco/mjx/_src/scan.py index ae0eeb30..9035c98e 100644 --- a/mjx/mujoco/mjx/_src/scan.py +++ b/mjx/mujoco/mjx/_src/scan.py @@ -49,7 +49,9 @@ def _take(obj: Y, idx: np.ndarray) -> Y: def take(x): # TODO(erikfrey): if this helps perf, add support for striding too - if ( + if not x.shape[0]: + return x + elif ( len(idx.shape) == 1 and idx.size > 0 and (idx == np.arange(idx[0], idx[0] + idx.size)).all() @@ -113,8 +115,12 @@ def _nvmap(f: Callable[..., Y], *args) -> Y: if isinstance(arg, np.ndarray) and not np.all(arg == arg[0]): raise RuntimeError(f'numpy arg elements do not match: {arg}') + # split out numpy and jax args np_args = [a[0] if isinstance(a, np.ndarray) else None for a in args] args = [a if n is None else None for n, a in zip(np_args, args)] + + # remove empty args that we should not vmap over + args = jax.tree_map(lambda a: a if a.shape[0] else None, args) in_axes = [None if a is None else 0 for a in args] def outer_f(*args, np_args=np_args): @@ -126,7 +132,15 @@ 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.""" - size = {'b': m.nbody, 'j': m.njnt, 'q': m.nq, 'v': m.nv, 'u': m.nu, 'a': m.na} + size = { + 'b': m.nbody, + 'j': m.njnt, + 'q': m.nq, + 'v': m.nv, + 'u': m.nu, + 'a': m.na, + 's': m.nsite, + } for idx, (arg, typ) in enumerate(zip(args, in_types)): if len(arg) != size[typ]: raise IndexError( @@ -162,7 +176,7 @@ def flat( ) -> Y: r"""Scan a function across bodies or actuators. - Scan group data according to type and batch shape then calls vmap(f) on it. + Scan group data according to type and batch shape then calls vmap(f) on it.\ Args: m: an mjx model @@ -223,14 +237,22 @@ def flat( 'j': ( m.actuator_trnid[i, 0] if m.actuator_trntype[i] == TrnType.JOINT - else np.array(-1) + else -1 + ), + 's': ( + m.actuator_trnid[i, 0] + if m.actuator_trntype[i] == TrnType.SITE + else -1 ), } - # v/q associated with joint transmissions - typ_ids.update({ - 'v': np.nonzero(m.dof_jntid == typ_ids['j'])[0], - 'q': np.nonzero(_q_jointid(m) == typ_ids['j'])[0], - }) + v, q = np.array([-1]), np.array([-1]) + if m.actuator_trntype[i] == TrnType.JOINT: + # v/q are associated with the joint transmissions only + v = np.nonzero(m.dof_jntid == typ_ids['j'])[0] + q = np.nonzero(_q_jointid(m) == typ_ids['j'])[0] + + typ_ids.update({'v': v, 'q': q}) + return typ_ids # build up a grouping of type take-ids in body/actuator order diff --git a/mjx/mujoco/mjx/_src/smooth.py b/mjx/mujoco/mjx/_src/smooth.py index ba470bed..81c53213 100644 --- a/mjx/mujoco/mjx/_src/smooth.py +++ b/mjx/mujoco/mjx/_src/smooth.py @@ -19,11 +19,13 @@ from jax import numpy as jp import mujoco from mujoco.mjx._src import math 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 DisableBit 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 @@ -432,38 +434,55 @@ def rne(m: Model, d: Data) -> Data: def transmission(m: Model, d: Data) -> Data: """Computes actuator/transmission lengths and moments.""" + # TODO: consider combining transmission calculation into fwd_actuation. if not m.nu: return d - def fn(gear, jnt_typ, m_j, qpos): - # handles joint transmissions only - if jnt_typ == JointType.FREE: + def fn(trntype, trnid, gear, jnt_typ, m_j, qpos, site_xpos, site_xmat): + if trntype == TrnType.JOINT: + if jnt_typ == JointType.FREE: + length = jp.zeros(1) + moment = gear + m_j = m_j + jp.arange(6) + elif jnt_typ == JointType.BALL: + axis, angle = math.quat_to_axis_angle(qpos) + length = jp.dot(axis * angle, gear[:3])[None] + moment = gear[:3] + m_j = m_j + jp.arange(3) + elif jnt_typ in (JointType.SLIDE, JointType.HINGE): + length = qpos * gear[0] + moment = gear[:1] + m_j = m_j[None] + else: + raise RuntimeError(f'unrecognized joint type: {JointType(jnt_typ)}') + + moment = jp.zeros((m.nv,)).at[m_j].set(moment) + elif trntype == TrnType.SITE: length = jp.zeros(1) - moment = gear - m_j = m_j + jp.arange(6) - elif jnt_typ == JointType.BALL: - axis, angle = math.quat_to_axis_angle(qpos) - length = jp.dot(axis * angle, gear[:3])[None] - moment = gear[:3] - m_j = m_j + jp.arange(3) - elif jnt_typ in (JointType.SLIDE, JointType.HINGE): - length = qpos * gear[0] - moment = gear[:1] - m_j = m_j[None] + jacp, jacr = support.jac( + m, d, site_xpos, jp.array(m.site_bodyid)[trnid[0]] + ) + jac = jp.concatenate((jacp, jacr), axis=1) + wrench = jp.concatenate((site_xmat @ gear[:3], site_xmat @ gear[3:])) + moment = jac @ wrench else: - raise RuntimeError(f'unrecognized joint type: {jnt_typ}') - moment = jp.zeros((m.nv,)).at[m_j].set(moment) + raise RuntimeError(f'unrecognized trntype: {TrnType(trntype)}') + return length, moment length, moment = scan.flat( m, fn, - 'ujjq', - 'uuuu', + 'uuujjqss', + 'uu', + m.actuator_trntype, + jp.array(m.actuator_trnid), m.actuator_gear, m.jnt_type, jp.array(m.jnt_dofadr), d.qpos, + d.site_xpos, + d.site_xmat, group_by='u', ) length = length.reshape((m.nu,)) diff --git a/mjx/mujoco/mjx/_src/smooth_test.py b/mjx/mujoco/mjx/_src/smooth_test.py index 203cb654..e83779ad 100644 --- a/mjx/mujoco/mjx/_src/smooth_test.py +++ b/mjx/mujoco/mjx/_src/smooth_test.py @@ -132,5 +132,40 @@ class SmoothTest(absltest.TestCase): dx = jax.jit(mjx.rne)(mx, dx) np.testing.assert_allclose(dx.qfrc_bias, 0) + def test_site_transmission(self): + m = mujoco.MjModel.from_xml_string(""" + + + + + + + + + + + + + + + + + + + + + + """) + d = mujoco.MjData(m) + mujoco.mj_forward(m, d) + mx = mjx.put_model(m) + dx = mjx.put_data(m, d) + + mujoco.mj_transmission(m, d) + dx = mjx.transmission(mx, dx) + _assert_attr_eq(d, dx, 'actuator_length') + _assert_attr_eq(d, dx, 'actuator_moment') + + if __name__ == '__main__': absltest.main() diff --git a/mjx/mujoco/mjx/_src/test_util.py b/mjx/mujoco/mjx/_src/test_util.py index 85cdb5dc..66bab2f7 100644 --- a/mjx/mujoco/mjx/_src/test_util.py +++ b/mjx/mujoco/mjx/_src/test_util.py @@ -47,7 +47,7 @@ _SOLIMPS = [ _DIMS = ['3'] _MARGINS = ['0.0', '0.01', '0.02'] _GAPS = ['0.0', '0.005'] -_GEARS = ['20', '50', '100'] +_GEARS = ['2.1 0.0 3.3 0 2.3 0', '5.0 3.1 0 2.3 0.0 1.1'] def p(pct: int) -> bool: @@ -123,14 +123,21 @@ def _make_geom( return attr -def _make_actuator(actuator_type: str, joint: str) -> Dict[str, str]: +def _make_actuator( + actuator_type: str, joint: str | None = None, site: str | None = None +) -> Dict[str, str]: """Returns attributes for an actuator.""" - attr = {'joint': joint} + if joint: + attr = {'joint': joint} + elif site: + attr = {'site': site} + else: + raise ValueError('must provide a joint or site name') + + attr['gear'] = np.random.choice(_GEARS) # set actuator type - if actuator_type == 'motor': - attr['gear'] = np.random.choice(_GEARS) - elif actuator_type == 'position': + if actuator_type == 'position': attr['kp'] = np.random.choice(_KP_POS) elif actuator_type == 'general': attr['biastype'] = 'affine' @@ -245,6 +252,7 @@ def create_mjcf( pos = f'{body_pos[0]:.3f} {body_pos[1]:.3f} {body_pos[2] + z_pos:.3f}' n_bodies = len(list(mjcf.iter('body'))) child = ET.SubElement(body, 'body', {'pos': pos, 'name': f'body{n_bodies}'}) + ET.SubElement(child, 'site', {'name': f'site{n_bodies}'}) n_joints = len(list(mjcf.iter('joint'))) for nj in range(np.random.randint(1, max_stacked_joints + 1)): @@ -282,17 +290,28 @@ def create_mjcf( for _ in range(num_trees): make_tree(world, 0) + bodies = list(mjcf.iter('body')) + n_bodies = len(bodies) + # actuators if add_actuators: actuator = ET.SubElement(mjcf, 'actuator') n_joints = len(list(mjcf.iter('joint'))) nu = np.random.randint(1, n_joints + 1) actuators = [] + + # joint transmission for i in range(nu): actuator_type = np.random.choice(_ACTUATOR_TYPES) attr = _make_actuator(actuator_type, joint=f'joint{i}') actuators.append((actuator_type, attr)) + # site transmission + for i in range(np.random.randint(0, n_bodies)): + actuator_type = np.random.choice(_ACTUATOR_TYPES) + attr = _make_actuator(actuator_type, site=f'site{i}') + actuators.append((actuator_type, attr)) + np.random.shuffle(actuators) for typ, attr in actuators: ET.SubElement(actuator, typ, attr) @@ -320,9 +339,7 @@ def create_mjcf( ET.SubElement(contact, 'pair', attr) # exclude contacts - bodies = list(mjcf.iter('body')) body_names = [b.get('name') for b in bodies] - n_bodies = len(bodies) for _ in range(min(max_contact_excludes, (n_bodies * (n_bodies - 1) // 2))): if p(50): continue diff --git a/mjx/mujoco/mjx/_src/types.py b/mjx/mujoco/mjx/_src/types.py index 9ccbe29a..ece81623 100644 --- a/mjx/mujoco/mjx/_src/types.py +++ b/mjx/mujoco/mjx/_src/types.py @@ -153,9 +153,11 @@ class TrnType(enum.IntEnum): Attributes: JOINT: force on joint + SITE: force on site """ JOINT = mujoco.mjtTrn.mjTRN_JOINT - # unsupported: JOINTINPARENT, SLIDERCRANK, TENDON, SITE, BODY + SITE = mujoco.mjtTrn.mjTRN_SITE + # unsupported: JOINTINPARENT, SLIDERCRANK, TENDON, BODY class DynType(enum.IntEnum): From 2a46ca37874181bbae6ac148ba9797a53101d0b9 Mon Sep 17 00:00:00 2001 From: Nimrod Gileadi Date: Wed, 20 Dec 2023 05:01:55 -0800 Subject: [PATCH 086/121] Update the documentation on the maximum number of contacts from hfield collisions. The docs were incorrectly asserting that the limit is 9 contacts, which hasn't been true since version 1.5. This fixes #1285. PiperOrigin-RevId: 592526249 Change-Id: Id45d73666fec8dcfd18d11605975e787f2cfeba2 --- doc/XMLreference.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 6ca8c13b..8fb1edd3 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -1378,9 +1378,9 @@ also known as terrain map, is a 2D matrix of elevation data. The data can be spe | For collision detection, a height field is treated as a union of triangular prisms. Collisions between height fields and other geoms (except for planes and other height fields which are not supported) are computed by first selecting the sub-grid of prisms that could collide with the geom based on its bounding box, and then using the general convex - collider. The number of possible contacts between a height field and a geom is limited to 9; any contacts beyond that - are discarded. To avoid penetration due to discarded contacts, the spatial features of the height field should be - large compared to the geoms it collides with. + collider. The number of possible contacts between a height field and a geom is limited to 50 + (:ref:`mjMAXCONPAIR `); any contacts beyond that are discarded. To avoid penetration due to discarded + contacts, the spatial features of the height field should be large compared to the geoms it collides with. .. _asset-hfield-name: From 4c066a26b5c939155c9ba0db45b4d9ec81bebcbd Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Sun, 24 Dec 2023 12:06:05 -0800 Subject: [PATCH 087/121] Change `mjVISSTRING` of connect and weld equality constraints from "Constraint" to "Equality". This is more consistent with the 'E' shortcut key and with `mjDISABLESTRING` ("constraint" means all constraints, "equality" means bilateral constraints). Also remove unused ampersands from `mjVISSTRING` names, these are a remnant from a very old version of the UI framework. PiperOrigin-RevId: 593469811 Change-Id: I0f6f73c9f5d2c61b2280f395f76434b0710ce4f3 --- python/mujoco/bindings_test.py | 2 +- simulate/simulate.cc | 12 ++++-------- src/engine/engine_vis_init.c | 34 +++++++++++++++++----------------- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/python/mujoco/bindings_test.py b/python/mujoco/bindings_test.py index 3d2e0ac6..407557f0 100644 --- a/python/mujoco/bindings_test.py +++ b/python/mujoco/bindings_test.py @@ -822,7 +822,7 @@ Euler integrator, semi-implicit in velocity. self.assertLen(mujoco.mjRNDSTRING, mujoco.mjtRndFlag.mjNRNDFLAG) self.assertEqual(mujoco.mjDISABLESTRING[11], 'Refsafe') self.assertEqual(mujoco.mjVISSTRING[mujoco.mjtVisFlag.mjVIS_INERTIA], - ('&Inertia', '0', 'I')) + ('Inertia', '0', 'I')) def test_enum_values(self): self.assertEqual(mujoco.mjtJoint.mjJNT_FREE, 0) diff --git a/simulate/simulate.cc b/simulate/simulate.cc index 226e290d..d9d4f99d 100644 --- a/simulate/simulate.cc +++ b/simulate/simulate.cc @@ -837,15 +837,8 @@ void MakeRenderingSection(mj::Simulate* sim, const mjModel* m, int oldstate) { {mjITEM_END} }; for (int i=0; iui0, defOpenGL); for (int i=0; i Date: Tue, 2 Jan 2024 08:37:44 -0800 Subject: [PATCH 088/121] Free `MjrContext` in Renderer destructor. Fixes #1186. PiperOrigin-RevId: 595125644 Change-Id: I5dadc3508ec296ba8844fed2a8f5d2de7e53221d --- python/mujoco/renderer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python/mujoco/renderer.py b/python/mujoco/renderer.py index dfb387c6..973e8076 100644 --- a/python/mujoco/renderer.py +++ b/python/mujoco/renderer.py @@ -315,6 +315,9 @@ the clause: if self._gl_context: self._gl_context.free() self._gl_context = None + if self._mjr_context: + self._mjr_context.free() + self._mjr_context = None def __enter__(self): return self From e07ea625acc9ca9f221ebc566e6990848929602a Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 2 Jan 2024 11:59:41 -0800 Subject: [PATCH 089/121] Add keyframes to humanoid model. PiperOrigin-RevId: 595175659 Change-Id: I2aae405a5dfbd9b14253d6330cb12003bd4f6c08 --- model/humanoid/humanoid.xml | 42 ++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/model/humanoid/humanoid.xml b/model/humanoid/humanoid.xml index 31cde3ed..013ebe23 100644 --- a/model/humanoid/humanoid.xml +++ b/model/humanoid/humanoid.xml @@ -233,17 +233,35 @@ left leg arms --> - - + + + + From 8ce2c92021ed202db6bbb71bf4e30e4f81150b4c Mon Sep 17 00:00:00 2001 From: Baruch Tabanpour Date: Tue, 2 Jan 2024 16:44:59 -0800 Subject: [PATCH 090/121] Add refsite transmission. PiperOrigin-RevId: 595239860 Change-Id: Id1ca8fe2ffab4b55013e8987e92eb99847278dc4 --- mjx/mujoco/mjx/_src/dataclasses.py | 4 +- mjx/mujoco/mjx/_src/io.py | 5 -- mjx/mujoco/mjx/_src/io_test.py | 35 --------- mjx/mujoco/mjx/_src/scan.py | 5 +- mjx/mujoco/mjx/_src/smooth.py | 73 +++++++++++++++++-- mjx/mujoco/mjx/_src/smooth_test.py | 17 +++-- mjx/mujoco/mjx/_src/test_util.py | 18 ++++- mjx/mujoco/mjx/_src/types.py | 2 - .../mjx/integration_test/smooth_test.py | 6 +- 9 files changed, 103 insertions(+), 62 deletions(-) diff --git a/mjx/mujoco/mjx/_src/dataclasses.py b/mjx/mujoco/mjx/_src/dataclasses.py index b939ab9e..5d95eb69 100644 --- a/mjx/mujoco/mjx/_src/dataclasses.py +++ b/mjx/mujoco/mjx/_src/dataclasses.py @@ -62,7 +62,7 @@ def dataclass(clz: _T) -> _T: def to_meta(field, obj): val = getattr(obj, field.name) - return to_tup(val) if isinstance(val, np.ndarray) else val + return (to_tup(val), val.dtype) if isinstance(val, np.ndarray) else val def to_data(field, obj): return (jax.tree_util.GetAttrKey(field.name), getattr(obj, field.name)) @@ -75,7 +75,7 @@ def dataclass(clz: _T) -> _T: def from_meta(field, meta): if field.type is np.ndarray: - return (field.name, np.array(meta)) + return (field.name, np.array(meta[0], dtype=meta[1])) else: return (field.name, meta) diff --git a/mjx/mujoco/mjx/_src/io.py b/mjx/mujoco/mjx/_src/io.py index ab255cfa..788c3899 100644 --- a/mjx/mujoco/mjx/_src/io.py +++ b/mjx/mujoco/mjx/_src/io.py @@ -103,10 +103,6 @@ def put_model(m: mujoco.MjModel, device=None) -> types.Model: f'{[mj_type(m) for m in missing]} not supported' ) - # TODO: implement reference sites. - if any(m.actuator_trnid[:, 1] != -1): - raise NotImplementedError('refsite is not supported') - opt = _put_option(m.opt, device=device) stat = _put_statistic(m.stat, device=device) @@ -196,7 +192,6 @@ def make_data(m: Union[types.Model, mujoco.MjModel]) -> types.Data: qfrc_bias=zero_nv, qfrc_passive=zero_nv, efc_aref=zero_nefc, - actuator_force=zero_nu, qfrc_actuator=zero_nv, qfrc_smooth=zero_nv, qacc_smooth=zero_nv, diff --git a/mjx/mujoco/mjx/_src/io_test.py b/mjx/mujoco/mjx/_src/io_test.py index 08dec57a..8b25f359 100644 --- a/mjx/mujoco/mjx/_src/io_test.py +++ b/mjx/mujoco/mjx/_src/io_test.py @@ -160,21 +160,6 @@ class ModelIOTest(parameterized.TestCase): ) ) - def test_site_actuator_not_implemented(self): - with self.assertRaises(NotImplementedError): - mjx.put_model(mujoco.MjModel.from_xml_string(""" - - - - - - - - - - - """)) - def test_tendon_not_implemented(self): with self.assertRaises(NotImplementedError): mjx.put_model(mujoco.MjModel.from_xml_string(""" @@ -224,25 +209,6 @@ class ModelIOTest(parameterized.TestCase): """)) - def test_refsite_not_implemented(self): - """Tests that site transmissions with refsites are not implemented.""" - with self.assertRaises(NotImplementedError): - mjx.put_model(mujoco.MjModel.from_xml_string(""" - - - - - - - - - - - - - - """)) - class DataIOTest(parameterized.TestCase): """IO tests for mjx.Data.""" @@ -305,7 +271,6 @@ class DataIOTest(parameterized.TestCase): self.assertEqual(d.qfrc_bias.shape, (nv,)) self.assertEqual(d.qfrc_passive.shape, (nv,)) self.assertEqual(d.efc_aref.shape, (nefc,)) - self.assertEqual(d.actuator_force.shape, (1,)) self.assertEqual(d.qfrc_actuator.shape, (nv,)) self.assertEqual(d.qfrc_smooth.shape, (nv,)) self.assertEqual(d.qacc_smooth.shape, (nv,)) diff --git a/mjx/mujoco/mjx/_src/scan.py b/mjx/mujoco/mjx/_src/scan.py index 9035c98e..89f360cb 100644 --- a/mjx/mujoco/mjx/_src/scan.py +++ b/mjx/mujoco/mjx/_src/scan.py @@ -220,6 +220,7 @@ def flat( m.actuator_dyntype[ids_u], m.actuator_trntype[ids_u], m.jnt_type[ids_j], + m.actuator_trnid[ids_u, 1] == -1, # key by refsite being present ) def type_ids_j(m, i): @@ -240,9 +241,9 @@ def flat( else -1 ), 's': ( - m.actuator_trnid[i, 0] + m.actuator_trnid[i] if m.actuator_trntype[i] == TrnType.SITE - else -1 + else np.array([-1, -1]) ), } v, q = np.array([-1]), np.array([-1]) diff --git a/mjx/mujoco/mjx/_src/smooth.py b/mjx/mujoco/mjx/_src/smooth.py index 81c53213..c1531192 100644 --- a/mjx/mujoco/mjx/_src/smooth.py +++ b/mjx/mujoco/mjx/_src/smooth.py @@ -27,6 +27,7 @@ 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 def kinematics(m: Model, d: Data) -> Data: @@ -432,13 +433,54 @@ def rne(m: Model, d: Data) -> Data: return d +def _site_dof_mask(m: Model) -> np.ndarray: + """Creates a dof mask for site transmissions.""" + mask = np.ones((m.nu, m.nv)) + for i in np.nonzero(m.actuator_trnid[:, 1] != -1)[0]: + id_, refid = m.actuator_trnid[i] + # intialize last dof address for each body + b0 = m.body_weldid[m.site_bodyid[id_]] + b1 = m.body_weldid[m.site_bodyid[refid]] + dofadr0 = m.body_dofadr[b0] + m.body_dofnum[b0] - 1 + dofadr1 = m.body_dofadr[b1] + m.body_dofnum[b1] - 1 + + # find common ancestral dof, if any + while dofadr0 != dofadr1: + if dofadr0 < dofadr1: + dofadr1 = m.dof_parentid[dofadr1] + else: + dofadr0 = m.dof_parentid[dofadr0] + if dofadr0 == -1 or dofadr1 == -1: + break + + # if common ancestral dof was found, clear the columns of its parental chain + da = dofadr0 if dofadr0 == dofadr1 else -1 + while da >= 0: + mask[i, da] = 0.0 + da = m.dof_parentid[da] + + return mask + + def transmission(m: Model, d: Data) -> Data: """Computes actuator/transmission lengths and moments.""" # TODO: consider combining transmission calculation into fwd_actuation. if not m.nu: return d - def fn(trntype, trnid, gear, jnt_typ, m_j, qpos, site_xpos, site_xmat): + def fn( + trntype, + trnid, + gear, + jnt_typ, + m_j, + qpos, + has_refsite, + site_dof_mask, + site_xpos, + site_xmat, + site_quat, + ): if trntype == TrnType.JOINT: if jnt_typ == JointType.FREE: length = jp.zeros(1) @@ -459,21 +501,34 @@ def transmission(m: Model, d: Data) -> Data: moment = jp.zeros((m.nv,)).at[m_j].set(moment) elif trntype == TrnType.SITE: length = jp.zeros(1) - jacp, jacr = support.jac( - m, d, site_xpos, jp.array(m.site_bodyid)[trnid[0]] - ) - jac = jp.concatenate((jacp, jacr), axis=1) - wrench = jp.concatenate((site_xmat @ gear[:3], site_xmat @ gear[3:])) + id_, refid = jp.array(m.site_bodyid)[trnid] + jacp, jacr = support.jac(m, d, site_xpos[0], id_) + frame_xmat = site_xmat[0] + if has_refsite: + vecp = site_xmat[1].T @ (site_xpos[0] - site_xpos[1]) + vecr = math.quat_sub(site_quat[0], site_quat[1]) + length += jp.dot(jp.concatenate([vecp, vecr]), gear) + jacrefp, jacrefr = support.jac(m, d, site_xpos[1], refid) + jacp, jacr = jacp - jacrefp, jacr - jacrefr + frame_xmat = site_xmat[1] + + jac = jp.concatenate((jacp, jacr), axis=1) * site_dof_mask[:, None] + wrench = jp.concatenate((frame_xmat @ gear[:3], frame_xmat @ gear[3:])) moment = jac @ wrench else: raise RuntimeError(f'unrecognized trntype: {TrnType(trntype)}') return length, moment + # pre-compute values for site transmissions + has_refsite = m.actuator_trnid[:, 1] != -1 + site_dof_mask = _site_dof_mask(m) + site_quat = jax.vmap(math.quat_mul)(m.site_quat, d.xquat[m.site_bodyid]) + length, moment = scan.flat( m, fn, - 'uuujjqss', + 'uuujjquusss', 'uu', m.actuator_trntype, jp.array(m.actuator_trnid), @@ -481,11 +536,15 @@ def transmission(m: Model, d: Data) -> Data: m.jnt_type, jp.array(m.jnt_dofadr), d.qpos, + has_refsite, + jp.array(site_dof_mask), d.site_xpos, d.site_xmat, + site_quat, group_by='u', ) length = length.reshape((m.nu,)) moment = moment.reshape((m.nu, m.nv)) + d = d.replace(actuator_length=length, actuator_moment=moment) return d diff --git a/mjx/mujoco/mjx/_src/smooth_test.py b/mjx/mujoco/mjx/_src/smooth_test.py index e83779ad..b7d62eb9 100644 --- a/mjx/mujoco/mjx/_src/smooth_test.py +++ b/mjx/mujoco/mjx/_src/smooth_test.py @@ -141,7 +141,11 @@ class SmoothTest(absltest.TestCase): - + + + + + @@ -149,10 +153,11 @@ class SmoothTest(absltest.TestCase): - - - - + + + + + """) @@ -162,7 +167,7 @@ class SmoothTest(absltest.TestCase): dx = mjx.put_data(m, d) mujoco.mj_transmission(m, d) - dx = mjx.transmission(mx, dx) + dx = jax.jit(mjx.transmission)(mx, dx) _assert_attr_eq(d, dx, 'actuator_length') _assert_attr_eq(d, dx, 'actuator_moment') diff --git a/mjx/mujoco/mjx/_src/test_util.py b/mjx/mujoco/mjx/_src/test_util.py index 66bab2f7..ab27c2b1 100644 --- a/mjx/mujoco/mjx/_src/test_util.py +++ b/mjx/mujoco/mjx/_src/test_util.py @@ -36,7 +36,7 @@ _JOINT_AXES = ['1 0 0', '0 1 0', '0 0 1'] _FRICTIONS = ['1.2 0.003 0.0002', '0.2 0.0001 0.0005'] _KP_POS = ['1', '2'] _KP_INTVEL = ['10000', '2000'] -_KV_VEL = ['123', '1'] +_KV_VEL = ['12', '1', '0', '0.1'] _PAIR_FRICTIONS = ['1.2 0.9 0.003 0.0002 0.0001'] _SOLREFS = ['0.04 1.01', '0.05 1.02', '0.03 1.1', '0.015 1.0'] _SOLIMPS = [ @@ -124,7 +124,10 @@ def _make_geom( def _make_actuator( - actuator_type: str, joint: str | None = None, site: str | None = None + actuator_type: str, + joint: str | None = None, + site: str | None = None, + refsite: str | None = None, ) -> Dict[str, str]: """Returns attributes for an actuator.""" if joint: @@ -134,11 +137,15 @@ def _make_actuator( else: raise ValueError('must provide a joint or site name') + if refsite: + attr['refsite'] = refsite + attr['gear'] = np.random.choice(_GEARS) # set actuator type if actuator_type == 'position': attr['kp'] = np.random.choice(_KP_POS) + attr['kv'] = np.random.choice(_KV_VEL) elif actuator_type == 'general': attr['biastype'] = 'affine' attr['gainprm'] = '35 0 0' @@ -312,6 +319,13 @@ def create_mjcf( attr = _make_actuator(actuator_type, site=f'site{i}') actuators.append((actuator_type, attr)) + # site transmission with refsite + for i in range(np.random.randint(0, n_bodies)): + j = np.random.randint(0, n_bodies) + actuator_type = np.random.choice(_ACTUATOR_TYPES) + attr = _make_actuator(actuator_type, site=f'site{i}', refsite=f'site{j}') + actuators.append((actuator_type, attr)) + np.random.shuffle(actuators) for typ, attr in actuators: ET.SubElement(actuator, typ, attr) diff --git a/mjx/mujoco/mjx/_src/types.py b/mjx/mujoco/mjx/_src/types.py index ece81623..595d7f90 100644 --- a/mjx/mujoco/mjx/_src/types.py +++ b/mjx/mujoco/mjx/_src/types.py @@ -590,7 +590,6 @@ class Data(PyTreeNode): qfrc_bias: C(qpos,qvel) (nv,) qfrc_passive: passive force (nv,) efc_aref: reference pseudo-acceleration (nefc,) - actuator_force: actuator force in actuation space (nu,) qfrc_actuator: actuator force (nv,) qfrc_smooth: net unconstrained force (nv,) qacc_smooth: unconstrained acceleration (nv,) @@ -650,7 +649,6 @@ class Data(PyTreeNode): qfrc_passive: jax.Array efc_aref: jax.Array # position, velcoity, control & acceleration dependent: - actuator_force: jax.Array qfrc_actuator: jax.Array qfrc_smooth: jax.Array qacc_smooth: jax.Array diff --git a/mjx/mujoco/mjx/integration_test/smooth_test.py b/mjx/mujoco/mjx/integration_test/smooth_test.py index d032be79..c4d3bfda 100644 --- a/mjx/mujoco/mjx/integration_test/smooth_test.py +++ b/mjx/mujoco/mjx/integration_test/smooth_test.py @@ -57,7 +57,9 @@ class TransmissionIntegrationTest(parameterized.TestCase): d = mujoco.MjData(m) d.ctrl = np.random.normal(scale=10, size=m.nu) d.act = np.random.normal(scale=10, size=m.na) + d.qpos = np.random.normal(m.nq) d.qvel = np.random.random(m.nv) + mujoco.mj_forward(m, d) # put on device mx = mjx.put_model(m) @@ -67,7 +69,9 @@ class TransmissionIntegrationTest(parameterized.TestCase): dx = transmission_jit_fn(mx, dx) _assert_attr_eq(d, dx, 'actuator_length', seed, f'transmission{seed}') - _assert_attr_eq(d, dx, 'actuator_moment', seed, f'transmission{seed}') + _assert_attr_eq( + d, dx, 'actuator_moment', seed, f'transmission{seed}', atol=1e-4 + ) if __name__ == '__main__': From cd972bbeee9691ed19c31e535c76b59e2611c075 Mon Sep 17 00:00:00 2001 From: Kevin Zakka Date: Wed, 3 Jan 2024 04:46:13 -0800 Subject: [PATCH 091/121] Document humanoid model changes in changelog section of its README + minor cosmetic improvements. PiperOrigin-RevId: 595367344 Change-Id: I17483de71c6cbcf6a66e2fad762cc32f8cbee8b5 --- model/humanoid/README.md | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/model/humanoid/README.md b/model/humanoid/README.md index 28ab7e7a..6f926186 100644 --- a/model/humanoid/README.md +++ b/model/humanoid/README.md @@ -1,17 +1,35 @@ -Humanoid -======== - -Degrees of Freedom: 27 -Actuators: 21 +# Humanoid This simplified humanoid model, introduced in [1], is designed for bipedal locomotion behaviours. While several variants of it exist in the wild, this version is based on the model in the DeepMind Control Suite [2], which has fairly realistic actuator gains. +* Degrees of Freedom: 27 +* Actuators: 21 + +

+ +

+ +## Changelog + +* 02-01-2024: Add more keyframes. +* 27-11-2023: Move humanoid geoms to group 1. +* 05-04-2023: Fix typo in texture size. +* 20-09-2022: Use default class for left_upper_arm geom. +* 17-09-2022: Increase offscreen render buffer resolution of the humanoid to 2560x1440. +* 12-09-2022: + * Increased maximum hip flexion. + * Symmetrised shoulder and ankle joints. + * Added hamstring tendons which couple the hip and knee at large flexion values. + * Moved duplicated values into defaults. + * Added two keyframes. + * Improved lighting. + * Changed naming convention. + +## References + [1] [Synthesis and Stabilization of Complex Behaviors through Online Trajectory Optimization] (https://doi.org/10.1109/IROS.2012.6386025). [2] [DeepMind Control Suite](https://arxiv.org/abs/1801.00690). - - -![humanoid](humanoid.png) From 6860e953ed884fab19a8785cb5353a362f0d9277 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Wed, 3 Jan 2024 06:01:49 -0800 Subject: [PATCH 092/121] Improve docstring of `mju_eig3`. PiperOrigin-RevId: 595380013 Change-Id: Ia3663c11ff1cd50a35d5d72ba01e09ff3d9054d0 --- doc/APIreference/functions.rst | 2 +- include/mujoco/mujoco.h | 2 +- introspect/functions.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index 6bf64e11..04d8802a 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -2985,7 +2985,7 @@ mju_eig3 .. mujoco-include:: mju_eig3 -Eigenvalue decomposition of symmetric 3x3 matrix. +Eigenvalue decomposition of symmetric 3x3 matrix, mat = eigvec * diag(eigval) * eigvec'. .. _mju_boxQP: diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index 5ab8a363..cb6962a1 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -1118,7 +1118,7 @@ MJAPI void mju_bandMulMatVec(mjtNum* res, const mjtNum* mat, const mjtNum* vec, // Address of diagonal element i in band-dense matrix representation. MJAPI int mju_bandDiag(int i, int ntotal, int nband, int ndense); -// Eigenvalue decomposition of symmetric 3x3 matrix. +// Eigenvalue decomposition of symmetric 3x3 matrix, mat = eigvec * diag(eigval) * eigvec'. MJAPI int mju_eig3(mjtNum eigval[3], mjtNum eigvec[9], mjtNum quat[4], const mjtNum mat[9]); // minimize 0.5*x'*H*x + x'*g s.t. lower <= x <= upper, return rank or -1 if failed diff --git a/introspect/functions.py b/introspect/functions.py index e57bd7a9..0e9d2568 100644 --- a/introspect/functions.py +++ b/introspect/functions.py @@ -7368,7 +7368,7 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), ), ), - doc='Eigenvalue decomposition of symmetric 3x3 matrix.', + doc="Eigenvalue decomposition of symmetric 3x3 matrix, mat = eigvec * diag(eigval) * eigvec'.", # pylint: disable=line-too-long )), ('mju_boxQP', FunctionDecl( From 8d1b05bbd26da9670638ce215a3c47c6cde4161b Mon Sep 17 00:00:00 2001 From: Kevin Zakka Date: Wed, 3 Jan 2024 06:18:36 -0800 Subject: [PATCH 093/121] Fix markdown hyperlink. PiperOrigin-RevId: 595383191 Change-Id: Ia11b962c68ed33cc0fd6c5279f571f4b21d81570 --- model/humanoid/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/model/humanoid/README.md b/model/humanoid/README.md index 6f926186..de4c7069 100644 --- a/model/humanoid/README.md +++ b/model/humanoid/README.md @@ -29,7 +29,6 @@ in the DeepMind Control Suite [2], which has fairly realistic actuator gains. ## References -[1] [Synthesis and Stabilization of Complex Behaviors through Online Trajectory Optimization] - (https://doi.org/10.1109/IROS.2012.6386025). +[1] [Synthesis and Stabilization of Complex Behaviors through Online Trajectory Optimization](https://doi.org/10.1109/IROS.2012.6386025). [2] [DeepMind Control Suite](https://arxiv.org/abs/1801.00690). From 2940b934febdce7feb89f402faf13b92af2865e3 Mon Sep 17 00:00:00 2001 From: Kevin Zakka Date: Wed, 3 Jan 2024 08:08:48 -0800 Subject: [PATCH 094/121] Fix typo in sdf plugin readme. PiperOrigin-RevId: 595404942 Change-Id: I43eb079779538e4ff82d0f549b5a49819ebcd9cb --- plugin/sdf/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/sdf/README.md b/plugin/sdf/README.md index a51d84bb..11ca621b 100644 --- a/plugin/sdf/README.md +++ b/plugin/sdf/README.md @@ -95,6 +95,6 @@ class MySDF { mjtNum attribute[MySDFAttribute::nattribute]; private: - Torus(const mjModel* m, mjData* d, int instance); + MySDF(const mjModel* m, mjData* d, int instance); }; ``` From 63ec34f9f8a5ffa3a97812e1d74075419e96637a Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Wed, 3 Jan 2024 09:52:30 -0800 Subject: [PATCH 095/121] Add plugins to flex parent body if there are pinned vertices. Fixes #1270. PiperOrigin-RevId: 595429578 Change-Id: Ifc61d4bde4dd3c6b4d4a09106152de1d24e71ef0 --- doc/changelog.rst | 7 +- plugin/elasticity/elasticity.h | 3 + src/user/user_flexcomp.cc | 8 +++ test/plugin/elasticity/elasticity_test.cc | 79 ++++++++++++++++------- 4 files changed, 71 insertions(+), 26 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index 52592511..0b89de4b 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -7,9 +7,12 @@ Upcoming version (not yet released) MJX ^^^ +1. Added :ref:`dyntype` ``filterexact``. +2. Added :at:`site` transmission. -1. Add :ref:`dyntype` ``filterexact``. -2. Add :at:`site` transmission. +Bug fixes +^^^^^^^^^ +3. Fixed a bug that prevented the use of pins with plugins if flexes are not in the worldbody. Fixes :github:issue:`1270`. Version 3.1.1 (December 18, 2023) diff --git a/plugin/elasticity/elasticity.h b/plugin/elasticity/elasticity.h index 8436777e..00074a6e 100644 --- a/plugin/elasticity/elasticity.h +++ b/plugin/elasticity/elasticity.h @@ -133,6 +133,9 @@ inline void ComputeForce(mjtNum* qfrc_passive, if (vertbodyid) { body_dofnum = m->body_dofnum[vertbodyid[v[i]]]; body_dofadr = m->body_dofadr[vertbodyid[v[i]]]; + if (body_dofnum && m->body_simple[vertbodyid[v[i]]] != 2) { + mju_error("Non-simple or non-static bodies are not yet supported"); + } } for (int x = 0; x < body_dofnum; x++) { qfrc_passive[body_dofadr+x] -= force[3*i+x]; diff --git a/src/user/user_flexcomp.cc b/src/user/user_flexcomp.cc index 033a215b..b6f24b96 100644 --- a/src/user/user_flexcomp.cc +++ b/src/user/user_flexcomp.cc @@ -399,6 +399,14 @@ bool mjCFlexcomp::Make(mjCModel* model, mjCBody* body, char* error, int error_sz // pinned: parent body if (pinned[i]) { pf->vertbody.push_back(body->name); + + // add plugin + if (plugin_instance) { + body->is_plugin = true; + body->plugin_name = plugin_name; + body->plugin_instance = plugin_instance; + body->plugin_instance_name = plugin_instance_name; + } } // not pinned: new body diff --git a/test/plugin/elasticity/elasticity_test.cc b/test/plugin/elasticity/elasticity_test.cc index ae240eba..199f3d0b 100644 --- a/test/plugin/elasticity/elasticity_test.cc +++ b/test/plugin/elasticity/elasticity_test.cc @@ -31,6 +31,38 @@ namespace { using ElasticityTest = PluginTest; +// -------------------------------- flex ------------------------------------ +TEST_F(ElasticityTest, FlexCompatibility) { + static constexpr char flex_xml[] = R"( + + + + + + + + + + + + + + + + + + )"; + + char error[1024] = {0}; + mjModel* m = LoadModelFromString(flex_xml, error, sizeof(error)); + ASSERT_THAT(m, testing::NotNull()) << error; + + mjData* d = mj_makeData(m); + mj_deleteData(d); + mj_deleteModel(m); +} + // -------------------------------- shell ----------------------------------- TEST_F(ElasticityTest, ElasticEnergyShell) { static constexpr char cantilever_xml[] = R"( @@ -144,6 +176,29 @@ 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()); +} + // -------------------------------- solid ----------------------------------- TEST_F(ElasticityTest, ElasticEnergySolid) { static constexpr char cantilever_xml[] = R"( @@ -202,7 +257,6 @@ TEST_F(ElasticityTest, ElasticEnergySolid) { } // -------------------------------- cable ----------------------------------- - TEST_F(ElasticityTest, CantileverIntoCircle) { static constexpr char cantilever_xml[] = R"( @@ -303,29 +357,6 @@ TEST_F(ElasticityTest, InvalidMixedAttribute) { ASSERT_THAT(m, testing::IsNull()); } -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()); -} - TEST_F(ElasticityTest, ValidAttributes) { static constexpr char cantilever_xml[] = R"( From 7ce05f495702fa3c04688b27025444173b0d3667 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Thu, 4 Jan 2024 09:16:52 -0800 Subject: [PATCH 096/121] Improve documentation of solver parameters. PiperOrigin-RevId: 595723619 Change-Id: I00b67eaec02d2e85ce60fb44eb3e582ce036be3a --- doc/modeling.rst | 23 ++++++--- test/engine/engine_core_constraint_test.cc | 56 ++++++++++++++++++++++ 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/doc/modeling.rst b/doc/modeling.rst index 681534cd..b146407f 100644 --- a/doc/modeling.rst +++ b/doc/modeling.rst @@ -272,6 +272,7 @@ approximately .. math:: \ac + d \cdot (b v + k r) = (1 - d)\cdot \au + :label: eq:constraint Again, the parameters that are under the user's control are :math:`d, b, k`. The remaining quantities are functions of the system state and are computed automatically at each time step. @@ -343,12 +344,9 @@ Next we explain the setting of the stiffness :math:`k` and damping :math:`b` whi .. admonition:: Intuitive description of the **reference acceleration** - The *reference acceleration* :math:`\ar` determines the **motion that constraint is trying to achieve** in - order to rectify violation. For example, consider a contact between a motionless free body pulled down by gravity - onto a static plane geom. Since there is no motion, the penetration will be entirely determined by the impedance - while the reference has no effect. Now imagine that the body is dropped onto the plane. Upon impact the constraint - will generate a normal force which attempts to rectify the penetration using a particular motion; this motion is - the reference acceleration. + The *reference acceleration* :math:`\ar` determines the **motion that constraint is trying to achieve** in order to + rectify violation. Imagine a body dropped onto the plane. Upon impact the constraint will generate a normal force + which attempts to rectify the penetration using a particular motion; this motion is the reference acceleration. Another way of understanding the reference acceleration is to think of the unmodeled deformation variables described in the :ref:`Computation chapter`. Imagine two bodies pressed together, leading to deformation at @@ -393,7 +391,12 @@ and the damping ratio is ignored. Equivalently, in the direct format, the :math: can go unstable. This is enforced internally, unless the :ref:`refsafe` attribute of :ref:`flag ` is set to false. The :math:`\text{dampratio}` parameter would normally be set to 1, corresponding to critical damping. Smaller values result in under-damped or bouncy constraints, while larger values result in - over-damped constraints. + over-damped constraints. Combining the above formula with :eq:`eq:constraint`, we can derive the following result. + If the reference acceleration is given using the positive number format and the impedance is constant + :math:`d = d_0 = d_\text{width}`, then the penetration depth at rest is + + .. math:: + r = \au \cdot (1 - d) \cdot \text{timeconst}^2 \cdot \text{dampratio}^2 Next we describe the direct format where the two numbers are :math:`(-\text{stiffness}, -\text{damping})`. This allows direct control over restitution in particular. We still apply some scaling so that the same numbers can be @@ -406,6 +409,12 @@ and the damping ratio is ignored. Equivalently, in the direct format, the :math: k &= \text{stiffness} \cdot d(r) / d_\text{width}^2 \\ \end{aligned} + Similarly to the above derivation, if the reference acceleration is given using the negative number format and the + impedance is constant, then the penetration depth at rest is + + .. math:: + r = \au \cdot (1 - d) \cdot \text{stiffness} + .. tip:: In the positive-value default format, the :math:`\text{timeconst}` parameter controls constraint **softness**. It is specified in units of time and means "how quickly is the constraint trying to resolve the violation". Larger diff --git a/test/engine/engine_core_constraint_test.cc b/test/engine/engine_core_constraint_test.cc index 6f62a739..6a7d3159 100644 --- a/test/engine/engine_core_constraint_test.cc +++ b/test/engine/engine_core_constraint_test.cc @@ -161,6 +161,62 @@ TEST_F(CoreConstraintTest, WeldRotJacobian) { mj_deleteModel(model); } +// test formulas for penetration at rest +TEST_F(CoreConstraintTest, RestPenetration) { + constexpr char xml[] = R"( + + + + + + + + + + )"; + mjModel* model = LoadModelFromString(xml); + ASSERT_THAT(model, testing::NotNull()); + mjtNum gravity = -model->opt.gravity[2]; + mjtNum damping_ratio = 0.8; + mjData* data = mj_makeData(model); + + for (const mjtNum reference : {-100.0, -10.0, 0.1, 0.01}) { + for (const mjtNum impedance : {0.3, 0.9, 0.99}) { + // set solimp + for (int i=0; i < model->ngeom; i++) { + model->geom_solimp[i*mjNIMP + 0] = impedance; + model->geom_solimp[i*mjNIMP + 1] = impedance; + } + + // set solref + for (int i=0; i < model->ngeom; i++) { + model->geom_solref[i*mjNREF + 0] = reference; + model->geom_solref[i*mjNREF + 1] = reference < 0 ? -10 : damping_ratio; + } + + // simulate for 50 seconds + mj_resetData(model, data); + while (data->time < 50) { + mj_step(model, data); + } + + mjtNum depth = -data->contact[0].dist; + mjtNum expected_depth; + if (reference < 0) { + expected_depth = gravity * (1 - impedance) / -reference; + } else { + mjtNum tc_dr = reference * damping_ratio; + expected_depth = gravity * (1 - impedance) * tc_dr * tc_dr; + } + + EXPECT_THAT(depth, DoubleNear(expected_depth, 1e-10)); + } + } + + mj_deleteData(data); + mj_deleteModel(model); +} + static const char* const kDoflessContactPath = "engine/testdata/core_constraint/dofless_contact.xml"; static const char* const kDoflessTendonFrictionalPath = From 721e2d5589d3fdafd440009374a31521214088b7 Mon Sep 17 00:00:00 2001 From: Baruch Tabanpour Date: Thu, 4 Jan 2024 10:44:00 -0800 Subject: [PATCH 097/121] Update tutorial notebook. PiperOrigin-RevId: 595746621 Change-Id: Ie0a4ea01d4f50491b14713f12ea8a4fe6877f890 --- mjx/tutorial.ipynb | 916 ++++++++++++++++++++++++--------------------- 1 file changed, 481 insertions(+), 435 deletions(-) diff --git a/mjx/tutorial.ipynb b/mjx/tutorial.ipynb index f27461c6..8644de41 100644 --- a/mjx/tutorial.ipynb +++ b/mjx/tutorial.ipynb @@ -157,20 +157,25 @@ "source": [ "#@title Import MuJoCo, MJX, and Brax\n", "\n", + "\n", "from datetime import datetime\n", "import functools\n", + "from IPython.display import HTML\n", "import jax\n", "from jax import numpy as jp\n", "import numpy as np\n", - "from typing import Any, Dict, Tuple, Union\n", + "from typing import Any, Dict, Sequence, Tuple, Union\n", "\n", + "from brax import base\n", "from brax import envs\n", "from brax import math\n", "from brax.base import Base, Motion, Transform\n", - "from brax.envs.base import Env, State\n", + "from brax.envs.base import Env, MjxEnv, State\n", + "from brax.mjx.base import State as MjxState\n", "from brax.training.agents.ppo import train as ppo\n", "from brax.training.agents.ppo import networks as ppo_networks\n", - "from brax.io import model\n", + "from brax.io import html, mjcf, model\n", + "\n", "from etils import epath\n", "from flax import struct\n", "from matplotlib import pyplot as plt\n", @@ -180,6 +185,211 @@ "from mujoco import mjx\n" ] }, + { + "cell_type": "markdown", + "metadata": { + "id": "Nj4-Xmx4DFaq" + }, + "source": [ + "# Introduction to MJX\n", + "\n", + "MJX is an implementation of MuJoCo written in [JAX](https://jax.readthedocs.io/en/latest/index.html), enabling large batch training on GPU/TPU. In this notebook, we will demonstrate how to train RL policies with MJX.\n", + "\n", + "Before we get into hefty RL workloads, let's get started with a simpler example! The entrypoint into MJX is through MuJoCo, so first we load a MuJoCo model:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bNus3mbbDz6a" + }, + "outputs": [], + "source": [ + "xml = \"\"\"\n", + "\u003cmujoco\u003e\n", + " \u003cworldbody\u003e\n", + " \u003clight name=\"top\" pos=\"0 0 1\"/\u003e\n", + " \u003cbody name=\"box_and_sphere\" euler=\"0 0 -30\"\u003e\n", + " \u003cjoint name=\"swing\" type=\"hinge\" axis=\"1 -1 0\" pos=\"-.2 -.2 -.2\"/\u003e\n", + " \u003cgeom name=\"red_box\" type=\"box\" size=\".2 .2 .2\" rgba=\"1 0 0 1\"/\u003e\n", + " \u003cgeom name=\"green_sphere\" pos=\".2 .2 .2\" size=\".1\" rgba=\"0 1 0 1\"/\u003e\n", + " \u003c/body\u003e\n", + " \u003c/worldbody\u003e\n", + "\u003c/mujoco\u003e\n", + "\"\"\"\n", + "\n", + "# Make model, data, and renderer\n", + "mj_model = mujoco.MjModel.from_xml_string(xml)\n", + "mj_data = mujoco.MjData(mj_model)\n", + "renderer = mujoco.Renderer(mj_model)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Po5oykJbFQbj" + }, + "source": [ + "Next we take the MuJoCo model and data, and place them on the GPU device using MJX." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TSpoOWqeEC3P" + }, + "outputs": [], + "source": [ + "mjx_model = mjx.put_model(mj_model)\n", + "mjx_data = mjx.put_data(mj_model, mj_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6rxMMSs4OJJf" + }, + "source": [ + "Below, we print the `qpos` from MuJoCo and MJX. Notice that the `qpos` for the mjData is a numpy array living on the CPU, while the `qpos` for `mjx.Data` is a JAX Array living on the GPU device." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ZOD582pfOLP-" + }, + "outputs": [], + "source": [ + "print(mj_data.qpos, type(mj_data.qpos))\n", + "print(mjx_data.qpos, type(mjx_data.qpos), mjx_data.qpos.devices())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZShF9-o_JLm3" + }, + "source": [ + "Let's run the simulation in MuJoCo and render the trajectory. This example is taken from the [MuJoCo tutorial](https://colab.sandbox.google.com/github/google-deepmind/mujoco/blob/main/python/tutorial.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "HDlPlX05I3m-" + }, + "outputs": [], + "source": [ + "# enable joint visualization option:\n", + "scene_option = mujoco.MjvOption()\n", + "scene_option.flags[mujoco.mjtVisFlag.mjVIS_JOINT] = True\n", + "\n", + "duration = 3.8 # (seconds)\n", + "framerate = 60 # (Hz)\n", + "\n", + "frames = []\n", + "mujoco.mj_resetData(mj_model, mj_data)\n", + "while mj_data.time \u003c duration:\n", + " mujoco.mj_step(mj_model, mj_data)\n", + " if len(frames) \u003c mj_data.time * framerate:\n", + " renderer.update_scene(mj_data, scene_option=scene_option)\n", + " pixels = renderer.render()\n", + " frames.append(pixels)\n", + "\n", + "# Simulate and display video.\n", + "media.show_video(frames, fps=framerate)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "m70b_RxBJOyd" + }, + "source": [ + "Now let's run the same exact simulation on the GPU device using MJX!\n", + "\n", + "In the example below, we use `mjx.step` instead of `mujoco.mj_step`, and we also [`jax.jit`](https://jax.readthedocs.io/en/latest/jax-101/02-jitting.html) the `mjx.step` so that it runs efficiently on the GPU. After each step, we convert the `mjx.Data` back to `mjData` so that we can use the MuJoCo renderer.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Pr29xq0-JRQv" + }, + "outputs": [], + "source": [ + "\n", + "jit_step = jax.jit(mjx.step)\n", + "\n", + "frames = []\n", + "mujoco.mj_resetData(mj_model, mj_data)\n", + "mjx_data = mjx.put_data(mj_model, mj_data)\n", + "while mjx_data.time \u003c duration:\n", + " mjx_data = jit_step(mjx_model, mjx_data)\n", + " if len(frames) \u003c mjx_data.time * framerate:\n", + " mj_data = mjx.get_data(mj_model, mjx_data)\n", + " renderer.update_scene(mj_data, scene_option=scene_option)\n", + " pixels = renderer.render()\n", + " frames.append(pixels)\n", + "\n", + "media.show_video(frames, fps=framerate)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wXsQ4qO2KO3Q" + }, + "source": [ + "Running single threaded physics simulation on the GPU is not very [efficient](https://mujoco.readthedocs.io/en/stable/mjx.html#mjx-the-sharp-bits). The advantage with MJX is that we can run environments in parallel on a hardware accelerated device. Let's try it out!\n", + "\n", + "In the example below, we create 4096 copies of the `mjx.Data` and we run the `mjx.step` over the batched data. Since MJX is implemented in JAX, we take advantage of [`jax.vmap`](https://jax.readthedocs.io/en/latest/_autosummary/jax.vmap.html) to run the `mjx.step` in parallel over all `mjx.Data`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "rrdrcKRVK6w9" + }, + "outputs": [], + "source": [ + "rng = jax.random.PRNGKey(0)\n", + "rng = jax.random.split(rng, 4096)\n", + "batch = jax.vmap(lambda rng: mjx_data.replace(qpos=jax.random.uniform(rng, (1,))))(rng)\n", + "\n", + "jit_step = jax.vmap(mjx.step, in_axes=(None, 0))\n", + "batch = jit_step(mjx_model, batch)\n", + "\n", + "print(batch.qpos)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "x4lL220cOj0q" + }, + "source": [ + "We can copy the batched `mjx.Data` back to MuJoCo like we did before:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Jtz7j1PDOnw5" + }, + "outputs": [], + "source": [ + "batched_mj_data = mjx.get_data(mj_model, batch)\n", + "print([d.qpos for d in batched_mj_data])" + ] + }, { "cell_type": "markdown", "metadata": { @@ -187,145 +397,10 @@ }, "source": [ "# Training a Policy with MJX\n", - "MJX is an implementation of MuJoCo written in [JAX](https://jax.readthedocs.io/en/latest/index.html), enabling large batch training on GPU/TPU. In this notebook, we demonstrate how to train RL policies with MJX.\n", "\n", - "First, we implement an environment `State` so that we can plug into the [Brax](https://github.com/google/brax) environment API. `State` holds the observation, reward, metrics, and environment info. Notably `State.pipeline_state` holds a `mjx.Data` object, which is analogous to `mjData` in MuJoCo.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "7DQ_rW4CkIB_" - }, - "outputs": [], - "source": [ - "#@title State\n", + "Running large batch physics simulation is useful for training RL policies. Here we demonstrate training RL policies with MJX using the RL library from [Brax](https://github.com/google/brax).\n", "\n", - "@struct.dataclass\n", - "class State(Base):\n", - " \"\"\"Environment state for training and inference with brax.\n", - "\n", - " Args:\n", - " pipeline_state: the physics state, mjx.Data\n", - " obs: environment observations\n", - " reward: environment reward\n", - " done: boolean, True if the current episode has terminated\n", - " metrics: metrics that get tracked per environment step\n", - " info: environment variables defined and updated by the environment reset\n", - " and step functions\n", - " \"\"\"\n", - "\n", - " pipeline_state: mjx.Data\n", - " obs: jax.Array\n", - " reward: jax.Array\n", - " done: jax.Array\n", - " metrics: Dict[str, jax.Array] = struct.field(default_factory=dict)\n", - " info: Dict[str, Any] = struct.field(default_factory=dict)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "acpXtDLNXLV9" - }, - "source": [ - "\n", - "Next, we implement `MjxEnv`, an environment class we'll use through the notebook. `MjxEnv` initializes a `mjx.Model` and `mjx.Data` object. Notice that `MjxEnv` calls `mjx.step` for every `pipeline_step`, which is analgous to `mujoco.mj_step`.\n", - "\n", - "`MjxEnv` also inherits from `brax.envs.base.Env` which allows us to use the training agents implemented in brax." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "ccujYeJ5XOhx" - }, - "outputs": [], - "source": [ - "#@title MjxEnv\n", - "\n", - "class MjxEnv(Env):\n", - " \"\"\"API for driving an MJX system for training and inference in brax.\"\"\"\n", - "\n", - " def __init__(\n", - " self,\n", - " mj_model: mujoco.MjModel,\n", - " physics_steps_per_control_step: int = 1,\n", - " ):\n", - " \"\"\"Initializes MjxEnv.\n", - "\n", - " Args:\n", - " mj_model: mujoco.MjModel\n", - " physics_steps_per_control_step: the number of times to step the physics\n", - " pipeline for each environment step\n", - " \"\"\"\n", - " self.model = mj_model\n", - " self.data = mujoco.MjData(mj_model)\n", - " self.sys = mjx.device_put(mj_model)\n", - " self._physics_steps_per_control_step = physics_steps_per_control_step\n", - "\n", - " def pipeline_init(\n", - " self, qpos: jax.Array, qvel: jax.Array\n", - " ) -\u003e mjx.Data:\n", - " \"\"\"Initializes the physics state.\"\"\"\n", - " data = mjx.device_put(self.data)\n", - " data = data.replace(qpos=qpos, qvel=qvel, ctrl=jp.zeros(self.sys.nu))\n", - " data = mjx.forward(self.sys, data)\n", - " return data\n", - "\n", - " def pipeline_step(\n", - " self, data: mjx.Data, ctrl: jax.Array\n", - " ) -\u003e mjx.Data:\n", - " \"\"\"Takes a physics step using the physics pipeline.\"\"\"\n", - " def f(data, _):\n", - " data = data.replace(ctrl=ctrl)\n", - " return (\n", - " mjx.step(self.sys, data),\n", - " None,\n", - " )\n", - " data, _ = jax.lax.scan(f, data, (), self._physics_steps_per_control_step)\n", - " return data\n", - "\n", - " @property\n", - " def dt(self) -\u003e jax.Array:\n", - " \"\"\"The timestep used for each env step.\"\"\"\n", - " return self.sys.opt.timestep * self._physics_steps_per_control_step\n", - "\n", - " @property\n", - " def observation_size(self) -\u003e int:\n", - " rng = jax.random.PRNGKey(0)\n", - " reset_state = self.unwrapped.reset(rng)\n", - " return reset_state.obs.shape[-1]\n", - "\n", - " @property\n", - " def action_size(self) -\u003e int:\n", - " return self.sys.nu\n", - "\n", - " @property\n", - " def backend(self) -\u003e str:\n", - " return 'mjx'\n", - "\n", - " def _pos_vel(\n", - " self, data: mjx.Data\n", - " ) -\u003e Tuple[Transform, Motion]:\n", - " \"\"\"Returns 6d spatial transform and 6d velocity for all bodies.\"\"\"\n", - " x = Transform(pos=data.xpos[1:, :], rot=data.xquat[1:, :])\n", - " cvel = Motion(vel=data.cvel[1:, 3:], ang=data.cvel[1:, :3])\n", - " offset = data.xpos[1:, :] - data.subtree_com[\n", - " self.model.body_rootid[np.arange(1, self.model.nbody)]]\n", - " xd = Transform.create(pos=offset).vmap().do(cvel)\n", - " return x, xd\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "iPlFu4CiIgBN" - }, - "source": [ - "Finally we can implement a real environment. We choose to first implement the Humanoid environment. Notice that `reset` initializes a `State`, and `step` steps through the physics step and reward logic. The reward and stepping logic train the Humanoid to run forwards." + "Below, we implement the classic Humanoid environment using MJX and Brax. We inherit from the `MjxEnv` implementation in Brax so that we can step the physics with MJX while training with Brax RL implementations.\n" ] }, { @@ -361,10 +436,10 @@ " mj_model.opt.ls_iterations = 6\n", "\n", " physics_steps_per_control_step = 5\n", - " kwargs['physics_steps_per_control_step'] = kwargs.get(\n", - " 'physics_steps_per_control_step', physics_steps_per_control_step)\n", + " kwargs['n_frames'] = kwargs.get(\n", + " 'n_frames', physics_steps_per_control_step)\n", "\n", - " super().__init__(mj_model=mj_model, **kwargs)\n", + " super().__init__(model=mj_model, **kwargs)\n", "\n", " self._forward_reward_weight = forward_reward_weight\n", " self._ctrl_cost_weight = ctrl_cost_weight\n", @@ -390,7 +465,7 @@ "\n", " data = self.pipeline_init(qpos, qvel)\n", "\n", - " obs = self._get_obs(data, jp.zeros(self.sys.nu))\n", + " obs = self._get_obs(data.data, jp.zeros(self.sys.nu))\n", " reward, done, zero = jp.zeros(3)\n", " metrics = {\n", " 'forward_reward': zero,\n", @@ -410,14 +485,14 @@ " data0 = state.pipeline_state\n", " data = self.pipeline_step(data0, action)\n", "\n", - " com_before = data0.subtree_com[1]\n", - " com_after = data.subtree_com[1]\n", + " com_before = data0.data.subtree_com[1]\n", + " com_after = data.data.subtree_com[1]\n", " velocity = (com_after - com_before) / self.dt\n", " forward_reward = self._forward_reward_weight * velocity[0]\n", "\n", " min_z, max_z = self._healthy_z_range\n", - " is_healthy = jp.where(data.qpos[2] \u003c min_z, 0.0, 1.0)\n", - " is_healthy = jp.where(data.qpos[2] \u003e max_z, 0.0, is_healthy)\n", + " is_healthy = jp.where(data.q[2] \u003c min_z, 0.0, 1.0)\n", + " is_healthy = jp.where(data.q[2] \u003e max_z, 0.0, is_healthy)\n", " if self._terminate_when_unhealthy:\n", " healthy_reward = self._healthy_reward\n", " else:\n", @@ -425,7 +500,7 @@ "\n", " ctrl_cost = self._ctrl_cost_weight * jp.sum(jp.square(action))\n", "\n", - " obs = self._get_obs(data, action)\n", + " obs = self._get_obs(data.data, action)\n", " reward = forward_reward + healthy_reward - ctrl_cost\n", " done = 1.0 - is_healthy if self._terminate_when_unhealthy else 0.0\n", " state.metrics.update(\n", @@ -492,31 +567,7 @@ "\n", "# define the jit reset/step functions\n", "jit_reset = jax.jit(env.reset)\n", - "jit_step = jax.jit(env.step)\n", - "\n", - "# instantiate the renderer\n", - "renderer = mujoco.Renderer(env.model)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "9f2ME2WbA5Ip" - }, - "outputs": [], - "source": [ - "#@title Define a render utility function\n", - "\n", - "def get_image(state: State, camera: str) -\u003e np.ndarray:\n", - " \"\"\"Renders the environment state.\"\"\"\n", - " d = mujoco.MjData(env.model)\n", - " # write the mjx.Data into an mjData object\n", - " mjx.device_get_into(d, state.pipeline_state)\n", - " mujoco.mj_forward(env.model, d)\n", - " # use the mjData object to update the renderer\n", - " renderer.update_scene(d, camera=camera)\n", - " return renderer.render()\n" + "jit_step = jax.jit(env.step)\n" ] }, { @@ -529,17 +580,15 @@ "source": [ "# initialize the state\n", "state = jit_reset(jax.random.PRNGKey(0))\n", - "rollout = [state]\n", - "images = [get_image(state, camera='side')]\n", + "rollout = [state.pipeline_state]\n", "\n", "# grab a trajectory\n", "for i in range(10):\n", " ctrl = -0.1 * jp.ones(env.sys.nu)\n", " state = jit_step(state, ctrl)\n", - " rollout.append(state)\n", - " images.append(get_image(state, camera='side'))\n", + " rollout.append(state.pipeline_state)\n", "\n", - "media.show_video(images, fps=1.0 / env.dt)" + "media.show_video(env.render(rollout, camera='side'), fps=1.0 / env.dt)" ] }, { @@ -550,7 +599,7 @@ "source": [ "## Train Humanoid Policy\n", "\n", - "Let's finally train a policy with PPO to make the Humanoid run forwards. Training takes about 13-14 minutes on a Tesla V100 GPU." + "Let's now train a policy with PPO to make the Humanoid run forwards. Training takes about 9-10 minutes on a Tesla A100 GPU." ] }, { @@ -604,7 +653,7 @@ "id": "YYIch0HEApBx" }, "source": [ - "## Save and Load Policy\n", + "\u003c!-- ## Save and Load Policy --\u003e\n", "\n", "We can save and load the policy using the brax model API." ] @@ -673,8 +722,7 @@ "# initialize the state\n", "rng = jax.random.PRNGKey(0)\n", "state = jit_reset(rng)\n", - "rollout = [state]\n", - "images = [get_image(state, camera='side')]\n", + "rollout = [state.pipeline_state]\n", "\n", "# grab a trajectory\n", "n_steps = 500\n", @@ -684,14 +732,12 @@ " act_rng, rng = jax.random.split(rng)\n", " ctrl, _ = jit_inference_fn(state.obs, act_rng)\n", " state = jit_step(state, ctrl)\n", - " rollout.append(state)\n", - " if i % render_every == 0:\n", - " images.append(get_image(state, camera='side'))\n", + " rollout.append(state.pipeline_state)\n", "\n", " if state.done:\n", " break\n", "\n", - "media.show_video(images, fps=1.0 / eval_env.dt / render_every)" + "media.show_video(env.render(rollout[::render_every], camera='side'), fps=1.0 / env.dt / render_every)" ] }, { @@ -702,7 +748,7 @@ "source": [ "# MJX Policy in MuJoCo\n", "\n", - "Note that we can also perform the physics step using the original MuJoCo python bindings to show that the policy trained in MJX works in MuJoCo." + "We can also perform the physics step using the original MuJoCo python bindings to show that the policy trained in MJX works in MuJoCo." ] }, { @@ -713,7 +759,7 @@ }, "outputs": [], "source": [ - "mj_model = eval_env.model\n", + "mj_model = eval_env._model\n", "mj_data = mujoco.MjData(mj_model)\n", "\n", "renderer = mujoco.Renderer(mj_model)\n", @@ -723,11 +769,11 @@ "for i in range(n_steps):\n", " act_rng, rng = jax.random.split(rng)\n", "\n", - " obs = eval_env._get_obs(mjx.device_put(mj_data), ctrl)\n", + " obs = eval_env._get_obs(mjx.put_data(mj_model, mj_data), ctrl)\n", " ctrl, _ = jit_inference_fn(obs, act_rng)\n", "\n", " mj_data.ctrl = ctrl\n", - " for _ in range(eval_env._physics_steps_per_control_step):\n", + " for _ in range(eval_env._n_frames):\n", " mujoco.mj_step(mj_model, mj_data) # Physics step using MuJoCo mj_step.\n", "\n", " if i % render_every == 0:\n", @@ -743,7 +789,7 @@ "id": "65mIPj6DQNNa" }, "source": [ - "# Domain Randomization\n", + "# Training a Policy with Domain Randomization\n", "\n", "We might also want to include randomization over certain `mjModel` parameters while training a policy. In MJX, we can easily create a batch of environments with randomized values populated in `mjx.Model`. Below, we show a function that randomizes friction and actuator gain/bias." ] @@ -766,7 +812,7 @@ " friction = sys.geom_friction.at[:, 0].set(friction)\n", " # actuator\n", " _, key = jax.random.split(key, 2)\n", - " gain_range = (-10, -5)\n", + " gain_range = (-5, 5)\n", " param = jax.random.uniform(\n", " key, (1,), minval=gain_range[0], maxval=gain_range[1]\n", " ) + sys.actuator_gainprm[:, 0]\n", @@ -798,7 +844,7 @@ "id": "gnsZo-GWSYYj" }, "source": [ - "If we wanted 10 environments with randomized friction and actuator params, we can call `domain_randomize`, which returns a batched `mjModel` along with a dictionary specifying the axes that are batched." + "If we wanted 10 environments with randomized friction and actuator params, we can call `domain_randomize`, which returns a batched `mjx.Model` along with a dictionary specifying the axes that are batched." ] }, { @@ -828,7 +874,18 @@ "source": [ "## Quadruped Env\n", "\n", - "Let's define a quadruped environment that takes advantage of the domain randomization function. Here we use the [Barkour v0 Quadruped](https://github.com/google-deepmind/mujoco_menagerie/tree/main/google_barkour_v0) and an environment that trains a joystick policy." + "Let's define a quadruped environment that takes advantage of the domain randomization function. Here we use the [Barkour vb Quadruped](https://github.com/google-deepmind/mujoco_menagerie/tree/main/google_barkour_vb) from [MuJoCo Menagerie](https://github.com/google-deepmind/mujoco_menagerie). We implement an environment that trains a joystick policy with Brax." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "VfyK73gtRXid" + }, + "outputs": [], + "source": [ + "!git clone https://github.com/google-deepmind/mujoco_menagerie" ] }, { @@ -839,7 +896,7 @@ }, "outputs": [], "source": [ - "#@title Barkour v0 Quadruped Env\n", + "#@title Barkour vb Quadruped Env\n", "\n", "def get_config():\n", " \"\"\"Returns reward config for barkour quadruped environment.\"\"\"\n", @@ -869,11 +926,10 @@ " # Penalize non-zero roll and pitch angles. L2 penalty.\n", " orientation=-5.0,\n", " # L2 regularization of joint torques, |tau|^2.\n", - " # torques=-0.0002,\n", - " torques=-0.002,\n", + " torques=-0.0002,\n", " # Penalize the change in the action and encourage smooth\n", " # actions. L2 regularization |action - last_action|^2\n", - " action_rate=-0.1,\n", + " action_rate=-0.01,\n", " # Encourage long swing steps. However, it does not\n", " # encourage high clearances.\n", " feet_air_time=0.2,\n", @@ -893,7 +949,10 @@ " return default_config\n", "\n", " default_config = config_dict.ConfigDict(\n", - " dict(rewards=get_default_rewards_config(),))\n", + " dict(\n", + " rewards=get_default_rewards_config(),\n", + " )\n", + " )\n", "\n", " return default_config\n", "\n", @@ -904,46 +963,69 @@ " def __init__(\n", " self,\n", " obs_noise: float = 0.05,\n", - " action_scale: float=0.3,\n", + " action_scale: float = 0.3,\n", + " kick_vel: float = 0.05,\n", " **kwargs,\n", " ):\n", - " path = epath.Path(epath.resource_path('mujoco')) / (\n", - " 'mjx/benchmark/model/barkour_v0/assets'\n", - " )\n", - " mj_model = mujoco.MjModel.from_xml_path(\n", - " (path / 'barkour_v0_mjx.xml').as_posix())\n", - " mj_model.opt.solver = mujoco.mjtSolver.mjSOL_CG\n", - " mj_model.opt.iterations = 4\n", - " mj_model.opt.ls_iterations = 6\n", + " path = epath.Path('mujoco_menagerie/google_barkour_vb/scene_mjx.xml')\n", + " self._dt = 0.02 # this environment is 50 fps\n", + " self.brax_sys = mjcf.load(path).replace(dt=self._dt)\n", + " model = self.brax_sys.get_model()\n", + " model.opt.timestep = 0.004\n", "\n", - " physics_steps_per_control_step = 10\n", - " kwargs['physics_steps_per_control_step'] = kwargs.get(\n", - " 'physics_steps_per_control_step', physics_steps_per_control_step)\n", - " super().__init__(mj_model=mj_model, **kwargs)\n", + " # override menagerie params for smoother policy\n", + " model.dof_damping[6:] = 0.5239\n", + " model.actuator_gainprm[:, 0] = 35.0\n", + " model.actuator_biasprm[:, 1] = -35.0\n", "\n", - " self.torso_idx = mujoco.mj_name2id(\n", - " mj_model, mujoco.mjtObj.mjOBJ_BODY.value, 'torso'\n", + " n_frames = kwargs.pop('n_frames', int(self._dt / model.opt.timestep))\n", + " super().__init__(model=model, n_frames=n_frames)\n", + "\n", + " self.reward_config = get_config()\n", + " # set custom from kwargs\n", + " for k, v in kwargs.items():\n", + " if k.endswith('_scale'):\n", + " self.reward_config.rewards.scales[k[:-6]] = v\n", + "\n", + " self._torso_idx = mujoco.mj_name2id(\n", + " model, mujoco.mjtObj.mjOBJ_BODY.value, 'torso'\n", " )\n", " self._action_scale = action_scale\n", " self._obs_noise = obs_noise\n", - " self._reset_horizon = 500\n", - " self._feet_index = jp.array([3, 6, 9, 12])\n", - " # local positions for each foot\n", - " self._feet_pos = jp.array([\n", - " [-0.191284, -0.0191638, 0.013],\n", - " [-0.191284, -0.0191638, -0.013],\n", - " [-0.191284, -0.0191638, 0.013],\n", - " [-0.191284, -0.0191638, -0.013],\n", - " ])\n", - " self._init_q = mj_model.keyframe('standing').qpos\n", - " self._default_ap_pose = mj_model.keyframe('standing').qpos[7:]\n", - " self.reward_config = get_config()\n", - " self.lowers = self._default_ap_pose - jp.array([0.2, 0.8, 0.8] * 4)\n", - " self.uppers = self._default_ap_pose + jp.array([0.2, 0.8, 0.8] * 4)\n", - " self._foot_radius = 0.014\n", + " self._kick_vel = kick_vel\n", + " self._init_q = jp.array(model.keyframe('home').qpos)\n", + " self._default_pose = model.keyframe('home').qpos[7:]\n", + " self.lowers = jp.array([-0.7, -1.0, 0.05] * 4)\n", + " self.uppers = jp.array([0.52, 2.1, 2.1] * 4)\n", + " feet_site = [\n", + " 'foot_front_left',\n", + " 'foot_hind_left',\n", + " 'foot_front_right',\n", + " 'foot_hind_right',\n", + " ]\n", + " feet_site_id = [\n", + " mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE.value, f)\n", + " for f in feet_site\n", + " ]\n", + " assert not any(id_ == -1 for id_ in feet_site_id), 'Site not found.'\n", + " self._feet_site_id = np.array(feet_site_id)\n", + " lower_leg_body = [\n", + " 'lower_leg_front_left',\n", + " 'lower_leg_hind_left',\n", + " 'lower_leg_front_right',\n", + " 'lower_leg_hind_right',\n", + " ]\n", + " lower_leg_body_id = [\n", + " mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY.value, l)\n", + " for l in lower_leg_body\n", + " ]\n", + " assert not any(id_ == -1 for id_ in lower_leg_body_id), 'Body not found.'\n", + " self._lower_leg_body_id = np.array(lower_leg_body_id)\n", + " self._foot_radius = 0.0175\n", + " self._nv = model.nv\n", "\n", " def sample_command(self, rng: jax.Array) -\u003e jax.Array:\n", - " lin_vel_x = [-0.6, 1.0] # min max [m/s]\n", + " lin_vel_x = [-0.6, 1.5] # min max [m/s]\n", " lin_vel_y = [-0.8, 0.8] # min max [m/s]\n", " ang_vel_yaw = [-0.7, 0.7] # min max [rad/s]\n", "\n", @@ -960,216 +1042,156 @@ " new_cmd = jp.array([lin_vel_x[0], lin_vel_y[0], ang_vel_yaw[0]])\n", " return new_cmd\n", "\n", - " def reset(self, rng: jax.Array) -\u003e State:\n", + " def reset(self, rng: jax.Array) -\u003e State: # pytype: disable=signature-mismatch\n", " rng, key = jax.random.split(rng)\n", "\n", - " qpos = jp.array(self._init_q)\n", - " qvel = jp.zeros(self.model.nv)\n", - " new_cmd = self.sample_command(key)\n", - " data = self.pipeline_init(qpos, qvel)\n", + " pipeline_state = self.pipeline_init(self._init_q, jp.zeros(self._nv))\n", "\n", " state_info = {\n", " 'rng': rng,\n", " 'last_act': jp.zeros(12),\n", " 'last_vel': jp.zeros(12),\n", - " 'last_contact_buffer': jp.zeros((20, 4), dtype=bool),\n", - " 'command': new_cmd,\n", + " 'command': self.sample_command(key),\n", " 'last_contact': jp.zeros(4, dtype=bool),\n", " 'feet_air_time': jp.zeros(4),\n", - " 'obs_history': jp.zeros(15 * 31),\n", - " 'reward_tuple': {\n", - " 'tracking_lin_vel': 0.0,\n", - " 'tracking_ang_vel': 0.0,\n", - " 'lin_vel_z': 0.0,\n", - " 'ang_vel_xy': 0.0,\n", - " 'orientation': 0.0,\n", - " 'torque': 0.0,\n", - " 'action_rate': 0.0,\n", - " 'stand_still': 0.0,\n", - " 'feet_air_time': 0.0,\n", - " 'foot_slip': 0.0,\n", - " },\n", + " 'rewards': {k: 0.0 for k in self.reward_config.rewards.scales.keys()},\n", + " 'kick': jp.array([0.0, 0.0]),\n", " 'step': 0,\n", " }\n", "\n", - " x, xd = self._pos_vel(data)\n", - " obs = self._get_obs(data.qpos, x, xd, state_info)\n", + " obs_history = jp.zeros(15 * 31) # store 15 steps of history\n", + " obs = self._get_obs(pipeline_state, state_info, obs_history)\n", " reward, done = jp.zeros(2)\n", " metrics = {'total_dist': 0.0}\n", - " for k in state_info['reward_tuple']:\n", - " metrics[k] = state_info['reward_tuple'][k]\n", - " state = State(data, obs, reward, done, metrics, state_info)\n", + " for k in state_info['rewards']:\n", + " metrics[k] = state_info['rewards'][k]\n", + " state = State(pipeline_state, obs, reward, done, metrics, state_info) # pytype: disable=wrong-arg-types\n", " return state\n", "\n", - " def step(self, state: State, action: jax.Array) -\u003e State:\n", - " rng, rng_noise, cmd_rng = jax.random.split(\n", - " state.info['rng'], 3\n", - " )\n", + " def step(self, state: State, action: jax.Array) -\u003e State: # pytype: disable=signature-mismatch\n", + " rng, cmd_rng, kick_noise_2 = jax.random.split(state.info['rng'], 3)\n", + "\n", + " # kick\n", + " push_interval = 10\n", + " kick_theta = jax.random.uniform(kick_noise_2, maxval=2 * jp.pi)\n", + " kick = jp.array([jp.cos(kick_theta), jp.sin(kick_theta)])\n", + " kick *= jp.mod(state.info['step'], push_interval) == 0\n", + " qvel = state.pipeline_state.data.qvel # pytype: disable=attribute-error\n", + " qvel = qvel.at[:2].set(kick * self._kick_vel + qvel[:2])\n", + " state = state.tree_replace({'pipeline_state.data.qvel': qvel})\n", "\n", " # physics step\n", - " cur_action = jp.array(action)\n", - " action = action[:12] * self._action_scale\n", - " motor_targets = jp.clip(\n", - " action + self._default_ap_pose, self.lowers, self.uppers\n", - " )\n", - " data = self.pipeline_step(state.pipeline_state, motor_targets)\n", + " motor_targets = self._default_pose + action * self._action_scale\n", + " motor_targets = jp.clip(motor_targets, self.lowers, self.uppers)\n", + " pipeline_state = self.pipeline_step(state.pipeline_state, motor_targets)\n", + " x, xd = pipeline_state.x, pipeline_state.xd\n", "\n", " # observation data\n", - " x, xd = self._pos_vel(data)\n", - " obs = self._get_obs(data.qpos, x, xd, state.info)\n", - " obs_noise = self._obs_noise * jax.random.uniform(\n", - " rng_noise, obs.shape, minval=-1, maxval=1)\n", - " qpos, qvel = data.qpos, data.qvel\n", - " joint_angles = qpos[7:]\n", - " joint_vel = qvel[6:]\n", + " obs = self._get_obs(pipeline_state, state.info, state.obs)\n", + " joint_angles = pipeline_state.q[7:]\n", + " joint_vel = pipeline_state.qd[6:]\n", "\n", " # foot contact data based on z-position\n", - " foot_contact_pos = (\n", - " self._get_feet_pos_vel(x, xd)[0][:, 2]\n", - " - self._foot_radius\n", - " )\n", - " contact = foot_contact_pos \u003c 1e-3 # a mm or less off the floor\n", - " contact_filt_mm = jp.logical_or(contact, state.info['last_contact'])\n", - " contact_filt_cm = jp.logical_or(\n", - " foot_contact_pos \u003c 3e-2, state.info['last_contact']\n", - " ) # 3cm or less off the floor\n", - " first_contact = (state.info['feet_air_time'] \u003e 0) * (contact_filt_mm)\n", + " foot_pos = pipeline_state.data.site_xpos[self._feet_site_id] # pytype: disable=attribute-error\n", + " foot_contact_z = foot_pos[:, 2] - self._foot_radius\n", + " contact = foot_contact_z \u003c 1e-3 # a mm or less off the floor\n", + " contact_filt_mm = contact | state.info['last_contact']\n", + " contact_filt_cm = (foot_contact_z \u003c 3e-2) | state.info['last_contact']\n", + " first_contact = (state.info['feet_air_time'] \u003e 0) * contact_filt_mm\n", " state.info['feet_air_time'] += self.dt\n", "\n", + " # done if joint limits are reached or robot is falling\n", + " up = jp.array([0.0, 0.0, 1.0])\n", + " done = jp.dot(math.rotate(up, x.rot[self._torso_idx - 1]), up) \u003c 0\n", + " done |= jp.any(joint_angles \u003c self.lowers)\n", + " done |= jp.any(joint_angles \u003e self.uppers)\n", + " done |= pipeline_state.x.pos[self._torso_idx - 1, 2] \u003c 0.18\n", + "\n", " # reward\n", - " reward_tuple = {\n", + " rewards = {\n", " 'tracking_lin_vel': (\n", " self._reward_tracking_lin_vel(state.info['command'], x, xd)\n", - " * self.reward_config.rewards.scales.tracking_lin_vel\n", " ),\n", " 'tracking_ang_vel': (\n", " self._reward_tracking_ang_vel(state.info['command'], x, xd)\n", - " * self.reward_config.rewards.scales.tracking_ang_vel\n", " ),\n", - " 'lin_vel_z': (\n", - " self._reward_lin_vel_z(xd)\n", - " * self.reward_config.rewards.scales.lin_vel_z\n", + " 'lin_vel_z': self._reward_lin_vel_z(xd),\n", + " 'ang_vel_xy': self._reward_ang_vel_xy(xd),\n", + " 'orientation': self._reward_orientation(x),\n", + " 'torques': self._reward_torques(pipeline_state.data.qfrc_actuator), # pytype: disable=attribute-error\n", + " 'action_rate': self._reward_action_rate(action, state.info['last_act']),\n", + " 'stand_still': self._reward_stand_still(\n", + " state.info['command'], joint_angles,\n", " ),\n", - " 'ang_vel_xy': (\n", - " self._reward_ang_vel_xy(xd)\n", - " * self.reward_config.rewards.scales.ang_vel_xy\n", - " ),\n", - " 'orientation': (\n", - " self._reward_orientation(x)\n", - " * self.reward_config.rewards.scales.orientation\n", - " ),\n", - " 'torque': (\n", - " self._reward_torques(data.qfrc_actuator)\n", - " * self.reward_config.rewards.scales.torques\n", - " ),\n", - " 'action_rate': (\n", - " self._reward_action_rate(cur_action, state.info['last_act'])\n", - " * self.reward_config.rewards.scales.action_rate\n", - " ),\n", - " 'stand_still': (\n", - " self._reward_stand_still(\n", - " state.info['command'], joint_angles, self._default_ap_pose\n", - " )\n", - " * self.reward_config.rewards.scales.stand_still\n", - " ),\n", - " 'feet_air_time': (\n", - " self._reward_feet_air_time(\n", - " state.info['feet_air_time'],\n", - " first_contact,\n", - " state.info['command'],\n", - " )\n", - " * self.reward_config.rewards.scales.feet_air_time\n", - " ),\n", - " 'foot_slip': (\n", - " self._reward_foot_slip(x, xd, contact_filt_cm)\n", - " * self.reward_config.rewards.scales.foot_slip\n", + " 'feet_air_time': self._reward_feet_air_time(\n", + " state.info['feet_air_time'],\n", + " first_contact,\n", + " state.info['command'],\n", " ),\n", + " 'foot_slip': self._reward_foot_slip(pipeline_state, contact_filt_cm),\n", + " 'termination': self._reward_termination(done, state.info['step']),\n", " }\n", - " reward = sum(reward_tuple.values())\n", - " reward = jp.clip(reward * self.dt, 0.0, 10000.0)\n", + " rewards = {\n", + " k: v * self.reward_config.rewards.scales[k] for k, v in rewards.items()\n", + " }\n", + " reward = jp.clip(sum(rewards.values()) * self.dt, 0.0, 10000.0)\n", "\n", " # state management\n", - " state.info['last_act'] = cur_action\n", + " state.info['kick'] = kick\n", + " state.info['last_act'] = action\n", " state.info['last_vel'] = joint_vel\n", " state.info['feet_air_time'] *= ~contact_filt_mm\n", " state.info['last_contact'] = contact\n", - " state.info['last_contact_buffer'] = jp.roll(\n", - " state.info['last_contact_buffer'], 1, axis=0\n", - " )\n", - " state.info['last_contact_buffer'] = (\n", - " state.info['last_contact_buffer'].at[0].set(contact)\n", - " )\n", - " state.info['reward_tuple'] = reward_tuple\n", + " state.info['rewards'] = rewards\n", " state.info['step'] += 1\n", - " state.info.update(rng=rng)\n", + " state.info['rng'] = rng\n", "\n", - " # resetting logic if joint limits are reached or robot is falling\n", - " up = jp.array([0.0, 0.0, 1.0])\n", - " done = jp.dot(math.rotate(up, x.rot[0]), up) \u003c 0\n", - " done |= jp.any(joint_angles \u003c 0.98 * self.lowers)\n", - " done |= jp.any(joint_angles \u003e 0.98 * self.uppers)\n", - " done |= x.pos[0, 2] \u003c 0.18\n", - "\n", - " # termination reward\n", - " reward += (\n", - " done * (state.info['step'] \u003c self._reset_horizon) *\n", - " self.reward_config.rewards.scales.termination\n", - " )\n", - "\n", - " # when done, sample new command if more than _reset_horizon timesteps\n", - " # achieved\n", + " # sample new command if more than 500 timesteps achieved\n", " state.info['command'] = jp.where(\n", - " done \u0026 (state.info['step'] \u003e self._reset_horizon),\n", - " self.sample_command(cmd_rng), state.info['command'])\n", + " state.info['step'] \u003e 500,\n", + " self.sample_command(cmd_rng),\n", + " state.info['command'],\n", + " )\n", " # reset the step counter when done\n", " state.info['step'] = jp.where(\n", - " done | (state.info['step'] \u003e self._reset_horizon), 0,\n", - " state.info['step']\n", + " done | (state.info['step'] \u003e 500), 0, state.info['step']\n", " )\n", "\n", " # log total displacement as a proxy metric\n", - " state.metrics['total_dist'] = math.normalize(x.pos[self.torso_idx])[1]\n", - " for k in state.info['reward_tuple'].keys():\n", - " state.metrics[k] = state.info['reward_tuple'][k]\n", + " state.metrics['total_dist'] = math.normalize(x.pos[self._torso_idx - 1])[1]\n", + " state.metrics.update(state.info['rewards'])\n", "\n", + " done = jp.float32(done)\n", " state = state.replace(\n", - " pipeline_state=data, obs=obs + obs_noise, reward=reward,\n", - " done=done * 1.0)\n", + " pipeline_state=pipeline_state, obs=obs, reward=reward, done=done\n", + " )\n", " return state\n", "\n", - " def _get_obs(self, qpos: jax.Array, x: Transform, xd: Motion,\n", - " state_info: Dict[str, Any]) -\u003e jax.Array:\n", - " # Get observations:\n", - " # yaw_rate, projected_gravity, command, motor_angles, last_action\n", + " def _get_obs(\n", + " self,\n", + " pipeline_state: base.State,\n", + " state_info: dict[str, Any],\n", + " obs_history: jax.Array,\n", + " ) -\u003e jax.Array:\n", + " inv_torso_rot = math.quat_inv(pipeline_state.x.rot[0])\n", + " local_rpyrate = math.rotate(pipeline_state.xd.ang[0], inv_torso_rot)\n", "\n", - " inv_base_orientation = math.quat_inv(x.rot[0])\n", - " local_rpyrate = math.rotate(xd.ang[0], inv_base_orientation)\n", - " cmd = state_info['command']\n", + " obs = jp.concatenate([\n", + " jp.array([local_rpyrate[2]]) * 0.25, # yaw rate\n", + " math.rotate(jp.array([0, 0, -1]), inv_torso_rot), # projected gravity\n", + " state_info['command'] * jp.array([2.0, 2.0, 0.25]), # command\n", + " pipeline_state.q[7:] - self._default_pose, # motor angles\n", + " state_info['last_act'], # last action\n", + " ])\n", "\n", - " obs_list = []\n", - " # yaw rate\n", - " obs_list.append(jp.array([local_rpyrate[2]]) * 0.25)\n", - " # projected gravity\n", - " obs_list.append(\n", - " math.rotate(jp.array([0.0, 0.0, -1.0]), inv_base_orientation))\n", - " # command\n", - " obs_list.append(cmd * jp.array([2.0, 2.0, 0.25]))\n", - " # motor angles\n", - " angles = qpos[7:19]\n", - " obs_list.append(angles - self._default_ap_pose)\n", - " # last action\n", - " obs_list.append(state_info['last_act'])\n", - "\n", - " obs = jp.clip(jp.concatenate(obs_list), -100.0, 100.0)\n", - "\n", - " # stack observations through time\n", - " single_obs_size = len(obs)\n", - " state_info['obs_history'] = jp.roll(\n", - " state_info['obs_history'], single_obs_size\n", + " # clip, noise\n", + " obs = jp.clip(obs, -100.0, 100.0) + self._obs_noise * jax.random.uniform(\n", + " state_info['rng'], obs.shape, minval=-1, maxval=1\n", " )\n", - " state_info['obs_history'] = jp.array(\n", - " state_info['obs_history']).at[:single_obs_size].set(obs)\n", - " return state_info['obs_history']\n", + " # stack observations through time\n", + " obs = jp.roll(obs_history, obs.size).at[:obs.size].set(obs)\n", + "\n", + " return obs\n", "\n", " # ------------ reward functions----------------\n", " def _reward_lin_vel_z(self, xd: Motion) -\u003e jax.Array:\n", @@ -1191,12 +1213,14 @@ " return jp.sqrt(jp.sum(jp.square(torques))) + jp.sum(jp.abs(torques))\n", "\n", " def _reward_action_rate(\n", - " self, act: jax.Array, last_act: jax.Array) -\u003e jax.Array:\n", + " self, act: jax.Array, last_act: jax.Array\n", + " ) -\u003e jax.Array:\n", " # Penalize changes in actions\n", " return jp.sum(jp.square(act - last_act))\n", "\n", " def _reward_tracking_lin_vel(\n", - " self, commands: jax.Array, x: Transform, xd: Motion) -\u003e jax.Array:\n", + " self, commands: jax.Array, x: Transform, xd: Motion\n", + " ) -\u003e jax.Array:\n", " # Tracking of linear velocity commands (xy axes)\n", " local_vel = math.rotate(xd.vel[0], math.quat_inv(x.rot[0]))\n", " lin_vel_error = jp.sum(jp.square(commands[:2] - local_vel[:2]))\n", @@ -1206,15 +1230,16 @@ " return lin_vel_reward\n", "\n", " def _reward_tracking_ang_vel(\n", - " self, commands: jax.Array, x: Transform, xd: Motion) -\u003e jax.Array:\n", + " self, commands: jax.Array, x: Transform, xd: Motion\n", + " ) -\u003e jax.Array:\n", " # Tracking of angular velocity commands (yaw)\n", " base_ang_vel = math.rotate(xd.ang[0], math.quat_inv(x.rot[0]))\n", " ang_vel_error = jp.square(commands[2] - base_ang_vel[2])\n", - " return jp.exp(-ang_vel_error/self.reward_config.rewards.tracking_sigma)\n", + " return jp.exp(-ang_vel_error / self.reward_config.rewards.tracking_sigma)\n", "\n", " def _reward_feet_air_time(\n", - " self, air_time: jax.Array, first_contact: jax.Array,\n", - " commands: jax.Array) -\u003e jax.Array:\n", + " self, air_time: jax.Array, first_contact: jax.Array, commands: jax.Array\n", + " ) -\u003e jax.Array:\n", " # Reward air time.\n", " rew_air_time = jp.sum((air_time - 0.1) * first_contact)\n", " rew_air_time *= (\n", @@ -1223,30 +1248,38 @@ " return rew_air_time\n", "\n", " def _reward_stand_still(\n", - " self, commands: jax.Array, joint_angles: jax.Array,\n", - " default_angles: jax.Array) -\u003e jax.Array:\n", + " self,\n", + " commands: jax.Array,\n", + " joint_angles: jax.Array,\n", + " ) -\u003e jax.Array:\n", " # Penalize motion at zero commands\n", - " return jp.sum(jp.abs(joint_angles - default_angles)) * (\n", + " return jp.sum(jp.abs(joint_angles - self._default_pose)) * (\n", " math.normalize(commands[:2])[1] \u003c 0.1\n", " )\n", "\n", - " def _get_feet_pos_vel(\n", - " self, x: Transform, xd: Motion) -\u003e Tuple[jax.Array, jax.Array]:\n", - " offset = Transform.create(pos=self._feet_pos)\n", - " pos = x.take(self._feet_index).vmap().do(offset).pos\n", - " world_offset = Transform.create(pos=pos - x.take(self._feet_index).pos)\n", - " vel = world_offset.vmap().do(xd.take(self._feet_index)).vel\n", - " return pos, vel\n", - "\n", " def _reward_foot_slip(\n", - " self, x: Transform, xd: Motion, contact_filt: jax.Array) -\u003e jax.Array:\n", - " # Get feet velocities\n", - " _, foot_world_vel = self._get_feet_pos_vel(x, xd)\n", - " # Penalize large feet velocity for feet that are in contact with the ground.\n", - " return jp.sum(\n", - " jp.square(foot_world_vel[:, :2]) * contact_filt.reshape((-1, 1))\n", - " )\n", + " self, pipeline_state: base.State, contact_filt: jax.Array\n", + " ) -\u003e jax.Array:\n", + " # get velocities at feet which are offset from lower legs\n", + " # pytype: disable=attribute-error\n", + " pos = pipeline_state.data.site_xpos[self._feet_site_id] # feet position\n", + " feet_offset = pos - pipeline_state.data.xpos[self._lower_leg_body_id]\n", + " # pytype: enable=attribute-error\n", + " offset = base.Transform.create(pos=feet_offset)\n", + " foot_indices = self._lower_leg_body_id - 1 # we got rid of the world body\n", + " foot_vel = offset.vmap().do(pipeline_state.xd.take(foot_indices)).vel\n", "\n", + " # Penalize large feet velocity for feet that are in contact with the ground.\n", + " return jp.sum(jp.square(foot_vel[:, :2]) * contact_filt.reshape((-1, 1)))\n", + "\n", + " def _reward_termination(self, done: jax.Array, step: jax.Array) -\u003e jax.Array:\n", + " return done \u0026 (step \u003c 500)\n", + "\n", + " def render(\n", + " self, trajectory: List[base.State], camera: str | None = None\n", + " ) -\u003e Sequence[np.ndarray]:\n", + " camera = camera or 'track'\n", + " return super().render(trajectory, camera)\n", "\n", "envs.register_environment('barkour', BarkourEnv)" ] @@ -1260,10 +1293,7 @@ "outputs": [], "source": [ "env_name = 'barkour'\n", - "env = envs.get_environment(env_name)\n", - "\n", - "# re-instantiate the renderer\n", - "renderer = mujoco.Renderer(env.model)" + "env = envs.get_environment(env_name)" ] }, { @@ -1274,7 +1304,7 @@ "source": [ "## Train Policy\n", "\n", - "To train a policy with domain randomization, we pass in the domain randomization function into the brax train function; brax will call the domain randomization function when rolling out episodes. Training the quadruped takes about 14 minutes on a Tesla V100 GPU." + "To train a policy with domain randomization, we pass in the domain randomization function into the brax train function; brax will call the domain randomization function when rolling out episodes. Training the quadruped takes 8-9 minutes on a Tesla A100 GPU." ] }, { @@ -1289,22 +1319,19 @@ " ppo_networks.make_ppo_networks,\n", " policy_hidden_layer_sizes=(128, 128, 128, 128))\n", "train_fn = functools.partial(\n", - " ppo.train,\n", - " num_timesteps=60_000_000, num_evals=3, reward_scaling=1,\n", - " episode_length=1000, normalize_observations=True,\n", - " action_repeat=1, unroll_length=20, num_minibatches=8, gae_lambda=0.95,\n", - " num_updates_per_batch=4, discounting=0.99, learning_rate=3e-4,\n", - " entropy_cost=1e-2, num_envs=8192, batch_size=1024,\n", + " ppo.train, num_timesteps=100_000_000, num_evals=10,\n", + " reward_scaling=1, episode_length=1000, normalize_observations=True,\n", + " action_repeat=1, unroll_length=20, num_minibatches=32,\n", + " num_updates_per_batch=4, discounting=0.97, learning_rate=3.0e-4,\n", + " entropy_cost=1e-2, num_envs=8192, batch_size=256,\n", " network_factory=make_networks_factory,\n", - " num_resets_per_eval=10,\n", " randomization_fn=domain_randomize, seed=0)\n", "\n", - "\n", "x_data = []\n", "y_data = []\n", "ydataerr = []\n", "times = [datetime.now()]\n", - "max_y, min_y = 30, 0\n", + "max_y, min_y = 40, 0\n", "\n", "# Reset environments since internals may be overwritten by tracers from the\n", "# domain randomization function.\n", @@ -1368,7 +1395,6 @@ }, "outputs": [], "source": [ - "\n", "# @markdown Commands **only used for Barkour Env**:\n", "x_vel = 1.0 #@param {type: \"number\"}\n", "y_vel = 0.0 #@param {type: \"number\"}\n", @@ -1380,8 +1406,7 @@ "rng = jax.random.PRNGKey(0)\n", "state = jit_reset(rng)\n", "state.info['command'] = the_command\n", - "rollout = [state]\n", - "images = [get_image(state, camera='track')]\n", + "rollout = [state.pipeline_state]\n", "\n", "# grab a trajectory\n", "n_steps = 500\n", @@ -1391,11 +1416,31 @@ " act_rng, rng = jax.random.split(rng)\n", " ctrl, _ = jit_inference_fn(state.obs, act_rng)\n", " state = jit_step(state, ctrl)\n", - " rollout.append(state)\n", - " if i % render_every == 0:\n", - " images.append(get_image(state, camera='track'))\n", + " rollout.append(state.pipeline_state)\n", "\n", - "media.show_video(images, fps=1.0 / eval_env.dt / render_every)" + "media.show_video(\n", + " eval_env.render(rollout[::render_every], camera='track'),\n", + " fps=1.0 / eval_env.dt / render_every)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aD6H6WD0915X" + }, + "source": [ + "We can also render the rollout using the Brax renderer." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "V7jqv08X95u4" + }, + "outputs": [], + "source": [ + "HTML(html.render(eval_env.brax_sys, rollout))" ] } ], @@ -1403,12 +1448,13 @@ "accelerator": "GPU", "colab": { "gpuClass": "premium", - "gpuType": "V100", + "gpuType": "A100", + "machine_shape": "hm", "private_outputs": true, "provenance": [ { - "file_id": "1QsuS7EJhdPEHxxAu9XwozvA7eb4ZnlAb", - "timestamp": 1701993737024 + "file_id": "11cFRVCJ8Kn71tlQFbFcw4JzQZ00F8BRG", + "timestamp": 1704355889284 } ], "toc_visible": true From ca023a6f2855ab627f5f9a81e4ec011a3f6b1278 Mon Sep 17 00:00:00 2001 From: Nimrod Gileadi Date: Fri, 5 Jan 2024 06:49:51 -0800 Subject: [PATCH 098/121] Update EGL init code for Python 3.12. Fixes google-deepmind/mujoco#1299. PiperOrigin-RevId: 595982512 Change-Id: Ib0261483a7f6cb8addf04f5010f09d3554c8bf45 --- python/mujoco/egl/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/python/mujoco/egl/__init__.py b/python/mujoco/egl/__init__.py index d695ddc1..045b2aff 100644 --- a/python/mujoco/egl/__init__.py +++ b/python/mujoco/egl/__init__.py @@ -92,12 +92,13 @@ class GLContext: del max_width, max_height # unused num_configs = ctypes.c_long() config_size = 1 - config = EGL.EGLConfig() + # ctypes syntax for making an array of length config_size. + configs = (EGL.EGLConfig * config_size)() EGL.eglReleaseThread() EGL.eglChooseConfig( EGL_DISPLAY, EGL_ATTRIBUTES, - ctypes.byref(config), + configs, config_size, num_configs) if num_configs.value < 1: @@ -106,7 +107,7 @@ class GLContext: 'desired attributes: {}'.format(EGL_ATTRIBUTES)) EGL.eglBindAPI(EGL.EGL_OPENGL_API) self._context = EGL.eglCreateContext( - EGL_DISPLAY, config, EGL.EGL_NO_CONTEXT, None) + EGL_DISPLAY, configs[0], EGL.EGL_NO_CONTEXT, None) if not self._context: raise RuntimeError('Cannot create an EGL context.') From c59a33dd81bed232e06f6e927b3cfee7cc3199a6 Mon Sep 17 00:00:00 2001 From: Nimrod Gileadi Date: Fri, 5 Jan 2024 07:19:54 -0800 Subject: [PATCH 099/121] Use std::string_view instead of absl::string_view in fixture.cc. Clean up some lint warnings. Fixes #1306. PiperOrigin-RevId: 595988916 Change-Id: I0e8584ff8704ea7294cf649664ab447cb1d4ff46 --- test/CMakeLists.txt | 1 - test/fixture.cc | 9 ++++++--- test/fixture.h | 12 +++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3c70fce1..046bdadf 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -57,7 +57,6 @@ target_compile_definitions(fixture PUBLIC MJSTATIC) target_link_libraries( fixture PUBLIC absl::core_headers - absl::strings absl::synchronization gtest gmock diff --git a/test/fixture.cc b/test/fixture.cc index 4c451b65..4fdad189 100644 --- a/test/fixture.cc +++ b/test/fixture.cc @@ -14,11 +14,14 @@ #include "test/fixture.h" +#include #include #include #include -#include +#include +#include #include +#include #include #include @@ -107,7 +110,7 @@ mjModel* LoadModelFromPath(const char* model_path) { return model; } -const std::string GetFileContents(const char* path) { +std::string GetFileContents(const char* path) { std::ifstream ifs; ifs.open(path, std::ifstream::in); EXPECT_FALSE(ifs.fail()); @@ -116,7 +119,7 @@ const std::string GetFileContents(const char* path) { return sstream.str(); } -const std::string SaveAndReadXml(const mjModel* model) { +std::string SaveAndReadXml(const mjModel* model) { EXPECT_THAT(model, testing::NotNull()); constexpr int kMaxPathLen = 1024; diff --git a/test/fixture.h b/test/fixture.h index cdf097ec..6091a2a5 100644 --- a/test/fixture.h +++ b/test/fixture.h @@ -17,13 +17,11 @@ #include #include -#include #include +#include #include #include -#include -#include #include #include @@ -80,14 +78,14 @@ auto MjuErrorMessageFrom(Return (*func)(Args...)) { } // Returns a path to a data file, under the mujoco/test directory. -const std::string GetTestDataFilePath(absl::string_view path); +const std::string GetTestDataFilePath(std::string_view path); // Returns a path to a data file, under the mujoco/model directory. -const std::string GetModelPath(absl::string_view path); +const std::string GetModelPath(std::string_view path); // Returns a newly-allocated mjModel, loaded from the contents of xml. // On failure returns nullptr and populates the error array if present. -mjModel* LoadModelFromString(absl::string_view xml, char* error = nullptr, +mjModel* LoadModelFromString(std::string_view xml, char* error = nullptr, int error_size = 0, mjVFS* vfs = nullptr); // Returns a newly-allocated mjModel, loaded from the contents in model_path. @@ -95,7 +93,7 @@ mjModel* LoadModelFromString(absl::string_view xml, char* error = nullptr, mjModel* LoadModelFromPath(const char* model_path); // Returns a string loaded from first saving the model given an input. -const std::string SaveAndReadXml(const mjModel* model); +std::string SaveAndReadXml(const mjModel* model); // Adds control noise. std::vector GetCtrlNoise(const mjModel* m, int nsteps, From feb92bf535683004e85dbf10fa3427c30dbb29d3 Mon Sep 17 00:00:00 2001 From: Erik Frey Date: Fri, 5 Jan 2024 08:41:20 -0800 Subject: [PATCH 100/121] MJX ray for planes, spheres, capsules, and boxes. PiperOrigin-RevId: 596004453 Change-Id: I6c15268f7ec6afc1aadd370a78ef225ab9c5fd04 --- doc/changelog.rst | 4 +- mjx/mujoco/mjx/__init__.py | 1 + mjx/mujoco/mjx/_src/io.py | 2 +- mjx/mujoco/mjx/_src/ray.py | 180 +++++++++++++++++++++++++++++++ mjx/mujoco/mjx/_src/ray_test.py | 149 +++++++++++++++++++++++++ mjx/mujoco/mjx/_src/test_util.py | 1 + mjx/mujoco/mjx/test_data/ray.xml | 16 +++ 7 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 mjx/mujoco/mjx/_src/ray.py create mode 100644 mjx/mujoco/mjx/_src/ray_test.py create mode 100644 mjx/mujoco/mjx/test_data/ray.xml diff --git a/doc/changelog.rst b/doc/changelog.rst index 0b89de4b..9665e7a5 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -9,10 +9,12 @@ MJX ^^^ 1. Added :ref:`dyntype` ``filterexact``. 2. Added :at:`site` transmission. +3. Updated MJX colab tutorial with more stable quadruped environment. +4. Added ``mjx.ray`` which mirrors :ref:`mj_ray` for planes, spheres, capsules, and boxes. Bug fixes ^^^^^^^^^ -3. Fixed a bug that prevented the use of pins with plugins if flexes are not in the worldbody. Fixes :github:issue:`1270`. +5. Fixed a bug that prevented the use of pins with plugins if flexes are not in the worldbody. Fixes :github:issue:`1270`. Version 3.1.1 (December 18, 2023) diff --git a/mjx/mujoco/mjx/__init__.py b/mjx/mujoco/mjx/__init__.py index d8e3f437..f4953c60 100644 --- a/mjx/mujoco/mjx/__init__.py +++ b/mjx/mujoco/mjx/__init__.py @@ -33,6 +33,7 @@ from mujoco.mjx._src.io import make_data from mujoco.mjx._src.io import put_data from mujoco.mjx._src.io import put_model from mujoco.mjx._src.passive import passive +from mujoco.mjx._src.ray import ray from mujoco.mjx._src.smooth import com_pos from mujoco.mjx._src.smooth import com_vel from mujoco.mjx._src.smooth import crb diff --git a/mjx/mujoco/mjx/_src/io.py b/mjx/mujoco/mjx/_src/io.py index 788c3899..d7ae4a1d 100644 --- a/mjx/mujoco/mjx/_src/io.py +++ b/mjx/mujoco/mjx/_src/io.py @@ -333,7 +333,7 @@ def put_data(m: mujoco.MjModel, d: mujoco.MjData, device=None) -> types.Data: efc_j[i, d.efc_J_colind[rowadr + j]] = fields['efc_J'][rowadr + j] fields['efc_J'] = efc_j else: - fields['efc_J'] = fields['efc_J'].reshape((-1, m.nv)) + fields['efc_J'] = fields['efc_J'].reshape((-1 if m.nv else 0, m.nv)) for fname in ('efc_J', 'efc_frictionloss', 'efc_D', 'efc_aref', 'efc_force'): value = np.zeros((nefc, m.nv)) if fname == 'efc_J' else np.zeros(nefc) diff --git a/mjx/mujoco/mjx/_src/ray.py b/mjx/mujoco/mjx/_src/ray.py new file mode 100644 index 00000000..582d6e4f --- /dev/null +++ b/mjx/mujoco/mjx/_src/ray.py @@ -0,0 +1,180 @@ +# 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. +# ============================================================================== +"""Functions for ray interesection testing.""" + +from typing import Tuple + +import jax +from jax import numpy as jp +import mujoco +# pylint: disable=g-importing-member +from mujoco.mjx._src.types import Data +from mujoco.mjx._src.types import GeomType +from mujoco.mjx._src.types import Model +# pylint: enable=g-importing-member +import numpy as np + + +def _ray_quad( + a: jax.Array, b: jax.Array, c: jax.Array +) -> Tuple[jax.Array, jax.Array]: + """Returns two solutions for quadratic: a*x^2 + 2*b*x + c = 0.""" + det = b * b - a * c + det_2 = jp.sqrt(det) + + x0, x1 = (-b - det_2) / a, (-b + det_2) / a + x0 = jp.where((det < mujoco.mjMINVAL) | (x0 < 0), jp.inf, x0) + x1 = jp.where((det < mujoco.mjMINVAL) | (x1 < 0), jp.inf, x1) + + return x0, x1 + + +def _ray_plane( + size: jax.Array, + pnt: jax.Array, + vec: jax.Array, +) -> jax.Array: + """Returns the distance at which a ray intersects with a plane.""" + x = -pnt[2] / vec[2] + + valid = vec[2] <= -mujoco.mjMINVAL # z-vec pointing towards front face + valid &= x >= 0 + # only within rendered rectangle + p = pnt[0:2] + x * vec[0:2] + valid &= jp.all((size[0:2] <= 0) | (jp.abs(p) <= size[0:2])) + + return jp.where(valid, x, jp.inf) + + +def _ray_sphere( + size: jax.Array, + pnt: jax.Array, + vec: jax.Array, +) -> jax.Array: + """Returns the distance at which a ray intersects with a sphere.""" + x0, x1 = _ray_quad(vec @ vec, vec @ pnt, pnt @ pnt - size[0] * size[0]) + x = jp.where(jp.isinf(x0), x1, x0) + + return x + + +def _ray_capsule( + size: jax.Array, + pnt: jax.Array, + vec: jax.Array, +) -> jax.Array: + """Returns the distance at which a ray intersects with a capsule.""" + + # cylinder round side: (x*lvec+lpnt)'*(x*lvec+lpnt) = size[0]*size[0] + a = vec[0:2] @ vec[0:2] + b = vec[0:2] @ pnt[0:2] + c = pnt[0:2] @ pnt[0:2] - size[0] * size[0] + + # solve a*x^2 + 2*b*x + c = 0 + x0, x1 = _ray_quad(a, b, c) + x = jp.where(jp.isinf(x0), x1, x0) + + # make sure round solution is between flat sides + x = jp.where(jp.abs(pnt[2] + x * vec[2]) <= size[1], x, jp.inf) + + # top cap + dif = pnt - jp.array([0, 0, size[1]]) + x0, x1 = _ray_quad(vec @ vec, vec @ dif, dif @ dif - size[0] * size[0]) + # accept only top half of sphere + x = jp.where((pnt[2] + x0 * vec[2] >= size[1]) & (x0 < x), x0, x) + x = jp.where((pnt[2] + x1 * vec[2] >= size[1]) & (x1 < x), x1, x) + + # bottom cap + dif = pnt + jp.array([0, 0, size[1]]) + x0, x1 = _ray_quad(vec @ vec, vec @ dif, dif @ dif - size[0] * size[0]) + + # accept only bottom half of sphere + x = jp.where((pnt[2] + x0 * vec[2] <= -size[1]) & (x0 < x), x0, x) + x = jp.where((pnt[2] + x1 * vec[2] <= -size[1]) & (x1 < x), x1, x) + + return x + + +def _ray_box( + size: jax.Array, + pnt: jax.Array, + vec: jax.Array, +) -> jax.Array: + """Returns the distance at which a ray intersects with a box.""" + + iface = jp.array([(1, 2), (0, 2), (0, 1), (1, 2), (0, 2), (0, 1)]) + + # side +1, -1 + # solution of pnt[i] + x * vec[i] = side * size[i] + x = jp.concatenate([(size - pnt) / vec, (-size - pnt) / vec]) + + # intersection with face + p0 = pnt[iface[:, 0]] + x * vec[iface[:, 0]] + p1 = pnt[iface[:, 1]] + x * vec[iface[:, 1]] + valid = jp.abs(p0) <= size[iface[:, 0]] + valid &= jp.abs(p1) <= size[iface[:, 1]] + + return jp.min(jp.where(valid, x, jp.inf)) + + +def _ray_mesh( + size: jax.Array, + pnt: jax.Array, + vec: jax.Array, +) -> jax.Array: + """Returns the distance at which a ray intersects with a mesh.""" + del size, pnt, vec + raise NotImplementedError("ray <> mesh not implemented yet") + + +_RAY_FUNC = { + GeomType.PLANE: _ray_plane, + GeomType.SPHERE: _ray_sphere, + GeomType.CAPSULE: _ray_capsule, + GeomType.BOX: _ray_box, + # GeomType.MESH: _ray_mesh, +} + + +def ray( + m: Model, d: Data, pnt: jax.Array, vec: jax.Array +) -> Tuple[jax.Array, jax.Array]: + """Returns the geom id and distance at which a ray intersects with a geom.""" + + ids = [] + dists = [] + + # map ray to local geom frames + geom_pnts = jax.vmap(lambda x, y: x.T @ (pnt - y))(d.geom_xmat, d.geom_xpos) + geom_vecs = jax.vmap(lambda x: x.T @ vec)(d.geom_xmat) + + for geom_type, fn in _RAY_FUNC.items(): + if not np.any(m.geom_type == geom_type): + continue + + geom_ids = jp.array(np.nonzero(m.geom_type == geom_type)[0]) + geom_dists = jax.vmap(fn)( + m.geom_size[geom_ids], geom_pnts[geom_ids], geom_vecs[geom_ids] + ) + ids.append(geom_ids) + dists.append(geom_dists) + + ids = jp.concatenate(ids) + dists = jp.concatenate(dists) + min_id = jp.argmin(dists) + id_ = jp.where(jp.isinf(dists[min_id]), -1, ids[min_id]) + dist = jp.where(jp.isinf(dists[min_id]), -1, dists[min_id]) + + return id_, dist diff --git a/mjx/mujoco/mjx/_src/ray_test.py b/mjx/mujoco/mjx/_src/ray_test.py new file mode 100644 index 00000000..712aca63 --- /dev/null +++ b/mjx/mujoco/mjx/_src/ray_test.py @@ -0,0 +1,149 @@ +# 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 ray functions.""" + +from absl.testing import absltest +import jax +from jax import numpy as jp +import mujoco +from mujoco import mjx +from mujoco.mjx._src import test_util +import numpy as np + +# tolerance for difference between MuJoCo and MJX ray calculations - mostly +# due to float precision +_TOLERANCE = 5e-5 + + +def _assert_eq(a, b, name): + tol = _TOLERANCE * 10 # avoid test noise + err_msg = f'mismatch: {name}' + np.testing.assert_allclose(a, b, err_msg=err_msg, atol=tol, rtol=tol) + + +class RayTest(absltest.TestCase): + + def test_ray_nothing(self): + """Tests that MJX ray returns -1 when nothing is hit.""" + m = test_util.load_test_file('ray.xml') + d = mujoco.MjData(m) + mujoco.mj_forward(m, d) + mx, dx = mjx.put_model(m), mjx.put_data(m, d) + + pnt, vec = jp.array([12.146, 1.865, 3.895]), jp.array([0, 0, -1.0]) + geomid, dist = jax.jit(mjx.ray)(mx, dx, pnt, vec) + _assert_eq(geomid, jp.array([-1]), 'geom_id') + _assert_eq(dist, jp.array([-1]), 'dist') + + def test_ray_plane(self): + """Tests MJX ray<>plane matches MuJoCo.""" + m = test_util.load_test_file('ray.xml') + d = mujoco.MjData(m) + mujoco.mj_forward(m, d) + mx, dx = mjx.put_model(m), mjx.put_data(m, d) + + # looking down at a slight angle + pnt, vec = jp.array([2, 1, 3.0]), jp.array([0.1, 0.2, -1.0]) + vec /= jp.linalg.norm(vec) + geomid, dist = jax.jit(mjx.ray)(mx, dx, pnt, vec) + _assert_eq(geomid, jp.array([0]), 'geom_id') + pnt, vec, unused = np.array(pnt), np.array(vec), np.zeros(1, dtype=np.int32) + mj_dist = mujoco.mj_ray(m, d, pnt, vec, None, 1, -1, unused) + _assert_eq(dist, mj_dist, 'dist') + + # looking on wrong side of plane + pnt = jp.array([0, 0, -0.5]) + geomid, dist = jax.jit(mjx.ray)(mx, dx, pnt, vec) + _assert_eq(geomid, jp.array([-1]), 'geom_id') + _assert_eq(dist, jp.array([-1]), 'dist') + + def test_ray_sphere(self): + """Tests MJX ray<>sphere matches MuJoCo.""" + m = test_util.load_test_file('ray.xml') + d = mujoco.MjData(m) + mujoco.mj_forward(m, d) + mx, dx = mjx.put_model(m), mjx.put_data(m, d) + + # looking down at sphere at a slight angle + pnt, vec = jp.array([0, 0, 1.6]), jp.array([0.1, 0.2, -1.0]) + vec /= jp.linalg.norm(vec) + geomid, dist = jax.jit(mjx.ray)(mx, dx, pnt, vec) + _assert_eq(geomid, jp.array([1]), 'geom_id') + pnt, vec, unused = np.array(pnt), np.array(vec), np.zeros(1, dtype=np.int32) + mj_dist = mujoco.mj_ray(m, d, pnt, vec, None, 1, -1, unused) + _assert_eq(dist, mj_dist, 'dist') + + def test_ray_capsule(self): + """Tests MJX ray<>capsule matches MuJoCo.""" + m = test_util.load_test_file('ray.xml') + d = mujoco.MjData(m) + mujoco.mj_forward(m, d) + mx, dx = mjx.put_model(m), mjx.put_data(m, d) + + # looking down at capsule at a slight angle + pnt, vec = jp.array([0.5, 1, 1.6]), jp.array([0, 0.05, -1.0]) + vec /= jp.linalg.norm(vec) + geomid, dist = jax.jit(mjx.ray)(mx, dx, pnt, vec) + _assert_eq(geomid, jp.array([2]), 'geom_id') + pnt, vec, unused = np.array(pnt), np.array(vec), np.zeros(1, dtype=np.int32) + mj_dist = mujoco.mj_ray(m, d, pnt, vec, None, 1, -1, unused) + _assert_eq(dist, mj_dist, 'dist') + + # looking up at capsule from below + pnt, vec = jp.array([-0.5, 1, 0.05]), jp.array([0, 0.05, 1.0]) + vec /= jp.linalg.norm(vec) + geomid, dist = jax.jit(mjx.ray)(mx, dx, pnt, vec) + _assert_eq(geomid, jp.array([2]), 'geom_id') + pnt, vec, unused = np.array(pnt), np.array(vec), np.zeros(1, dtype=np.int32) + mj_dist = mujoco.mj_ray(m, d, pnt, vec, None, 1, -1, unused) + _assert_eq(dist, mj_dist, 'dist') + + # looking at cylinder of capsule from the side + pnt, vec = jp.array([0, 1, 0.75]), jp.array([1, 0, 0]) + vec /= jp.linalg.norm(vec) + geomid, dist = jax.jit(mjx.ray)(mx, dx, pnt, vec) + _assert_eq(geomid, jp.array([2]), 'geom_id') + pnt, vec, unused = np.array(pnt), np.array(vec), np.zeros(1, dtype=np.int32) + mj_dist = mujoco.mj_ray(m, d, pnt, vec, None, 1, -1, unused) + _assert_eq(dist, mj_dist, 'dist') + + def test_ray_box(self): + """Tests MJX ray<>box matches MuJoCo.""" + m = test_util.load_test_file('ray.xml') + d = mujoco.MjData(m) + mujoco.mj_forward(m, d) + mx, dx = mjx.put_model(m), mjx.put_data(m, d) + + # looking down at box at a slight angle + pnt, vec = jp.array([1, 0, 1.6]), jp.array([0, 0.05, -1.0]) + vec /= jp.linalg.norm(vec) + geomid, dist = jax.jit(mjx.ray)(mx, dx, pnt, vec) + _assert_eq(geomid, jp.array([3]), 'geom_id') + pnt, vec, unused = np.array(pnt), np.array(vec), np.zeros(1, dtype=np.int32) + mj_dist = mujoco.mj_ray(m, d, pnt, vec, None, 1, -1, unused) + _assert_eq(dist, mj_dist, 'dist') + + # looking up at box from below + pnt, vec = jp.array([1, 0, 0.05]), jp.array([0, 0.05, 1.0]) + vec /= jp.linalg.norm(vec) + geomid, dist = jax.jit(mjx.ray)(mx, dx, pnt, vec) + _assert_eq(geomid, jp.array([3]), 'geom_id') + pnt, vec, unused = np.array(pnt), np.array(vec), np.zeros(1, dtype=np.int32) + mj_dist = mujoco.mj_ray(m, d, pnt, vec, None, 1, -1, unused) + _assert_eq(dist, mj_dist, 'dist') + + +if __name__ == '__main__': + absltest.main() diff --git a/mjx/mujoco/mjx/_src/test_util.py b/mjx/mujoco/mjx/_src/test_util.py index ab27c2b1..d350c11a 100644 --- a/mjx/mujoco/mjx/_src/test_util.py +++ b/mjx/mujoco/mjx/_src/test_util.py @@ -26,6 +26,7 @@ TEST_FILES: List[str] = [ 'constraints.xml', 'convex.xml', 'pendula.xml', + 'ray.xml', ] _ACTUATOR_TYPES = ['motor', 'velocity', 'position', 'general', 'intvelocity'] diff --git a/mjx/mujoco/mjx/test_data/ray.xml b/mjx/mujoco/mjx/test_data/ray.xml new file mode 100644 index 00000000..a6424ec4 --- /dev/null +++ b/mjx/mujoco/mjx/test_data/ray.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + From c79e1e5d01c4ee03055a57c0b51d6e219cfde272 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Sat, 6 Jan 2024 14:01:27 -0800 Subject: [PATCH 101/121] Add documentation image variants compatible with dark mode. Fixes #587 PiperOrigin-RevId: 596265596 Change-Id: Iedd076fbb7a6dca77a91e52e538be1c764d1f8fb --- doc/computation/fluid.rst | 8 +- doc/computation/index.rst | 20 +- doc/images/computation/contact_frame_dark.svg | 296 ++++++++++++++++++ doc/images/computation/gPGS_dark.svg | 102 ++++++ .../computation/kutta_cond_plate_dark.svg | 124 ++++++++ doc/images/computation/softcontact_dark.png | Bin 0 -> 60000 bytes doc/images/modeling/flexelem.png | Bin 92767 -> 74718 bytes doc/images/modeling/impedance_dark.png | Bin 0 -> 109383 bytes doc/images/modeling/musclemodel_dark.png | Bin 0 -> 439053 bytes doc/images/modeling/musclerange_dark.png | Bin 0 -> 30679 bytes doc/modeling.rst | 42 ++- 11 files changed, 578 insertions(+), 14 deletions(-) create mode 100644 doc/images/computation/contact_frame_dark.svg create mode 100644 doc/images/computation/gPGS_dark.svg create mode 100644 doc/images/computation/kutta_cond_plate_dark.svg create mode 100644 doc/images/computation/softcontact_dark.png create mode 100644 doc/images/modeling/impedance_dark.png create mode 100644 doc/images/modeling/musclemodel_dark.png create mode 100644 doc/images/modeling/musclerange_dark.png diff --git a/doc/computation/fluid.rst b/doc/computation/fluid.rst index 070cb7b3..ea6f0569 100644 --- a/doc/computation/fluid.rst +++ b/doc/computation/fluid.rst @@ -520,8 +520,14 @@ in the surrounding flow a circulation of sufficient strength to hold the rear st This is the Kutta condition, a fluid dynamic phenomenon that can be observed for solid bodies with sharp corners, such as slender bodies or the trailing edges of airfoils. -.. cssclass:: caption-small .. figure:: ../images/computation/kutta_cond_plate.svg + :class: only-light + :figwidth: 95% + :align: left + +.. cssclass:: caption-small +.. figure:: ../images/computation/kutta_cond_plate_dark.svg + :class: only-dark :figwidth: 95% :align: left diff --git a/doc/computation/index.rst b/doc/computation/index.rst index 190be5ac..9e7dcedc 100644 --- a/doc/computation/index.rst +++ b/doc/computation/index.rst @@ -966,6 +966,12 @@ is :math:`E f`. The matrix of basis vectors is constructed as follows. .. image:: ../images/computation/contact_frame.svg :width: 700px :align: center + :class: only-light + +.. image:: ../images/computation/contact_frame_dark.svg + :width: 700px + :align: center + :class: only-dark The figure illustrates the full basis set corresponding to the case :math:`n = 6`. Otherwise we use only the first :math:`n` or :math:`2(n-1)` columns depending on the cone type. Elliptic cones are easier to understand. Since the @@ -1325,6 +1331,12 @@ representations of the constraint Jacobian and related matrices. .. image:: ../images/computation/gPGS.svg :width: 500px :align: center + :class: only-light + + .. image:: ../images/computation/gPGS_dark.svg + :width: 500px + :align: center + :class: only-dark When using pyramidal friction cones, the problem involves box constraints to which PGS has traditionally been applied. If we applied PGS directly to the conic constraints resulting from elliptic friction cones, it would get @@ -1424,12 +1436,18 @@ approximations, no matter how accurate the approximation is. The figure below il where the pyramid is not even an approximation, but represents the same constraint set as the elliptic cone. We plot the contours of the penalty/shadow for the pyramidal (red) and elliptic (dashed blue) cones, for different friction coefficients varying from left to right. Mathematically, the penalty in the pyramidal case is a quadratic spline, while -the penalty in the elliptic case contains pieces that are quadratics minus square roots of quadratics - allowing +the penalty in the elliptic case contains pieces that are quadratics minus square roots of quadratics -- allowing circular contours around the tip of the cone. .. image:: ../images/computation/softcontact.png :width: 600px :align: center + :class: only-light + +.. image:: ../images/computation/softcontact_dark.png + :width: 600px + :align: center + :class: only-dark In summary, elliptic and pyramidal friction cones define different soft-contact dynamics (although they are usually very close). The elliptic model is more principled and more consistent with physical intuition, and the corresponding solvers diff --git a/doc/images/computation/contact_frame_dark.svg b/doc/images/computation/contact_frame_dark.svg new file mode 100644 index 00000000..ea67cb4f --- /dev/null +++ b/doc/images/computation/contact_frame_dark.svg @@ -0,0 +1,296 @@ + + + + + + + + + + e + 1 + + + e + 2 + + + e + 3 + + + + + + + + + + + + + + + e + 4 + + + e + 5 + + + e + 6 + + x + y + z + + + + + + + + + + + + + + + e + 1 + + + e + 2 + + + e + 3 + + + e + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + elliptic basis: E = I + 6 + + pyramidal basis: E = + + + + + + + + + + + e + 5 + + + + + + + + + + + + + e + 6 + + + + + + + + + + + + + e + 7 + + + + + + + + + + + + + e + 8 + + + + + + + + + + + + + e + 9 + + + + + + + + + + + + + e + 10 + + + 1 + + +m + 1 + + 0 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 0 + + -m + 1 + + 1 + + +m + 2 + + 0 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 0 + + -m + 2 + + 1 + + +m + 5 + + 0 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 0 + + -m + 5 + + ... + diff --git a/doc/images/computation/gPGS_dark.svg b/doc/images/computation/gPGS_dark.svg new file mode 100644 index 00000000..7a8cca57 --- /dev/null +++ b/doc/images/computation/gPGS_dark.svg @@ -0,0 +1,102 @@ + + + + + + + + + + cone + constraint + + + unconstrained + minimum + + + + + + + + + + continuum of + PGS local minima + + + + + + + search + ray + + + search + ellipsoid + + + + + + + + diff --git a/doc/images/computation/kutta_cond_plate_dark.svg b/doc/images/computation/kutta_cond_plate_dark.svg new file mode 100644 index 00000000..18ea52df --- /dev/null +++ b/doc/images/computation/kutta_cond_plate_dark.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + v + + + + + y + x + + + + + + + + α + + + diff --git a/doc/images/computation/softcontact_dark.png b/doc/images/computation/softcontact_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..f10cf3f9a67f2dfeff9bec27c000c4c1115aa447 GIT binary patch literal 60000 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 Cc;Alz literal 0 HcmV?d00001 diff --git a/doc/images/modeling/flexelem.png b/doc/images/modeling/flexelem.png index 39df7463c0ee2348cd8582d9ef9fe1a3f4e82b42..74dbe6a82bb67518a6efac42eca997b636f7cf59 100644 GIT binary patch literal 74718 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 literal 92767 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 diff --git a/doc/images/modeling/impedance_dark.png b/doc/images/modeling/impedance_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..ecb40fd76aa151329eb00c319c5d351e32b05c39 GIT binary patch literal 109383 zcmeFZgf{_St90+G}0wT5Hc66=m5Qc$9c(XlOU&<)qcn(6HFi z&@icRu)rBC%!&)}foUP3B!PzZ3F;p$R9Tp^-a&u2+2wE)<#R$eSxEp|OHv95gKSTWA>I2p#;Rp;MxvY6C}TTIhHG z>(~gL`R_6iaFGWMyrWTpmwC7q8s^{SgTN>1&jh?s_y74+lF(3;kYuOTur#ApGIg=k zc5t$BW~Y_6f5C}{h5<+a?V+Q5 zMtB!IY1B$X$3;g;QTT;}9jEC_hv#OT9(InXyU-qc2!lgAGZ#}@4?A0XXJHRf`adOv z!7=JI7d`EtA}%(f^g2o^v{DXEX0#7ExjDJ%#qelpX&*biG#6Hre)9M2;F~DDrHhNB zFc+7*yE~^lAE$$p1s9KykPsI)FBdN_2Pnbe>}l^}>cL^}eE+XX{;5aW%=v|rm7|N5 zgFP*(Ueo6et}deV^r#2@*WX|5bg?r3?e(AocL-~YJF z!_@JwoBs6buZKNGRVDn)$;u2g7u6^+p2vTx{~zc6yK#>}?ZQ${W~MF zwU4>}@4|oA^Oy_O^Z&<4{58yf&Vq3k!vlBzGiPFWi?Tb1XlO7rd1(m^5A>}WT*rGB zzCZUE$h0J>BEBckJ_x3x!g|>iGe{w)R7-$E!7OEpmsm-)kk^$T)E{TM-vS-? zU)l;ve8%_>Z9p~dLp>UL#BfCb=KtQzzuJ$E-}@g1^v{#!3~ z4hFc8#R+SnKV+vx|ChGHT7sPaZOGNgX-z(Qk$ph>7YU#>X%+gnA)U0%TZHW||z4Q1WohU^l>hk;;`K%%WTHDv&htAoi56hy8TYJU}T@T zxW{fVjlsmQq5BN!#g>IQz7aBM46|m|%O*3q^bGk~TT}fDJxa>)1H;8H7FyTVEO+x{ zMXh%zk>Go}lifs}vjsOle@w)Zr3^ibIRCTQeJLdS#4Xf40)bq9dl9W9skCC>gJUeQC`BrU zCdpY@-On=W#b^^vR=CeHXvN9wag+OE)A`O*_9uDoUJWS;s)n6p68F(sU-znn<;@iR zXzodTs>0%LqWAe%!mW3f6GGe~B6r#NiOb65_f>RM<})YPYnm_Zx_X(A`W>D>swO_; zUo0EUQ*|5*?H<)M`_7uwA|^?yH*8K_8EsZIjWEb!X!b8JyC;tW4jjc^5jql{^aciF z6P>-qpEcv-5-6?wfovHw!-rs zw=F4V;xV(D02A!gG%ASvk1FA~g~KL=wffmvc|_m1-o+|ABCkNax%w&$*jk1LL6y&W9a`qOEP;5WUSOG~+Mk`>$I z1nUEk1*#Ha?}3^?-RO(Bp|w_@vG>s>a^MbG(|X_aMIYM|%jxV3-G`XW;xF&h$U%Wf zsb~almNtud%wy2p5os(t&!sK8VSKvUpzZtnJ`II|$3p0rzAv;QkQlo1XmVX8T=a;d z6@qn1+DypXkZ@l`>;kjC+vK%p8->9(C3Wp*$Dv$4bWP8n#QAK!putxbzdX>fuC|*Z zdpwU$grgy~Z0F#u50^A`sh_*tA#YrwM!e>R>=&qtIamr2I24HTFcx zm*-Z`y!HA=Ml?^4>&L!wx|J3X*-cim98F|!!UsNQ{t@b$hIXi9^*XVV1 zcaJq+xgyK=k2^lzZ|yQV;sgxw!FpMC*E^-b%jW6fB;o`O&rEZK|tD03mzj*;o z>44rzl)GO_u^bL`*RR}%CqK38h}pA_IzD!dgQh>{)QX2d^p+^Gq#iE z(qmDAWVRYSJ*-H(EZRRcAG5~^=9!%dQ@a0k{l+=P`(p9AI?*|v&99B0wehL35$fji zRLt!{u+s$%v4@!#a2rI`n31<6$ARB*!M@8S~|B#)xwCZ!{oZp``_{# zOWT2kvNIY9LwHqb+3q)*t6k~wTXb&sxAfsFA^6$OFJJx+xo0rO8;bE%kLX8_SQ9b@@%^Fm zd<(h*<-z=ZML4vLP18r`CC5X!2b5d!j4S89{yOT}T-+8TIFd<-HA+h>^|#pmNj|R|D6Cn9j` zb7z<5vM~35_bd8zvU^{uYA!au=^%Tye8m6ar~d<^=z?Aiom1)YOLzUw8v_V# zur8YVQ2l(HYGdZ@tLM?8h;!Qr_JTNp#b^HB*6h~?-g^?`KO%K)FMWHRQ%ORI)Qio` zS5Vf$>@yyp!&Ml0w}Ss0_vu%%W(EWmkI#+|hMp0%aS~-jurASs1mf5v4tk$1$Ln{s zwLEcLNwh$zFbtX#7}9CS;Z9wux1ABQ-Jznw-0-l58C66{Ny)q@!t0~nJRA#QKsL_!}4@_zAS2vxFG;I_VKJO|t+#Mk;@|ehPH<*^-$*GoAC-2;8){AFJJ_v!qW{=dq!Uy-`?gzlWV0ODo$jY3iyLz6# zdFDf}RXr$>JIYg&lqlIr^eZWb=98TVHn4Quz@jQE3?m5^w<`J`zH|21JrB1eQ+-o1N-w2-&%#RJ6a-yf|5c+bLRUkORkEo>i>TKNH|7<8tZ_{V} zsqx!l5<|y@P;$M^!j%S*5#A@V{JzMqymV1z6x{e1NxW=*?z1)_jW({lyi-8J9*_UqRppT*Wb75ek@}r(G`Mi8N2yuMXzji zFxM*Vsu#VJmfO_5QJU;_fyIO%%N!bRM~jQ_>t5r%%{ z(lb`}?qid&<6WI4XzfG$At^)J5vAyc)I@D^>?ZrL1MbnH%;t@Vfs~k>HDTn9?xH?x zldiQhuU)E?6>nndOM~LliUX4gZ5_Fddim90hffuKEj4y2HaqnrnCw1EX0c?(AG&SD zZ5I}w?z}newLwl~H8&wTeOBL;qHUz+%AXgwddBq@>J9at#jpWj;7NGmM{@X0WDdc; z9NxaKw`6*nQwFu&I-Bf7#3Cr4oze+8_|B06u`nur>^|{u=QGB{BW$1M2p}0+l*Ox5 znl;amO0w_oiKg$w`WljLI@dF<>ne(#mmC7vYJE<4*2D%r`HBvydN$8;&wk;)55Nt~ zzzY!%Xyr>g?f(7^-=_2D?B>>1Wn5_04o1y-y4J~Xu9#Udc}g7$u>k`uL#}DmphV@q zHGVT>Nnd9(+F$5V2k35?*z1E+cB-!!C6m{4hZ%||>LTX8eEBl&WC-J``+du%j+4K+-Ym#L`ar?MnzuOKO( z$-F3hpd=+FCGRlu^>LA?-m%3gEecs!w8B2@QF6 z$QZg$`7BZorgoz^U@dJ&CM9r)e6UUOw~_hwrgeb67p51nMb{*pOM4>9rkIhudx$s= z{YHeLV=Hvj>BggVWt4x*7BNkE`j#0eKSZ^b_9hXD$@=>Gm)Rl3Bn<8LTSM(o*5Q+! ztY!I7{JDo4hB>WcGgj%&8k^@qj5}`DiQ-PbykII}otKjB|!OZu{p_S24f zw3qxzSMx~tuLebPiOC&e9Zdc1f00}ER@x^kbFd@X43gEH!dvLGnAC4 z34mW{pZ6>>Bf@j$0hm~&pqG3okxqto>V@I_#6c=t1NZZgb4+sXtDPDG_s!jf$OJH@ zcX071AHZ5Nl=`#LoNuod5> zLd)QRFX|a0yT0!UsU8s^(w7yBuEoNlz=U*~)!TU=N@{+ucR-=5gN$ zPuYO|V2n0)6t`ds^zET|@X5uhIHG4Ft0bec96_n;K;Ao>vg(uG7j8i;)HfW&lolY< z%9rp>gk?VJdcHoPBxZWq`p_h2?HhJQ_gK|?&%TRD`)}++b6uPb?85q<+YQ{tE(0=w z`r|yl{PlsaD|&_5RQlUeu67Vty@ua?-a>#G*0x39r#!`@=f6c8vCVBz=7}_)TQu?T z7;l(97Ig}?oh2$KMtO(%0SI?4FXkAU49Z`n{-<5WjMYW`Rco=OK4$yjo)*dh``@Ub zT!8Wm?8XOl!Zk&RSM;vZ(>V4$juLjHs82I+ok_;HrtZejb;d3Icr;70V7f~b-1Zq` zLF$6rgDQ=)akuUJi^i+7%@;H{na87N$q((`7H2|@FBZv*52q1Bb~Q6(=LX|NoO^tx z#_?YcGx6IvJyyi-61G$O7VhhuFbX^-RY;dx~}>6?R&-Ppi;}H0OYQDVt8Brp*ja zOV&~Yr(X;lbz#G2Uo$UM&W~fj(loD1jW2r7vkLP=S=SyesSu*^7st(j2YIuMbu9E+ zj4I0XW369BlQA$X*Qvl)o+;8@wdt+K(CFnYS{ASMJeFf+4>1&Gm<*la`@tDz8u51g ztcga8Lw&wwmCS8bqmIs9@ZKo@K}o4yh`{K~NU`TJJ@qJM*{r*TeeFWnci+=NgUxa; z8uoAwELJEEDSdbIgP+zzG4X9dQ9rfeXEO-D_yleF(`m~V$vJ#?JZzze?4?}s%lw8( zTwjOjMk}qRQE#KVb<V+xDcH!@+xJ{TlnoSQ?Et!zmf0qCLlvydl&& zpD6lhl`qNm2o_^PPkXa21f6l;UY=wTNP9Vro;}DfgVQYO+t>Z*d}dhYBd}UkVqMv= zItz>>$yd_kHuO|w=0`UTjmeb^AI!)L?jWmuM~k zz}b$(;k)G*H0R^fm)Sd-zI}!oH-~$~jm1>M-~^_!`e%zlb-LuLDJg8Zi6niLIK{=Rzv zC-C9WjMhi6AtT7S2=DC%U}Hv*aqKshqQ6bM_|4=!7r{y3&zxD|T>9Gb(d1WMQJ(68{(a|;Voh7cWp%^x_;bk?}ez_1G^vTxiJtXIKEebRj2+{1|$@;t3QZ_5IrKuf;-$@jrLs zy=*RDmOJ3nHB*0P;Ixk=-^95~6Dp7G%rYM$}e zNb&0mcOm==y4}iLQ4Sm%PmS0Fn(LFF662q13tVjs*UP&&@x_pdeICJNO?ERbLrSef zX;&_D`s`Am$#|&dUu_vQ`FLK^tVAR6RQwX+!v#UiEKm`!rk2VNLky*$K=l@Qy%;le zH{1opd&9+)p}zxVM0GJeRYm@(%5sj0gcWB!-g&o^I}MdT^K7tOf5CxY|| z&*cevA1%F`PqHb&ZEO9oYIq$bvV#K#_89uiqm5+6kn3y(%*}Jew--=RpP{SE^A{1{ zcJiMW^E_uWvz1WmihajWk^f7#!dz*$^!lt+Vmxc4VWuq6W_SIWzp%$(bn0*4W?g*a z%MER82cM<87%8Rl(RwF7+cw7d_s++EG8VU?;0N3h0+dzXV4438On_!eK-KT{Q+2=B z$z__eYH{7rJi;+{N2^ZK_%>UA$TR1WnDs! z@!dTON8#5SyD7)!wTq?pPLSzDLvLj?>}ujmX|Ymzk`9ZvcHY^AEo) zYi<@lX~7mFc5!vpwC_LM)#VVL1q2lqh+%9_`+43Z|1eWx*ud&=v7z0J=jG+K$QJwZ z*K)k^Jp+I-TN5o(b*;_{0uuDZE4|e?$hNgtE9zmYeA5WPBDFTm9+tQeLb z?{^l`PGV3xRx~huVrHvYlY1jHa~sP2Eoxh*}9^6MN=b{FP13QGy^?SBZr{X6dw!QVAlW`jm( zZIs*n_5Cdyn2B1?u8*Y8=KU7vtW2aBG4bvb6MB#y93JZabpBh8s;k~n{9OU81xLk? z`__dF=m83b>49DhXZ)xKoos1?P~(@jQR4i5I?rZ&s^AE?JhlN8@g~rRQd)mTftVD2 zK|C@q@b`BOR0H%_Mo}uDfCL^YxGo})6C+;==pJpMw0Rv}%6M}^Bz0-S{fb!8-N zi02zi&?7WgL5H}xh#cY!0_G+^rJ+GZFN#veFxm0EG8m}if=J(ccE9*EYc2$-yFK67 zYgYYn%@gxc?rQ5ou?FTNCUsMbT#noGpFyM|Sy?FnDJyd zoKFyJwAZFSM4^M3?EXIj_A#f8(*juz zRcb}y|8YvuARJ)z5AfLYO4PAPw}f)j=!}Srmy9xqncjEyFujAb1VPog#TsjXWzC|XjyE-=PJTv94$4b0fq~j zo8snF0m2rSK@3mhFj1a6E72VV{h20-wYCLKJAI^2%!O zlPrjh3dfh3ehcetLik^w_iaDjwDz}1an&*t88dKJVprki;kkLS6ZbpL6EqCpsLu2~*D(k|8`^|@H0Da9nMEGA)6 z#lB-PQOr+{5vl}%&LdAJ?aG5PrR>h9% z{;=QkeOFt3tjU_2uO}uZIyWo(nfC58Z|WeW@xuo=}@>GA)QU(kGg-b|$UL5hQQ zSn{Q{b-~~#DTz?$K2brB7L0o;r*g8gvJ4;gM{Al!Y%}(?C2npXck}fXWo@Ip^Id?W z1W`0v@gV>J?mbZF%B#sLo8sdsA?H>oIcD0`LGD!D)1!ZsyAYX5oxB-}dwk(6n_eED zBNa{v#U{E(;0K(3EbpT&+^YqeD^q*wzbr66C;Y8jJwk&)TyuUX>#b`+mIb5wuV=B> z3B(<513HNcLcD&EYQJQVk2MmoR(i*RtW8s==ul+EPn0*K=Tm*i3oI6G1Ejv!daz0> zx}+wlTcCy)`SaT4(lvMOaNL*6%BU}~f;XQaJ@r$$T9`^j=tc$(Zjku-L|>4s=!R;n z`(ev9idJ#l`$4X6Q+PLP_3^5ZR#)hs(0ZqO>ht2+?AW zGHAS!m(?!t6`Xfkn`c#2mh|V&w6Yn(byYc7E8nAdyjU68D5^yK#|f^t6V`TiO1cZq znX7K|6hG`1J}WFK8Rl(MG(Mjk3yN?2&4A(yw;lYw;n7o-oTsOK{`Z&@UgQ9l7y2H+ z4z>zm^x54oK@Do|+Vz}dl#Sz3p&W_Ta2a%KJ{u{0Lf&((=XnS`RUwbjid0f&1Pc82 zG>AFSf51jh89U=FOs+yXC!TV=R?@Vpu(1pa0N7(UljR#6p!s?BL@asq^dfB8Q3r=~)@Am;gW zg=SW^8B~@Ah*avs?{luCtAurEH$5pWRH&<~-l*EK>vlHiiTCYPqsT~CiMrPzzW?$4 z=dNd>TkgT6UPv%TesKF_#iy_9h`N!I5;#?UDw~mO2dT%6Ototn5k;J-{K}#ADQ}gn zXW*O(inonJA47Q*3$e2=>4-e#mhVr->q(ykk?KiIX=ND{i|~u0?8HUnRWAJPsCPSG zs;X`g4R>TA4?n*Ku3~~o=mh8ctt+d6<7}^n|8fZdqXapXK`N>B^Rl2&)M7}UJA7fyc)Wo7Ebl_y3^%rJm^8Qj{&^dDlS)sH4t8K1j*6Ql zF5BH=srgs$y$u{eOkZ0peNq|z#a^f|p8 zb;tlBJ5_Z(ty4i%R5Z5y)95gtKy3WMyozXKUU$~2YC%?4%nbmvCOWVMN$XP!EU2Lm zl3t0r))>c~7RAs};^|n<+aQArlb_X$NbvKGBuNe|S>v0Wsk#M|ntK=6S+{5P6L}8$ zjVuo^iS4~npd6``VJgAZ$@}rWM{v$S=*`HdC6JjVn0$OS@`#ktB%NeVaDCxZd$vj; z;Womx)Y~vyYqJYvJ=BfF9%(l&k~$-+8s)4_(MDuU$s*LF= z;{4WZ8P-S-l`(Su;PMpKJiw@v?&&t&q;X@dh1nm}5nn*Vn7VF1EH$-A%W9uKmvME*tmctoe zx}ZP2$sxYxXw3n*gh_}>0^p@JN`wspJb%sXCbDfEb@rlz{0H%EPfHT|CHv?26AM-=Oc8I?9> ze<|X{B+!QFl3fHkkGZqtiE+H%!N)Pf;8pBfuNxv2aTfcyk@DFCIfeV0PTy4 ztj`o%O`PbiEO~R!-3P07t4e)~8xlM*t;sAafv8OQUmyFIYRsw#K1)(;F7f*6w5)KM ze~@kTbjX^dMP>WBru{%Qh#*nej+dpEi(W~=H6bz7dJJ7|(U{#;O_S%Q?jkGbiGEB< zX{Y8}v=uv_7@o3<-c@0INPl6c0SAd4dwY8d@v>ekA7XzUs2V@@%2-ZaTg|wfl1(E` zyxLN7Ps%BGIH;&eN z1IOm1bt3;0DS<-GuTkypDuVJ}d2>@!`s-1^Qg+Ljr{6I2JsfGpH$G{r)4IL#G;2?k zv+nn&P_fzlV}ae8Gk$>=-j?C@js?cn5HgNc3v2++1GHf?*!6-utnA#qAL1iIS#U29 z*H;eNW={kHO=naRT1`p2`;{74NEjH5=g?gv;Ceq<1z%yLK0rkyxgb5a^Rh4cyD!aO>7OZaN7pNlWd($X&8+)q9sU$rOjyFx3RutX1bq=dYNlF13ar4p#$i^pVfj&6WU9tmX z1H;MiFbL&{)>;3mrkfL+U*0WF5{M^Bo}ip5gan(AD(~HS1{qQ1<4*mZGzEtt2${Sg zSIVR&@!IcSg9kZ+Us?LeB@6{m8L6X_F7qg|M&+#`Yv)B+3X#f`j=WEYj<%;TBI|Az zZ)f69cXul?I7Vkzej)?d9i9hiG6{top+bQ9bh^WeTzjwT1Xoyt6O&)fm@p}^rdd+) z4s8S_BOb7z5(ttYE9g%Tu2@+_8DZ(ir%F%No(n;iZ*wm*Ajg=cyOi*&3K< z22`y--j%yD*Mi4w<`K3TJjFe-c=+_L!njqWkMLwcMpA2Kg@(6ruv_!-S40ZF7W1Q5 zD8QYBk^kcd0LD8Cf+~Ooa)Wckk-zNoldi)fKAN-sa8`6e4lUx}XPc#^ z7b5^bU!TAb7G{G`ht1LAFL7K2q|*KLY<1GYVBTIs&LUlHVm9?&mOPbtY#xOhc#4@J zxUhmXU94d$<^UmISxIgQ{kXsd!KqY-PhzvzHzkL&;i>GJWT(`9Zz5(c2`Jr)(b$tR zW18_MARy3ykjJFF^)~vw-T^vP0I3bSU2n`do|bD+B3^t0%5L9ux>6@9`_m>tf35Km zq7nI-VZEn-3&xjk%y*+wbS`GtPUC=&vg{lr%K0uo&00P`MwGrYOK(m(SoWIPG$ObB zTBKOtVMT0Dnw3c+5s`;gisDCCaP!Qgrwl{GSuO@f* z)Ii89Yj<(K5TIk~(W&%ckdb7mpp6cr*~#dQGlC^ai>~fASX*>9qdM`+w>|T4x;#3L zhwejIizEk0kLz${mP(;*juS5b?XnY)`^Put1rvW~Ul_u?tUtRSH~-nW_YJ4Qp=%|6 zA3#c99C3n0rz}f+IN($Z|~&=wvRCsp{{k2?}6zM5h9!`tbtwAf$|P>2usD zv4f{Ta<;PG<`=~7*nF|pE3|%xU;oD1VyBEN#X%S`jpy^E9h0@&b(nqC;S>`nG?Yh0 z1d=3;HHQy~uTjZJsPvfj+bHh@JKoT3!k{a79FK+l^_68|VNTXM${3=Oj%O4aO38|X zNLTQ4OW^!s-6qS{A~p1SC9L21jj8EdW*c zKA=;%AhtxI0?*(Zzgsr=(v)0fgOXL)&}Z*QYjMr2aE;vNrn7ER zO#E5kYvnV#0q8q1!c5jbJ+&t3cgQO>gSNh%q|&{M?u@jvzQH)-liUKW!ViJ8$nz!*`rzP2}U7L-=&RX@ptVXkfPSQ89 z*@vBA7=p=g8Q^~Ig5(wfRQ#IeV%7i304pk9=R z$T5?_B*uXrB%A7KJ{Z9iAkw*W5v`7ej=4_@FOT*sRh(~As_@%fGaAw~ZBj+oyf^r@ z458QQTB$no+`nldAk^dFTW^Ul}f*}pj4DZ3go1tiJ~VR(J3I3>5Fxcd$% zo|u96c2z6Nc`v3@6jVbI&6SP)trzpy;yArx=Ubhx>I^07u+LKvK0hg&CDTy+#`K6< zns&dWCq+v454QSuD0_+q!N(`KapQ;9pac61?1zp{0IUZx&=RWe#&cKQ2H(CV63hQ4 z%!Cd-av?XIEvLcQ-S=`dZ=WI%Pdm&LK#<@7T!Xx)yK5EP*@k_-2UD`-s-7LcwsHqt z(3f*GR#R9Juc%$k65ivvNLx^P7RQkdyrSHgi1XZS?zhOC;o_()3BpGzz4xA8TJfIp zLcHVaEn5*lB_98b>#3Q~h~UIK;M$D#dvvgLiz9DyF`IFK70&mE*<@#jx72u3C#9rh zJ63a3+t8{+2iP|qKC8;m?0p+&TuJAQQG&?Q!?H2_W0+Xsb3~fFj-^xP)KFHB+UNWy z0ET#ybDPl_*U2w4$0iPMi;zGLaZYxEtEgx2-CbQrD&)n~c>{AQ%a{{&tq8R-!W*hc zUB8L*@u^c{5Z(p^VxD%3ww(G?Q9R4TxBEhvWMn`^-D5tpzOganMJ#U<5lXF#5m=6! zphenb0u_+xa(n35h!Z2*5|a$lS#R|t(pifJi_JZI!_BGkd&%c~%c_faR_n;^!<>Yw zC0a75P7TIXHx{$uq3d{Ew$g?#Z&q?s{$bn~fyHv6@mi~i(NlQE76;TJdaUWnX6D2& z)o;c|(lc9&kY-2@Bb%65mj=d7pS0oxE}CWI}27lOmD-lB2j zn085AX_@2nswVb!QU9UFOee_+m;G$bMh9T56{=xVO7)KUZuAln5HtSpAT-Dg{Jf{Q zH3%QlGm7(EQ@w`)uo(#JPiB*~&7GEab`qXB_C{i_VVux|6dEe3gB-(V(mxMHnGqF6 zbmZ30;x&o>3`Glx?VrNNWkCwwRo?q=qES2BND6LolXD7A&d$sz^=Gh3fN*JG27X>n z&Qx}YA0HDV<6FNqDjG-~G4~90t>A(}BP8-*=yQbagO51;4@~L>u|I=0{Ff=eYRlM-n38-f!0F zNl^c3q!#)m)|s_6tBwS6tUQ&J%AE}Q$-XUC%lW>tO$7T2m6as?Rp4oTK!&EwQj;w@ zI_BH^@cmEie1}jDjn-gX^!xL7O_EJQ2NoRegI-Y%uq{})E+l>aTp48`c!Gg}5opcM zgIX+RKqaSf>F|h&DVJr~GGY8o#z&7eQ?$XaS$5fy-2_ge1Z*-ubh@X$|1;>k=&k#(u&P2qvFz{lwKXLZ^ot>Y5m55kaFbx92 zV?hW-2Ma-^>^VwHlHzK8mjfzg1=Hoe7%%UwLeabsR?GBcl+o^}X^}L9@eo@!A>)U^ zAlg7Rv?!^mi(L}QXHcZ%I^|A}F& zi#dEZUwe}|H{fj3h3WF);EkBVw#Sf(+uX$*)4Y*tg~{CQRUPI{A6YRAZmN{Ejg?l5 z6(Sz{+GuBX2m>K;LywK+&k3Dk)r=KExfO1yYWJG!bzFW14W72TR>kB>brB{7$aGmo zG@A;fxH;@9UHAPE_lg2JN}ZhZHjqNbAs05)Bfj9JzRVUML+N~8tR=nQJlgWkiO^5& zg>d{l-ZWAw5eUvOn~7d_y#(;(#k?l+$OUrKkgnlHNXg>3L}JSv&?$54I7fYX44#2p zQ`~1*gcO0dZ~@z74VeP_Tj-&Lq=C#Wdz0S~ zM*3(;h#!QF^8;9zWZWe~NAjKs8J&CGNk~Y*aq;7(>sJKf$(~{Mh3bRMEO@9rT1NC4 z{{zdLqziy;zb!+{q9Z{vp7ERer>Z%+GvAJlM5oELnHb-rqeh%Of#DfhKz{*I_(86}rsv)4qXaZ5WZ-v{;oZzgL1Je#gZM z8;dMjpe?TDV|8d?W;AONZ>o$&mPhHxNCrYIVdq0c{Ftr~J%mj6*PG9|-!M6*#ct$B zCLrH2oC4Uy1edHmr+qYLRKbd8u4xJjIKW>#*V|R!{&5wL}ib9$zrRC2Xdi@AK zCTs}KZ$p@X!Zau2fqw3lFsKt=3$b2oGV6-dnRqfo!)%!zh>d!ZD7r7UZ!4Z*CTS5o z`R&1em?#kTS5OVp5$cz^W|@(91<8)#Pkmn2_tujfx0c`_d&oc>$9CY81J`;R(EayL z{w!fnu!Q6FGo^pRB`Jv{_v4bY$O9*#iWBX{A55{59$2b2u%yA@6p*u>8$?7u>+|~E zV_U0|>AD}HqO&j2W8Y8nG!Ns&u8RFK_wN zln^`wdc&ZRH_iJ2kn#+O*PTUtEfpKSg6}q%Z@GXr*zqvLH=WCl{y?@ z;xg#2(TayMv}l2&`0pYkf3)+CIMd`T;?$nzZ?YS5kTLmJ_&jjK%UCHx z3Hce&eSi9J&{$Yl;A&;XFl~{6h0g&m|ESNIVUu@}Mp8dJNUCv#nd zct%KH78RJ3yA8L=lTMm$`T|nM*tT7uWj5WWQm>sDsisvS16M_1Oj% zn+9yjRPG@%LWAx@98B#r3@)Q5)aDn#4lB#(sO$Pd7XX`uY9xN!W|bq=1nDFx$5L!& zXR!`Dmu1P#*&z$?DOQlPT0FfxlgPDLVjhIa(dn)1_#5Y?DoQ7$jq7%E42HnYAebD3 zeHdygK7m;#q4ic-yb^oJwENO1(j{-oKy&}CGVpZt{dgK42MrYD)<7GeYP#q4=ml%&6|Y(&%4n`Cka1_svHOK)061o)Cwo5gQs z6`f`2)&BEa01!sYR~IL*BH+2X;qXv~HjsHxsSoQhB85dNpB2;V{H6LH;1XBLeKHk- z9O4rf<;_JsL*+MH;jAUT7YSPjJY*0E_vL=1I?~My*q85H&4}JtKkW>GHGn<%mHLgp z&xeR+Iy&S)vwt%|!r#NfFb`lady)jS-X7c);Ndalcmcpaal$i`COA}Z1Nm$39^@6e zuLTx1_6vu+yUco8-vQtTY}m^uu$e&FwBx+$tMaJKC#bt0Xaz|mXiV!Ou4}@ zmkS(a07X( z^DY#MTKeWt4GGt|CSR>HOEc3~FRh=98P~XmPAM>|<1+GO6VsZgzP0V$kzjqkN!XbM zhZ-`y6zr3u06138i#7v|m4j_RwIPcpa0{oKys|@?RUQ{I0WiXIunR#eDC(XlB?vC- zb|s+XkGP`=t(d($O_njVvIHc(^n3yqfqnUj{7~BsUwOXNZ%=3$(C092)+foa@Qrgo zpyd56(M@kDKtPY>9HLB`;0;6Y`k& z*Q=F}E|^a2a5yT6|3U(P#5_4EsdMimI7><1?;a3G8Yxl^MKv@60#@Fz443*tOiRO$ zkv_DfuYf*!MG}|wpyh_I!tNGX;4PfI6&xaIg%1N%^biVCZZoP8y$EFxHp1k{pl#%g zZ2>!jA->od(M|U&i(bFor9_QsO+aaEsED~_(|hjb{9IR%wpT*H!epWvX3#sp<0O@q z*-f91^IO^k@V|lj&LP5_qRxjq~;jSHL z_KJ#%I{(aCO+csk`O$U-;qzag7)BrStpjUR29%)7tu~{ZyEw#UyYMn*!@#V&Ue-()nn>Fx73+sW+1M&Obbld4cyI5`bYzOla(9v+UXr^DKPTNT!d zm$JFTkHYV5GN$*y@G-v*=oAC#!=nSJ6fI--cwjzG9#*pVl6??a{)5*k^!<#1>FMb@ zk;>3rOWlDS0uw>hT2p|$c=2L1RdZ*dE=p#wnxd{@Nd#G&=Fq(6&}Gy?LBl2)_OdoVf)?}vMbu`L_L+gs0?gImmI^E72gNvjIrDXJ% zqTauBUc;;a+XA0V-jUu`(hflp5I0XTTUS+q&2L>eA@SkRh^0C@H$xRV)e`4Vo31b9(dP#p*?z<#JzKYn_ zcb>D7V-_INMLcGm1uGtWdh3zjpZZ^z^qM& zFi?;J9Y7`G#-7Q-s0-LmWprb064rqA8sPJRJ{AN7k=?(4Uu)k5qqLRTO5XYlceGSs zmOUkKT%A1l9{|S+&rZ(CF|Z^F3theQ7uj1W(WMbIMxQ{tXWKO!~&&%z)D5C+#tv zFO)x-qaGpznW}N9d6>0N80?_`fFoIT)6i3v$&3EA^{((!$WdZacFadI%*T-WbQ)^v zs-&CN0%zVwCSPy@KjAQ&th-@k$C3VH{%=dediDfnm0dE_o@fM*d6MjEGMXsnH!jn! zqp}Q>*x(kz8|8w+JST1%nXaWEGsetrx|qQB>n{Qb3+k6sgfPZ^w;}Z{r)S<;bF~C& zx(!TeFxo!IS#Lvd*Z`MXs&h=`eSpA5oSYpN^J7$Zoaswt)A&0c3J4*SGP6)>*)lc2 ztM|1sK@;_e<%~_ur!-a!KDrN@D9P)g7!)W41HbWq7<=zPs{i(XJcna8>`gXhCKAGt zh>9rLE7?0UWT#<8Wrvd$**l}GL{|1*Wo9*ujMVpf9lAgF{k`Ac&+qrw{l3L{o!9eu z&Byh)uIuYCOZpRRQ;l8CBWSyMLipWvH&D1w-bDm%_YU6pyNg8WeR#Iy4po==?heq+ z9r78p)RU#<@s=b<4(ukL%*RJDZhfh)(3gJ1G4==-S(bFx0+-(`J{3xr1_+$}&)E&h zNaL`<)f^HVkPi@3U&S0#Kw<`>O!;#s?jRuDA(SL#mt)-M6HSR95U(F@NJ_gaV0+Cc z$Sx-^s?!=5h(o8Dzw}T^^0Y{q8n&R|GAX~`vg$ASn!gf^| zaSB0N3v#|N%(cQm`Ez7Xo0y4F8FcE~N_a!a?FTtsRJ@a#!0suqPjg3S<* zaMB^Ab~WWjM3`E>-@1$uXCxYLH>~4)h%!D(oOB^`LPD8aoWk~=nHzyK+f+_vwi#7! zi~L;F1yFYtOpsIcwC;k1i-kpI=1QR`#5CWI6z93<%!}em0f$f;crwk8+{rS2f|H_y zRwq?TCqwa}@?Y`fEyairo{#d1PI z@*3ml=!i{hT}YRIJ?{Guk@EmC0!{B;miFt%QeBUp-e*cqQ9;)5pzO}RujrsqA3i0u zGP!TQMNdF{|8)3#>LNwgk*ETm)USA!GNbcgOC$6BtKyv!ix;H}{JC+Uj%ENvrkGoY zF7R$BTnP@#A};8q{#NGOCpkJi9B)xPB!R1Pyhm>?58ujA|LNtrVjm@TZaQbCVb7&8 z*A9CGv~$G2U1$Vit5WFn*V(14PX@{Pf_m$~lT|tFYH z{Ivv5kuzs#s~5h!PnJzdoNm(s<~bq(35hG6B%8oCv?f`&KOtd6XP#Kd{TD6d4{O--KVBdW4#3wy$n@%Mw?yd#MBvYzau&f4=8>TU6 zFFvCvfY-qzn4{}jOI#>nstQh+%!%>v8JGW%NCJ2~aHbW6qTVN<*Ewt4mIicEKH|a- zn}Hd8;&e{zp|oALd$L8nAILUC4xxt0P0oed&ERJfpS+cI_~=nRYJ)VvqZYUayrMqq zH058$Gk#u|+3jN&i{`lNT@ie^3(-gtt{3892(vy~dSEXF(&l})$N@kTQ+8&<5RwTY zVZBnms)xAhbT*`;JYHR4mK6O{Zq)?z6{&_j%SabE-`hju=gW8=kGJ}{67@4$mX9!k zQOUb)p2I-#ZG4E|8j0)+JPnkdsEU&FmH7wKIOM4YKrr)cBsd7k47sqy;g09Xs@}}} zvWoTZ6!#b-J(6%qTYaS!Zxit>)R!^la5y#&$vep&AL2;@YLKJ3tQOT-gj+? zT6+~fG&o4NP|pW)3X@|_=+qxd9{#|24WQO9WLD=w7sB6MOK+urX4~^IG4&;8K5i6s zZQc}=r~6paG(ycLw|W7uAx;;&icMJhFfbwvx#Qm4@+ovc`p84DL(#H#t}9bkUpRz! z(FCUBOd!Ucna={LPO8_jSWy+W`3Lis0$Ze=ft)e6o6@lN4=3h5-<@K@rV%l&67Z05s z7JoK+BRP;3!s2giU6cWw7b6Iynp2;8A61&1KH!x0lP)9+k9cVgGU-KVEp#zsBctOrld;qY2VvE2SeRjZskYK-6!a4 z&i8gHAj$v+ovl?hG74vrNQkZIQF4h!Z-B7v7uk9YWQUrdhL$RHzQ}Z%LOS8nSwyIZTL{r$ZeKN|q@#VpT%Q%p$<5Y zZAS!Y)s3p4lUDCzz7-PdJQL}aoOz1MRQe2rLP1Kj;OqJdtY}yyeop-j5U1o@I{Qwf>q>x(_ohzbb%|J@HI&4hnx<1%#P?L> z)w`f1ekX|+Z}&>)^;Q*rlqzJ@dRbZ=f`SS6+AYYp()>vi9qd)#0BjIIf%6bSsJHnq zT&FL2X37sy`WnoKjnec1h>3Lx+aEI+phdDEHC;|`SnY0~2j1O?D0~E!Skr*7{smcW6(pLNhr$RvvRqj#%p%`%6hBTyS2s^2psI`x5=h zk0R{q;r<2ueDC94DMh&9ggSv8haH?|VPSy}^~I|&PhVR2Lw%W|1%b%z=E$-ysbI<| zd3kyB-YoGL@5wC%@PjGq6T*J&%v4P@J)Ul^7|$$0%PlE6CQcC zZ2qi!{kr?)cuX&P%;AOR{TCSvzuFmjMWwPohcH7-kpqvZnPdLq5D$SKl3Z;<{>9vm zlokszGvD5F71W^OlS>G9aVSZo;zDYa*#2qk`L zP#9fA-=#S`=E7Gci?BQp8ysB+U0J!Ry@Jo0C=zucyf3qZJ_p=2sKPZ&IESs5tD4x^ zwKJ6hr{7wiafkxtN&5}WFJ@&4Mj-iEXesgvl@#B?KW6+6?_W7a1%Z|u_oq#tynLoF zY8(jGi%5h!$?|YX^9T*%5(mQaKmB;5wr&AkJ8rD#<;4HCaYKMABcJi<_6N`lyeOOOh_pij(+Ufdoj?+iv%^O4qe>;o3alW@ky zp1Ny|{-TYgnf9mQ0dWhNtN4?dYfq4-OYxfOr*X23K-xYvyAv`@ zAB+4Q>(q|yC>C-w$6xB9z_1O1Igx!8JiqjtcaTWs^Ahd1B(hb>>p$P)<}|=NyOAQz zlRGm%X4@c3RwH%w@X)6QdJRalJlkV%_CMlm(<}1;JbXcv-<^$Mhl`t<1lYyaM3-;(%N!Ko$EOYt2Ub3K6=LwMGr>9hh==2S-kLYyKLr2deZp*Y-07$Koz0=A<_hbbiWIa917!A^{v!r1SbV;@$08+}qByGDLv*j3&e(n5HT4ve1Lx{zxLEM~e zXVj`D0T@%j5I6G@_W}X{#)~06ZBD7aVqjc&ev&4T#$1Z(tQMGqD*x6~4uk59v7sS! zx*sd&US9QFbK;st<#(@k-2wk@0x_|y)dY;KyxFVfJUcxFiY zS^&pdcO_UqH1K9vLY}PZhBEKfAzLq$)VzfK2=(Pd^&v2q0g>e^#979-D!kD*YD`YX z*6nO8<>3!Vc8`wQ?qjs@8>Q&6+XCy$!Sg~dofTQTA33n3G1hdJ`1@GQZ!0THqd-U% zF=bsq?4fRKY&1BR##bX$>Q5Silo4)eVjjoP98|~R$<_JEj0e@jK14rSw!X~qY|vqe zv?|bsOhy`qCT3}1W@#%@o1nMEcI?tAZI9-HxTGY(xd3O)3~GQaTkYntiR@TCD;4=r z3%eA1wn#UKcgBcVPjYGGa4tz++<{|RMpbQlINC5g&0)3-KFPGczOF-*;}Q@;_*bVG zD4m*%bO3_Ae=XMN+Sqsgp35ZOO4w}>yadNq+RI#!U4VHw5rul7r-XN;?+ert)-rN( za>AIIPlMJ*N+Ws9)YkWDmoHkEH7XLZl@gvB^mxW?%K78LAB>J7eX4xv{?ZY_(hdsj z$EretoLT5=Q%S|NK7@nNjhWGqbiAq?8q&#qluB60Wfj%W%xM@-3eXd@dNPfRrQix4 zR!{x)beG4g2S*Z7-3~ud1ElRUdKk|otutM5*CC+;r6o4g^}j9ftFm#6#+NRJ76HUh z{fJ6a)~H)YuWaUIVbOc?FXK)vS8=0A-&G!yc3q{)wIY#VLQn&}lUSi&V?Qn%8yjE# z`Qd#pmD8G*=T&X>FSA76I33PXgU|k~=76TE$oaU%s}qQM3((IDf332;68cIP$4FV= zlUK5VBzk6Iy~czov?$FyYB8&@(fKWoXxvRy}!GREU_dmL&i8(;?-QH&lN? zZ`K$K5y>Xm>!57@0&j2cJ^m7e=>qC;m~`uX5ueK}X+=NR&sm0Z)+*N&K$aLkPd9?S zr!I9O5)qU5ZT=SfK11gT5mG1WyiKa;*&^QqQ5rF@b&5krP1VflD*R@GE8a2cQhhs8 zx-#cB&~wELXIIFbfBMwe3kPu(ge2IA)oSKtmCfO^vk8`%kJUB8b0 z9EUrOkQ3Nd+A-SH!*xj?YqVk`4wu6Ja`xjC!WVDyEUkI&;a8TIx*w4SYaB76Q>>G1 z3w73HCPFbFfh7`DC}eY;Abw8SRSBBi7koimdoVj1*LTgPt{kv3TG326C1e{*=Gkc? z@sZw2`eUWa2+q%NO))i_ZY=g^G~8}3aH?JtpdlpEw}R9nxYOvU5I>Td6#2f`zzhv) z%JEzCu*@RNMpt+D7^-_|h)P}JnPT$SH>dqcSZO9uDP=%7(s{!@N(Xl^j09j~5rTK) z%eC59Z~@RDxaOzMjE7={2e4ApvUl0xjsME|Xd&D>B>#z%N1-b`!Nd277w!qyQLA^3K#VH4VL@V4Ijy}sSzAkRv4cHE z15|>+CFz!$BB(?kmz>nb`Z{^(!@zG+G}=8!6`8$W!oOTqj79MtMNRxNS|<53}+7lp`(CEKMN`vr5KLng6 zeSYeG5Nky59Zvfy=6n<@PsxNwiERvfWTV%~>)u3(fjigd+sN*Y2^y4~v(=R^OcngKjCh@v zaQ_{p0c=ON3XMQqF?S(SDwl69`Tg71mSI1jB&L@CkI$#*w1{9wa+D{B+bFDp{2jQ) zf4`t9h4B`dj~C8t#k$dvYHd}8s~-w?z_xm6IgP=$^f$)P2#m=m-q0F;<6ZFwU%(z- zq`}_s)12-oRnyUfU_8oS`dfJ`w|Ulj_H>xq$+T1K>Y8}FcEXDq&IZJ0I>_MGQ&v%* z%$}AqlOd&pA@!-SggD7g%_k9HkV@4t^>!7}ir}8eezs)VQaG&z29(=&-!jAra}CLY z{2P5C-8Wut6GziB*Q;n0!Nc;%YIVMVvtqVjR1r$XT2lQXfN{EY_1Al7|)rw#7eipbcyHpLV2XB!EZd|dOXu`d@z=~76 z`aByAh*x%D$#sIyh`t9faSt9@fNqDo<}qyQ4*(%VQvHa$?-aPNDN2%cCT%p_9O`i= zs!Up(Wq(36^5MGf6Cn-(%6)W=ln>FDb<(HRI&}NSqW`)rcF>HPYb;>u7qct5d#As z&t&==?kC@o5r1Dp|GMauyVN|3w|xp6)TV9;F99vJde?bFA*4#9r(HI`;9k#hGm{W6 zNt*PoB5%?y0{&Ev{it3?88iC~*eSN+{aX-+Czgf950F_oY5W{nol{-9UeAhC7i30+ z`4cPit1?}}HlFWTYR`X`^5r225dasD5HDu0Vd#J!*+%S_d8vRD-vfJnHP<7x4A!8cp2dHqn@@%h+vj|&Iff{=Q< zqf56-_%wgC{|+a#v6n~a(&YkX)e(>G%CO7hyRjy^qpL!w9BT>hZn6lMw_Czl-iSbIQ3Kx*v24`Ezud#<~Z+w ziHV7KSjE+25%^ZG{0Tp~+M4i9HDR8snp@n$8N3%jF3Xz5E!LU>nvP${ zdjF`CS^n6wc|qlXX@Ame8*}-dFK`3Pbx+| z3OptnJz?{OYy4QUJ}3g=dlYhyw@{Y0IKCZ7be^q5X?3g26V``7l?C_CdN}$?jzMbT z5-{t^ontdb$_uy!$7Ny`si#bEzd}i{e)nBom+mt#Mz`z7EHNZp0!mnN@ZY3S;mj9? z30yT;85oLKkG24sQQ1jA^KOhW_T;;TF_V3ZiZw)?jKyUvNqV}}vjG8aCicl1muV_Y z14mOcI#f(Oa7RUKriabKcRW4io_ST8a?lJ6sw$zm3vnqfB;SLOoXCM|!tlZNFR}&p zEuitC!3G~Sw?JG5`Q zv$PJLQf7H*i3SOBC|sg%oeT#63(?&O?PJ6+C^4(wDx2Dt2j{F=kv>cvqPA|_fzO-= zGlmq3tuvy7n>5zK@_$0MTETs}gK73)cL(*AgGbb>X&!ya11}&JcTVMJ^EF@R!f(Jl=ZI6CFb?!C5sNh?sA&-Zm3#25+4 zx6mO{-^!DhF7V3iLSaAvwK;4YlQwsjf-lbB!N})dv|?K{|Ff5?=`}CSgAsx+^1;<; zSI#%LnvdF6K<`6l|4Z&`<6(N*EkaV2<=sE`ky?L@E?V>VL-)%khzu{TovR$b3%8N% z^0ai26C{^aTs8>GTlkUtGIl7#5s}z4v6Bg5l5s4SOp_T zoOkw=EA4st2jM&<#DM1v`0@O}{tA%17R0kM*U(?>s!Ba0Jb=qdF<2`^aL&xT4g=Bb z+1}x)Eizsx)Xg2yYj1C-Ue#%H#z3o~jh?%kTXeAhG}K!lD2f8&4`Z8vp0?Or^Glp? zwDPK#;tasud6A2DfR+@>#3``-L@m~8$dgq_-_N)lOGqI2Blb<}>55~vv-ZFp`F-?0 zHST7fDdFOVqMp`<&&tnVxG8cRal{D>TO=RyspI)r1}EuLOP*<1L~{~~n6)a=uBUo~ zP#z{cXhGm!w1z)TOVc(w@ex>a&rEQ*Ao2ma)ZdQyONxj@Hl*MER%6i`DJrfAX9wd32TiX4PhO-brXfjz52Ch#QfhL#S;! z9hed#bgV&D0^ea?lu5-k2;PN}U}hR12euPd-AH!;M01~OX~YO?~H zb@9V(Ag(H6!AU?4B^IjmI32x$az1$VTY9 zYA?zvcs3!NM6fdy1Y@-LrNw9CbHH#l40oK`zkNl>FT8=mp{t^2_~q=~Ehod2X$9bf z!~IUUM6PqHKL9ryCt-C8+J%}2wv;)M=3pBQAQg%i>&oJa=l+;X0xR=RACKy3ZyD-9 zc^Y}#X@VbTzHK6(SRqHl{<8s1pDND^sKxaz>T|@Vp@J>ALvk3kk8_1;vD2J@J#~%t_CP zU`%3&(C35NY%mozy6J;JhMVFiHT$Fc6fJy|*Lw<|w^1)ol_0xCsQSJ`=BotX*Sy^= zf+66czmPxhpav8T9U=9gDt&lXd?|yJ9dxzm%u7`NyLb2gHLxs%59{2#yqxQ11(EE) zT?Pv)L!6%A>|_u5Yv`Kn8M1HOH7GI-E;~|DV}>wg&`^8O_;z^i8+jHIZ>KK3;t33- zeaq+PDr2NA%+>~3_7D+u?oi=P1v^OuWd<|Mmx>wI*9Bw#&{Dh^Cqj`l0r*5mbyRe# z-Sfm#`WZaxR$$1Dxp;OuSwIXJWh=>3(ipbmPEHjAq4a0C)3lO*_z@0g8NB4-GM!r>s`J)RUbs;6RLY3a{^$@XB>$z zH0~7wY5gP=ypPZcH7_n0UfyR0PjKo`aNgXnU8xYXj>!f0?(VMDIE9K7kHUSJtW0zQvFag7W920vilqVO-0XPHy+E-NO?E8)` zSG_(B(D3JM%m)HW0Tkb0j*4QZ$Eq~+R)!Rk?029o4$;}C1V@j{+S#8F#&=A-Q2j-s zZ95dBi+nEkOG!2OTzV#^P`ANCTz#ymAx@<}q_eX#Sg!qGB;d0PSb;nN5XdJ~361-{ z{9YX(lew%ZFASy__NtMcahzZ_e3m_U$=KDo_qfmFLE>lfb-$IKeR_!0pR!`7$${@I$IHj~^BVB@QaEkK@Fhct#CF!=C!|eRJ#QnHaZn z$I0X-vnMc4CD|52S$pD!erRQbeN@-r zU@Q}zL>?5WDpjOO_D6zZl~FKjrSPaFvVQ6A$Z2HI{S#Pb#%`O;K+uX@MXuz>+nily zcrpe+yeZmA8W0MhELFeWfzShN{-Xp2j_5u52jEN5Ok;1e`h9r*NtDDXan@1bLmfy1LqFU>xPZIG?CpXzJu7QXUdW!*s^( zp`azs34(x%!JRfim{3MhF&nsHp=2>8wn*08XZI1cdfhF0?fb7qx~KtW~l(Y#~zC-P{n zC4Vo=cY4e|%T?@+xLl~8a3g!At{g8IJ$btK5%+E4QY5QqDjKFinNz?-{T1&K0zoiz z&zT4_`f+#dP#k#=SKpe^sT*uH1^!9AOzMpsEnVw0x8SDw10QHlr>@H(HDD2QQf@0b zobL_16gdI(LPOw!E@i|$LHRil`fs}aw(o~ zraEg+ueI-bT6DV1NvxgQ*K0AwI$wpG-=^y0@3x(H^yRbfz6pSFx61o{Bm@J@S%5sO z8;y1+XN#Iyne;z6ke)7&B!P+)h?^Nh=O+%JqMW+*o{LaWFFrzPGs7gmXTB4PMZvwK47SO=B-h5-)s#jC|ZY zkr7?NZ_0HAm&UMloe2QzUuHkq?_L87%bIBAHu|m?S@V$dl_22eylis>CX}G>dC&+p zV#2N~e?#muw8!W@3;FGlb|_@2gEWw4Z?x!uGuT0WXY130TZvHOZKf^1)8Jd(0DbIF z8&?lbmyP3F9saQknPJ_D3A*&_hm>3kdl?MhpN0PPhbb)P(q0jCP_gI1BAq=I!%z;l zOG&%%7XI5E)W68 zq(!)|gydGW{)^;5Zwce(B6HLo+IT?2ul25{1vbaw4zAR=&SB-#RM0U8+te6gW7g5y zq_;!o;nqL>6djue4`8I`t!`Qh9St=osaqid?Hc=Xr7nV3t+3?oQIqF=yt_Vs@fAc>h%yHxqwT(QvkrOATSEviT27hahbm!TPSBYgo zKiw0NX=Zl0GTN(vBIm2ZK$iO$0GHm^MlVVS$ugm=FYDta!tlEvIR+3yoFM!V#7>gB zAJ=&$oIWhp2TV$SR^Kk!l5f)7la&`2_kEUC9;SM?sPMQi2*$vioOjz}3=cl-_9xB^ zpiK(kp{a4YU6Y*f{OoO0RS9-5^JH{FGlU9Pq7P0Qo+4YAFf67C%uaL{?DplKJGGzW z!3_&ycMq>DHaBd1<34uHqPixsY7??{s}vdFJ_Yy^tk1rI9AijNez(tSpgXfP}sKRw5}d zW8I{kjeLjh?B|D^?Jy(x7fp)a@9p1mb&%^uCe+P*c)DkSFVfJ zEe)=jE6wI)CLy9J_1sCeF;QT;=6>L?HSLy~Xz6~h{3UW5+AB&DT4`ft3-yt(IK z1`c!sQUQF-TfS%2#ZiUnIAf*j+GSaGx6ya6gTt!T6$mXkEuyC>MJHVF?==Wg^(}!`^;#?*y zt0_($CVXR&{ZaI>zpZp{qOVUC?ZYEwm53Ci57aU8DqcTb z>YmO5-(og@pLgG)ac{o1)gO0TiVcU@4GO+uj6RAM(Hc*fO56eKV{W>Z+=hVr_tHpN zIZ;lgVY1KW@K8q^O!@u@#h?1gnDT7)UC%<#xJu zZx-I|{hWl63A;6*lBuD;Zfkq3)~u=hiF389_jR4t9F<5Ms!$ybN`7Yzo4@e+IU&KGCq0 zb}za5^-i;NJ4_E-^)nlLeY^f*V{P~P0zI-_EdcdsfLKAM_#D-iD-Z3YweD`+szo?DZn)D_(H=9R<8u5#&CF+W|zZ z#K*Q^&|9^!qq_ez#9D{vm)@F+EKHbQ8xQnS#J)2oc!GAFe=F>H+v{>)WjivyT%-{4 zH%lYmZSyoWi*>KEK$JY2LR%oV0R}GKItr(%bDmm9-Mdups+=fwE^+zGwMZu@dtlqhcRRR})^LLTO?b$XAAf!s3c9(MdjVv1b>)M#Dz zYjIYSkh9O<3EI?iWZ~!#9sZSOam<{$tMN!TR-xobA0ydDhy{Pio)SS%jnPYu2Uf#O zv~tjK(Fij?iIGA29k39E;k2g6RNSoyl^PNT{ozD)%=vm!G|BH-bT}l01c?vN&H5BMWtS|I-OCo2Ajo-{tYA zZYkMa-*NVX%_3huUUyRJ7(({$6}i1qwHo!(=aO33qA|;M8?r5U)6VL>{HY*+U6d{I zA|84Sw#XaDQK_AM@!9Le8nTaSQeW>&OB%a%XZfMQMZ5W{tk>!Ckdx~AevM}>ZYf`)R#Qy~XNvH?p$V=ij8px7t-}1mqeP=u;MUjneFXt@X2VhduV%Fq`?? z(5=TCllPGE=*RO%Cm%Yfy;|{dD`#R6db?%nvY7pn5zQ4SU*Mt`M3Yc3mqM*^#5Zz5 zkl5$F1MSHnB|pE7m?l^rXdK1rqjuD*#8HFAuTCbfK1M**b>zHWELW+#E z(3-0o33GsSt`hMCxlX`d-^^qejxAVA z!etcg;+E8%1Y=dMB-b95M{zd4RaDOmC(ZNtU|4KN>qATo#XYGB1gM9UN04~v)>~L2 zw`<*}&$MWn+6`+V*7*$eSkpiBA6JTWA2b#x67N!8wtks3cl@QD>)2tWLET*E>InjS z<;D91vr-==FOA^i<@W~=`1MlKTd@R+#%rk+Cb^Sj`FYq250}IGwXjPwvESgqR98g7eg9v%xpINYQ7bAoiK@f_YBf#{-trK^BWt zc@C0!m8U%Wjg%3B6 z7r|cd4OJu{cX$7{Kf)q^ z1dqrg*~zB|m>T9?U#2|qQTpq-60q1@LjMBR{^`3?U&V6cg+gG|b8c_F_w58Y8OJ@yQUBZ5{nvZ6a>4}!pA~*Rg(`t2S7Y~RUt1!()odC2EVY(^zU0md zJ|piHaTi$nn23D0x*2B1|K-w$kiX{`Dw-(mstq{IsT7v=WxDye-(A%u>kKIK@#afA z&$PTV*HgFkDqW4b$eJy8_`lx^a}%w0?)wFo7WM~mV1OqLU{hgcRT+*@t$JE=_xVYGzn>m{mg}*20jb35`JqpqGGAoF#2@9_N~FPSHPdnqbWP{R z#H(F?vSOy<@0^q|#wJJ^Cvr1z7)DJ0s(r|&PL0>Qw)|5o|-zt)l5OGo{3NL+d|P_G(P${8;doGdS>qvGihB)tbyeT31zZ_a}hfU zc09cGyX5~OhRUiYM2?qzvhBlu~)PcM0o$8 ztYd8#k8KN6g|p&qJW!h5GGHL+c{>nOA9kz#EoNla3+qOVq8%1K&66cC!s(?B zLV-HHmITb}be!muK0CZc@(x;yUAQ8c|CS|3E#bH;ExLGYkE93}(qR+oFiYg?H#Jlx zINPYBP4`6{tAY3SB4c#hpZS*EU-Rr25n(O8?Ef{={X>Oc?TpXvnm;w7T%NS_3qOe+ zb!H@gs=&a(f6_PPXJ`s+?@FK<*Moq6Fi87~fJp{)0zL*>p8l7Q0R$>CY)0gtRC51k zEeL>kZl2Y8AJO_p1z|&|Lrv!* z*SCTxAkf?Q+&i7+-;MWw{2H`kLg7PM$(jED{iGRPcv7;hgh>9+zlM>ADl|10 zr{v$gZ~t&YKM-Q*$I6TQ%^&~$ZQ(1!Oet6p`Tr#}gtL<0)5is!I>ZO#Iy=?;J^>2Z z0n3>`85P~H+*y{M+*b4J0NzT%WueOS`&XN^w_#nM)h6sx@|%Nr-zvZ6N9z{p6?TzB zc+mxb(#Js(`2i?JAI3cAljdM)C~F<RCa zvjxE{hzAC%y!cEnDp`ZT&^a)QY!mZsmNvfvhW*f2i(?K7nWAG zd6PHhnAzLlz;6zdw$}WH=;f{BM2+XOGL+%qqTq2|1Gzlg(}U*uvAP12>5rUM7_f)c z3Hk7!;k6zz_ZP(+{9o&5coT)+s2K=(oW8#KnQ-EIW*o4k58El+Wei|M*D#e^z)D6H z1MfjY5Al@UvHAAbx2M~oV3GKml?-XP7@S2KF8*#v`g^sQBJ@II>d^M+dfM7VoXA(7 zuihP_6G|F3QfH*Up&=)IE27g4nH;Ov0()&-5a3J(s0rTMpNE_gB8~TttUX?a7+ZLX!dcmz=34^mX`Y`SN@O?0IfmFur>A@v!&TH{pS_$U?r3U^*lE{6=CJ=rT1jCgi!fUyA&9 zIrRimQ&jBpSDZ=57BDYm zE|5&A;DZ6DlALNb94F+ir97(03MEawrYRP3OC;NHiMof z3z(7hY5A#|)Cp!wq?6<@E70XhXAcxuZUis3`CRt z+GF)u@BVahapCOQw@^*Ew`Ry$G3i_F346eQ?V)#JRLdK`a}QX(vX(g8`77&6t$W)e z*%j`h#fdWJUc5RYbu91&Y+|6F6-J#3&@zlI>(wjhFo?aS`|fX1;(v_5vn$w-(ni0_ z>ZTs?CNHcxbngDC$&{z;fV+`L`}&RE@;ue1my_Ri zQf|*Mu~LYxB<%JIoAHVNTZ;~qO5K}b>IhjuX{pT?vX{(Bdw03y>RE@!%diKkNEa67 zG@JI;zw_H0^b20uuKm_PR=@1~M=egIenBu|+impXuqC=wO_VjxvzljbrzvQ``-dlrCGf@{2FlQ&U^!SDX)(;kglWGrMUx!eX z^d*Bl1_hetP5d1eOFV!W86t}Ujo|@oXT??7?V0o&@y4cj`EBo=428av)V(-B)Bh#+ zKyFPw=TJXNWAt{nv!02<>$CnEs2+C79|h(Ef+O6xV_jRhjv#p{egVhfJzM?y{Bq`=OCPg30#Cj)19n0y+~k9IRr z13pi)yN=@dR9>1)rxgu$xYYu+6rP^-=0egn^yd1^Y$#jGzKwnm)!XuOsNvfd|&Yjor1yb#81AF6HY#njc`22bzf5`98UgNCMY3VuG6?gb$P}Hr*G6M@F8>J8IN6X_2 zU>8FskgZI6A22&V{2s{g>6gk=p5W;)HW9!p$>nFWVm`rrlWCLf)Dl~Y1lH+;0jm^p z`HQ6f>wx_+XTal?o;{A#HpIns)1y4@as*BC#19vPz&X%8L z=W_;kOs-mqc7YL#ezF*DHKLu;rTPHGMp)l)8M^skJLxuWepemJt<^A(^PAQ6YX}I) zID~)v1k7Q(TR*i?KpCdB$0SCQ5k0YaM#$qv(WRQmCDFlIN;1 zdy}y5DLZYynU*LT?_+Z(h(C!_PzwD)R#!&Yaxq$M!NwALeJy*_&+i_6GTl(BolgF_XoB{xHPU<$%qfGgNGh6 zKI8jqYiKjbT1MU%Ox63C8;L^zel#|L#G9Q>7W=&M^rq5d)}+$ctJ^^IFW8A+2wzyB z+PES2uHhQ-g~^z#vMWk$>UkZ?(Ws|EVYqL~%ujw$o7|5%aTkagCAibbf!+Z>_5Mnd zRN10j9@Sw|6hR|I^ekaJDaUvmYQ8J2j(Zvb3+>Teoc61L3t9QuA2~$SQV+{i})eJ>H}_ZR{)b1fLfU)IV1;!G*~V;3@t3h53DJ}=x1QXUFo&rWRP6V;4F!sGlE1g6 zAMdW&>JvAzk{8LzL<66E&Mc<&;!YnS}*BRAeif^T!YC!u}( z?zeg;?^t8sN{t7jUTLNPh5!DAy|oJU={Nbm`PXl*@X~LN*cOi~cq{98==R$D0~VLT9&7j4FDjH$Rh3u3w{Ct8`GR%% z8iH=eV4F}_5p?M?s+dvq^N5TfBOZnCJ;S{Gzi-_lUOdOq2^ zVT0ye7cNLz3#35v1SXc&*Gy&Vi5PyIL@bQmaB^UE2*!W$=nEG+lMnLh})C>NP( z&o^`%@}d}>Xa&b;|Aa@3a=tYhYZeGxQ!(zISh?PFS-;GCGYE4XltCgmIvhuB`*+pE zya$BB>|BuY-v!_Q{*MQ-fH^r3H1Z{|RdT{?{{kfFBr~$>+(k@P?A5C5bxoD*;-(G*4Q ze-TapYl?#rMSu<^A=j|~`A+u{0g=y_o=^V6hyB+R{D*|3KsauP^-1Fo_BQ z&@sV$b@hMzi+}Iv{ohIRf6cA`oizVpIsfO${%>vI|G%0v=32~@(Xi2ArpaXgj0Yl+ z73rh*(CLU268`G3WI;+jt)x7!sbwaCy}kyvpJEaPQNchWZ9B@%UmPr0D_jCd_6W9RybMl~K-1wCWEaO$)|ZO;PS*iROFZ_HtwMz^W5lYIiB=hxKmd% z#DC#vOZ^(zP`A5eKi!sOO?i3T|F;YR7yhm{K>`PecSK(feLw$nN9o>ZF9(b31VT+m z-zIXZ_<3Rtad)mc{0-%xM5vqmKuYFs_Z_dXrxkUpv!`7wzvY|TG0c1@g1~qz{Kt`W zHC3(#4j>B0cO?C3V+Wh%5~^dMH=(=YxxdjbQ1)bZu{`U&P?vmGzwzv@%jq}{)Ofk3K0145 zV>v-~l5Znur-dEid0#R1sKsK7BjXtE%^l4%xZZm*%3eGI#t+z334qd>M6X) zR?^#gkQ|twq8g)>a``xL)`dhMjs2|9%5&-L_{g4QRpUs_GemoIwCm+9wrC=od9Pxnl0;`ex%L>b$Uq}@CMY|A z`1AvR;$Cuij$=OiM0b}y_hP1nCwbG#$%j0NB2mqER)pZ7z1HRK=JrS9YTq8gEr(Ee zg0jLHyDY*B4`a=^8Nt&5&$kP>jwO#`M#f!Chu^_kdq*9lBv!zU|LpUw#A5qXFkN^* zn_<(Qiq!%Tg2Eq%bsQaT_f657?UkcHdbUD#R6&w)r_hCh<4*(e-oZ=LmiSi`1anki zc&&q!Pw4f>Q+$>#B6v{KPp>wG1YnCc9nS3JSHW~h(!DA5_Lpnx*9dsHg-PXGy3s#2 zRd4BncQYEBj|%xK6=+8)Z4x4SWP5+UC~P`^xcUkygLZvW5BFR48M`F2T;r;o=u|ny z=(@M^Wp@oJs-{2o8y+tEQnAszXi&GxP`}n2Le+m!NpoySvRIFxFFfzyg!@waQA81G zM$jKfLH6vhZEP>R5<&L`>SJ@ko?UHVP7QO=_Q?`+L*ckv#{pK zo#JbfKW+^kVwXQU|7O_>sp(U$kjU(`w9Brky8_hr+4r4YBQUqx00%Hw**?<_mGwI~ zouKTFcggmz-s`eZy`K2&FRG==jCiZ>)UYl)yZ_YN*G!NZahfiD%be^43Nd>3Gq*~v zH&y-=C(tiSk(Oh{uXx9P4Ox^2eD3(@61t~pW;-I|DC|8haC~gL{SiNIIq_$0@=VQV zAIx0HMy*G7@_e-9#uNqe;JkM{wGk{Hv-yg^{kf z3e4?$sX5jbP|;9X6ny9CCr6Xb^9d>neDyKkAgr#P%S86KOHhp+kOuo_rdDVXySEk) zj9}r7*@urHPneEL-MjB_TjWg^ML^bMpqE|qY;o$ogNRYj*x887ANJ|^Q;Fcu5W{><0 zYoBgSWw>+ZV5i_vx~vjm^zBV39NcqAk^8A%)0$^g+zL{DZO|SX&UEtk@Mv!hUN?Jl z=7e;G$$+_*G7-Q*qG+X1f#y@TU8E{CROP0+q2j1Hy+kd{rHckj%g&;?v*ZDIKO!*2_ zFIU-&Ba1tUV;hdnCdJo4*y#P$^diNrYaS!+25aE-zT0+n#Ctwp#hJat0wz?wDDthE zcY;H}?W9rmbtRKQI zdP{nPo?ki5!q2i{Cs$nRdPy47OBdYoh