From 36cbef50b70b2d733f5f1072d3ad4a0f5b96482f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A1lint=20Hodossy?= Date: Mon, 20 Jan 2025 11:09:18 +0000 Subject: [PATCH 01/82] Allow loading obj meshes --- unity/Editor/Importer/MjImporterWithAssets.cs | 26 +++-- unity/Editor/Importer/ObjMeshImportUtility.cs | 94 +++++++++++++++++++ .../Runtime/Components/Shapes/MjMeshShape.cs | 2 +- 3 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 unity/Editor/Importer/ObjMeshImportUtility.cs diff --git a/unity/Editor/Importer/MjImporterWithAssets.cs b/unity/Editor/Importer/MjImporterWithAssets.cs index a9a93184..9937ce57 100644 --- a/unity/Editor/Importer/MjImporterWithAssets.cs +++ b/unity/Editor/Importer/MjImporterWithAssets.cs @@ -153,20 +153,21 @@ public class MjImporterWithAssets : MjcfImporter { var assetReferenceName = MjEngineTool.Sanitize(unsanitizedAssetReferenceName); var sourceFilePath = Path.Combine(_sourceMeshesDir, fileName); - if (Path.GetExtension(sourceFilePath) == ".obj") { - throw new NotImplementedException("OBJ mesh file loading is not yet implemented. " + - "Please convert to binary STL. " + + if (Path.GetExtension(sourceFilePath) != ".obj" && Path.GetExtension(sourceFilePath) != ".stl") { + throw new NotImplementedException("Type of mesh file not yet supported. " + + "Please convert to binary STL or OBJ. " + $"Attempted to load: {sourceFilePath}"); } - var targetFilePath = Path.Combine(_targetMeshesDir, assetReferenceName + ".stl"); + var targetFilePath = Path.Combine(_targetMeshesDir, assetReferenceName + + Path.GetExtension(sourceFilePath)); if (File.Exists(targetFilePath)) { File.Delete(targetFilePath); } var scale = MjEngineTool.UnityVector3( parentNode.GetVector3Attribute("scale", defaultValue: Vector3.one)); CopyMeshAndRescale(sourceFilePath, targetFilePath, scale); - var assetPath = Path.Combine(_targetAssetDir, assetReferenceName + ".stl"); + var assetPath = Path.Combine(_targetAssetDir, assetReferenceName + Path.GetExtension(sourceFilePath)); // This asset path should be available because the MuJoCo compiler guarantees element names // are unique, but check for completeness (and in case sanitizing the name broke uniqueness): if (AssetDatabase.LoadMainAssetAtPath(assetPath) != null) { @@ -174,7 +175,7 @@ public class MjImporterWithAssets : MjcfImporter { $"Trying to import mesh {unsanitizedAssetReferenceName} but {assetPath} already exists."); } AssetDatabase.ImportAsset(assetPath); - var copiedMesh = AssetDatabase.LoadMainAssetAtPath(assetPath) as Mesh; + var copiedMesh = AssetDatabase.LoadAssetAtPath(assetPath); if (copiedMesh == null) { throw new Exception($"Mesh {assetPath} was not imported."); } @@ -186,9 +187,16 @@ public class MjImporterWithAssets : MjcfImporter { private void CopyMeshAndRescale( string sourceFilePath, string targetFilePath, Vector3 scale) { var originalMeshBytes = File.ReadAllBytes(sourceFilePath); - var mesh = StlMeshParser.ParseBinary(originalMeshBytes, scale); - var rescaledMeshBytes = StlMeshParser.SerializeBinary(mesh); - File.WriteAllBytes(targetFilePath, rescaledMeshBytes); + if (Path.GetExtension(sourceFilePath) == ".stl") { + var mesh = StlMeshParser.ParseBinary(originalMeshBytes, scale); + var rescaledMeshBytes = StlMeshParser.SerializeBinary(mesh); + File.WriteAllBytes(targetFilePath, rescaledMeshBytes); + } else if (Path.GetExtension(sourceFilePath) == ".obj") { + ObjMeshImportUtility.CopyAndScaleOBJFile(sourceFilePath, targetFilePath, scale); + } else { + throw new NotImplementedException($"Extension {Path.GetExtension(sourceFilePath)} " + + $"not yet supported for MuJoCo mesh asset."); + } } private void ParseMaterial(XmlElement parentNode) { diff --git a/unity/Editor/Importer/ObjMeshImportUtility.cs b/unity/Editor/Importer/ObjMeshImportUtility.cs new file mode 100644 index 00000000..3e2e9885 --- /dev/null +++ b/unity/Editor/Importer/ObjMeshImportUtility.cs @@ -0,0 +1,94 @@ +// Copyright 2019 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. +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace Mujoco { + + /// + /// Scale vertex data manually line by line. We skip normals. Parameter vertex points + /// (`vp`) was unclear to me how to properly scale them, if you use them and notice an + /// issue please report it. + /// + public static class ObjMeshImportUtility { + private static Vector3 ToXZY(float x, float y, float z) => new Vector3(x, z, y); + + public static void CopyAndScaleOBJFile(string sourceFilePath, string targetFilePath, + Vector3 scale, bool flipFaces=false) { + // OBJ files are human readable + string[] lines = File.ReadAllLines(sourceFilePath); + StringBuilder outputBuilder = new StringBuilder(); + // Culture info for consistent decimal point handling + CultureInfo invariantCulture = CultureInfo.InvariantCulture; + + scale = ToXZY(scale.x, scale.y, scale.z); + foreach (string line in lines) { + if (line.StartsWith("v ")) // Vertex line + { + // Split the line into components + string[] parts = line.Split(' '); + if (parts.Length >= 4) { + // Scale the vertex + float x = -float.Parse(parts[1], invariantCulture) * scale.x; + float y = float.Parse(parts[2], invariantCulture) * scale.y; + float z = float.Parse(parts[3], invariantCulture) * scale.z; + + var swizzled = ToXZY(x, y, z); + outputBuilder.AppendLine( + $"v {swizzled.x.ToString(invariantCulture)} {swizzled.y.ToString(invariantCulture)} {swizzled.z.ToString(invariantCulture)}"); + } + } else if (line.StartsWith("vn ")) { + string[] parts = line.Split(' '); + if (parts.Length >= 4) { + float x = -float.Parse(parts[1], invariantCulture); + float y = float.Parse(parts[2], invariantCulture); + float z = float.Parse(parts[3], invariantCulture); + + var swizzled = ToXZY(x, y, z); + outputBuilder.AppendLine( + $"vn {swizzled.x.ToString(invariantCulture)} {swizzled.y.ToString(invariantCulture)} {swizzled.z.ToString(invariantCulture)}"); + } + } else if (line.StartsWith("f ") && flipFaces) { + string[] parts = line.Split(' '); + if (parts.Length >= 4) { + outputBuilder.Append(parts[0] + " "); + + // Apply same vertex order as STL parser: [0,2,1] + var face = parts.Skip(1).ToArray(); + if (face.Length >= 3) { + outputBuilder.Append(face[0] + " "); // vertex 0 + outputBuilder.Append(face[2] + " "); // vertex 2 + outputBuilder.Append(face[1]); // vertex 1 + + // Append any remaining vertices in original order + for (int i = 3; i < face.Length; i++) { + outputBuilder.Append(" " + face[i]); + } + } + outputBuilder.AppendLine(); + } + } else { + // Copy non-vertex lines as-is + outputBuilder.AppendLine(line); + } + } + // Write the scaled OBJ to the target file + File.WriteAllText(targetFilePath, outputBuilder.ToString()); + } + } +} \ No newline at end of file diff --git a/unity/Runtime/Components/Shapes/MjMeshShape.cs b/unity/Runtime/Components/Shapes/MjMeshShape.cs index e4fa338d..22721209 100644 --- a/unity/Runtime/Components/Shapes/MjMeshShape.cs +++ b/unity/Runtime/Components/Shapes/MjMeshShape.cs @@ -36,7 +36,7 @@ public class MjMeshShape : IMjShape { var assetName = MjEngineTool.Sanitize( mjcf.GetStringAttribute("mesh", defaultValue: string.Empty)); if (!string.IsNullOrEmpty(assetName)) { - Mesh = (Mesh)Resources.Load(assetName); + Mesh = Resources.Load(assetName); } } From a58f8d0033bfad79aa159ba3f11e6d16f988a21c Mon Sep 17 00:00:00 2001 From: Balint-H Date: Tue, 21 Jan 2025 15:11:27 +0000 Subject: [PATCH 02/82] Add OBJ mesh support to Unity plugin --- unity/Editor/Importer/MjImporterWithAssets.cs | 16 ++- unity/Editor/Importer/ObjMeshImportUtility.cs | 131 +++++++++--------- .../Importer/ObjMeshImportUtility.cs.meta | 11 ++ .../Runtime/Components/Shapes/MjMeshFilter.cs | 14 +- 4 files changed, 96 insertions(+), 76 deletions(-) create mode 100644 unity/Editor/Importer/ObjMeshImportUtility.cs.meta diff --git a/unity/Editor/Importer/MjImporterWithAssets.cs b/unity/Editor/Importer/MjImporterWithAssets.cs index 9937ce57..4ece4cae 100644 --- a/unity/Editor/Importer/MjImporterWithAssets.cs +++ b/unity/Editor/Importer/MjImporterWithAssets.cs @@ -174,7 +174,14 @@ public class MjImporterWithAssets : MjcfImporter { throw new Exception( $"Trying to import mesh {unsanitizedAssetReferenceName} but {assetPath} already exists."); } + AssetDatabase.ImportAsset(assetPath); + ModelImporter importer = AssetImporter.GetAtPath(assetPath) as ModelImporter; + if (importer != null && !importer.isReadable) { + importer.isReadable = true; + importer.SaveAndReimport(); + } + var copiedMesh = AssetDatabase.LoadAssetAtPath(assetPath); if (copiedMesh == null) { throw new Exception($"Mesh {assetPath} was not imported."); @@ -283,12 +290,12 @@ public class MjImporterWithAssets : MjcfImporter { // We use the geom's name, guaranteed to be unique, as the asset name. // If geom is nameless, use a random number. var name = - MjEngineTool.Sanitize(parentNode.GetStringAttribute( - "name", defaultValue: $"{UnityEngine.Random.Range(0, 1000000)}")); - var assetPath = Path.Combine(_targetAssetDir, name + ".mat"); + MjEngineTool.Sanitize(parentNode.GetStringAttribute( + "name", defaultValue: $"{UnityEngine.Random.Range(0, 1000000)}")); + var assetPath = Path.Combine(_targetAssetDir, name+".mat"); if (AssetDatabase.LoadMainAssetAtPath(assetPath) != null) { throw new Exception( - $"Creating a material asset for the geom {name}, but {assetPath} already exists."); + $"Creating a material asset for the geom {name}, but {assetPath} already exists."); } AssetDatabase.CreateAsset(material, assetPath); AssetDatabase.SaveAssets(); @@ -297,6 +304,7 @@ public class MjImporterWithAssets : MjcfImporter { material = DefaultMujocoMaterial; } } + if (parentNode.GetFloatAttribute("group") > 2) renderer.enabled = false; renderer.sharedMaterial = material; } } diff --git a/unity/Editor/Importer/ObjMeshImportUtility.cs b/unity/Editor/Importer/ObjMeshImportUtility.cs index 3e2e9885..2eea2211 100644 --- a/unity/Editor/Importer/ObjMeshImportUtility.cs +++ b/unity/Editor/Importer/ObjMeshImportUtility.cs @@ -11,84 +11,85 @@ // 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. + using System; using System.Globalization; using System.IO; using System.Linq; using System.Text; +using UnityEditor; using UnityEngine; namespace Mujoco { - /// - /// Scale vertex data manually line by line. We skip normals. Parameter vertex points - /// (`vp`) was unclear to me how to properly scale them, if you use them and notice an - /// issue please report it. - /// - public static class ObjMeshImportUtility { - private static Vector3 ToXZY(float x, float y, float z) => new Vector3(x, z, y); +/// +/// Scale vertex data manually line by line. We skip normals. Warning: Parameter vertex points +/// (`vp`) were unclear to me how to handle with scaling, if you use them and notice an issue +/// please report it. +/// +public static class ObjMeshImportUtility { + private static Vector3 ToXZY(float x, float y, float z) => new Vector3(x, z, y); - public static void CopyAndScaleOBJFile(string sourceFilePath, string targetFilePath, - Vector3 scale, bool flipFaces=false) { - // OBJ files are human readable - string[] lines = File.ReadAllLines(sourceFilePath); - StringBuilder outputBuilder = new StringBuilder(); - // Culture info for consistent decimal point handling - CultureInfo invariantCulture = CultureInfo.InvariantCulture; + public static void CopyAndScaleOBJFile(string sourceFilePath, string targetFilePath, + Vector3 scale) { + // OBJ files are human readable + string[] lines = File.ReadAllLines(sourceFilePath); + StringBuilder outputBuilder = new StringBuilder(); + // Culture info for consistent decimal point handling + CultureInfo invariantCulture = CultureInfo.InvariantCulture; + scale = ToXZY(scale.x, scale.y, scale.z); + foreach (string line in lines) { + if (line.StartsWith("v ")) // Vertex line + { + // Split the line into components + string[] parts = line.Split(' '); + if (parts.Length >= 4) { + // Scale the vertex. It is unclear to me why flipping along x axis was necessary, + // but without it meshes were incorrectly oriented. + float x = -float.Parse(parts[1], invariantCulture) * scale.x; + float y = float.Parse(parts[2], invariantCulture) * scale.y; + float z = float.Parse(parts[3], invariantCulture) * scale.z; - scale = ToXZY(scale.x, scale.y, scale.z); - foreach (string line in lines) { - if (line.StartsWith("v ")) // Vertex line - { - // Split the line into components - string[] parts = line.Split(' '); - if (parts.Length >= 4) { - // Scale the vertex - float x = -float.Parse(parts[1], invariantCulture) * scale.x; - float y = float.Parse(parts[2], invariantCulture) * scale.y; - float z = float.Parse(parts[3], invariantCulture) * scale.z; - - var swizzled = ToXZY(x, y, z); - outputBuilder.AppendLine( - $"v {swizzled.x.ToString(invariantCulture)} {swizzled.y.ToString(invariantCulture)} {swizzled.z.ToString(invariantCulture)}"); - } - } else if (line.StartsWith("vn ")) { - string[] parts = line.Split(' '); - if (parts.Length >= 4) { - float x = -float.Parse(parts[1], invariantCulture); - float y = float.Parse(parts[2], invariantCulture); - float z = float.Parse(parts[3], invariantCulture); - - var swizzled = ToXZY(x, y, z); - outputBuilder.AppendLine( - $"vn {swizzled.x.ToString(invariantCulture)} {swizzled.y.ToString(invariantCulture)} {swizzled.z.ToString(invariantCulture)}"); - } - } else if (line.StartsWith("f ") && flipFaces) { - string[] parts = line.Split(' '); - if (parts.Length >= 4) { - outputBuilder.Append(parts[0] + " "); - - // Apply same vertex order as STL parser: [0,2,1] - var face = parts.Skip(1).ToArray(); - if (face.Length >= 3) { - outputBuilder.Append(face[0] + " "); // vertex 0 - outputBuilder.Append(face[2] + " "); // vertex 2 - outputBuilder.Append(face[1]); // vertex 1 - - // Append any remaining vertices in original order - for (int i = 3; i < face.Length; i++) { - outputBuilder.Append(" " + face[i]); - } - } - outputBuilder.AppendLine(); - } - } else { - // Copy non-vertex lines as-is - outputBuilder.AppendLine(line); + var swizzled = ToXZY(x, y, z); + outputBuilder.AppendLine( + $"v {swizzled.x.ToString(invariantCulture)} "+ + $"{swizzled.y.ToString(invariantCulture)} "+ + $"{swizzled.z.ToString(invariantCulture)}"); } + } else if (line.StartsWith("vn ")) { + // We swizzle the normals too + string[] parts = line.Split(' '); + if (parts.Length >= 4) { + float x = -float.Parse(parts[1], invariantCulture); + float y = float.Parse(parts[2], invariantCulture); + float z = float.Parse(parts[3], invariantCulture); + + var swizzled = ToXZY(x, y, z); + outputBuilder.AppendLine( + $"vn {swizzled.x.ToString(invariantCulture)} "+ + $"{swizzled.y.ToString(invariantCulture)} "+ + $"{swizzled.z.ToString(invariantCulture)}"); + } + } else if (line.StartsWith("f ") && scale.x*scale.y*scale.z < 0) { + // Faces definition, flip face by reordering vertices + string[] parts = line.Split(' '); + if (parts.Length >= 4) { + outputBuilder.Append(parts[0]+" "); + var face = parts.Skip(1).ToArray(); + if (face.Length >= 3) { + outputBuilder.Append(face[0]+" "); + outputBuilder.Append(face[2]+" "); + outputBuilder.Append(face[1]); + } + outputBuilder.AppendLine(); + } + } else { + // Copy non-vertex lines as-is + outputBuilder.AppendLine(line); } - // Write the scaled OBJ to the target file - File.WriteAllText(targetFilePath, outputBuilder.ToString()); } + // Write the scaled OBJ to the target file + File.WriteAllText(targetFilePath, outputBuilder.ToString()); } +} } \ No newline at end of file diff --git a/unity/Editor/Importer/ObjMeshImportUtility.cs.meta b/unity/Editor/Importer/ObjMeshImportUtility.cs.meta new file mode 100644 index 00000000..d36dd1bb --- /dev/null +++ b/unity/Editor/Importer/ObjMeshImportUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: adec9978c00bb934eb8bf974c26193e1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/unity/Runtime/Components/Shapes/MjMeshFilter.cs b/unity/Runtime/Components/Shapes/MjMeshFilter.cs index 83f4963d..e509da12 100644 --- a/unity/Runtime/Components/Shapes/MjMeshFilter.cs +++ b/unity/Runtime/Components/Shapes/MjMeshFilter.cs @@ -41,19 +41,19 @@ public class MjMeshFilter : MonoBehaviour { return; } - _shapeChangeStamp = currentChangeStamp; - Tuple meshData = _geom.BuildMesh(); - - if (meshData == null) { - throw new ArgumentException("Unsupported geom shape detected"); - } - if(_geom.ShapeType == MjShapeComponent.ShapeTypes.Mesh) { MjMeshShape meshShape = _geom.Shape as MjMeshShape; _meshFilter.sharedMesh = meshShape.Mesh; return; } + _shapeChangeStamp = currentChangeStamp; + Tuple meshData = _geom.BuildMesh(); + if (meshData == null) + { + throw new ArgumentException("Unsupported geom shape detected"); + } + DisposeCurrentMesh(); var mesh = new Mesh(); From b3274bb91da3c41fd286dc41ba79cfcffe97980f Mon Sep 17 00:00:00 2001 From: Balint-H Date: Tue, 21 Jan 2025 15:21:51 +0000 Subject: [PATCH 03/82] Fix formatting --- unity/Editor/Importer/ObjMeshImportUtility.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/unity/Editor/Importer/ObjMeshImportUtility.cs b/unity/Editor/Importer/ObjMeshImportUtility.cs index 2eea2211..262fd128 100644 --- a/unity/Editor/Importer/ObjMeshImportUtility.cs +++ b/unity/Editor/Importer/ObjMeshImportUtility.cs @@ -12,12 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using System.Globalization; using System.IO; using System.Linq; using System.Text; -using UnityEditor; using UnityEngine; namespace Mujoco { @@ -32,7 +30,7 @@ public static class ObjMeshImportUtility { public static void CopyAndScaleOBJFile(string sourceFilePath, string targetFilePath, Vector3 scale) { - // OBJ files are human readable + // OBJ files are human-readable string[] lines = File.ReadAllLines(sourceFilePath); StringBuilder outputBuilder = new StringBuilder(); // Culture info for consistent decimal point handling @@ -92,4 +90,4 @@ public static class ObjMeshImportUtility { File.WriteAllText(targetFilePath, outputBuilder.ToString()); } } -} \ No newline at end of file +} From 621e250d63c5bed23ecc6046c7d6ca5f00304a54 Mon Sep 17 00:00:00 2001 From: Balint-H Date: Fri, 7 Mar 2025 13:16:26 +0000 Subject: [PATCH 04/82] Adjust whitespace to be consistent with old version of scripts --- unity/Editor/Importer/MjImporterWithAssets.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/unity/Editor/Importer/MjImporterWithAssets.cs b/unity/Editor/Importer/MjImporterWithAssets.cs index 4ece4cae..3227a960 100644 --- a/unity/Editor/Importer/MjImporterWithAssets.cs +++ b/unity/Editor/Importer/MjImporterWithAssets.cs @@ -290,12 +290,12 @@ public class MjImporterWithAssets : MjcfImporter { // We use the geom's name, guaranteed to be unique, as the asset name. // If geom is nameless, use a random number. var name = - MjEngineTool.Sanitize(parentNode.GetStringAttribute( - "name", defaultValue: $"{UnityEngine.Random.Range(0, 1000000)}")); + MjEngineTool.Sanitize(parentNode.GetStringAttribute( + "name", defaultValue: $"{UnityEngine.Random.Range(0, 1000000)}")); var assetPath = Path.Combine(_targetAssetDir, name+".mat"); if (AssetDatabase.LoadMainAssetAtPath(assetPath) != null) { throw new Exception( - $"Creating a material asset for the geom {name}, but {assetPath} already exists."); + $"Creating a material asset for the geom {name}, but {assetPath} already exists."); } AssetDatabase.CreateAsset(material, assetPath); AssetDatabase.SaveAssets(); From 744b37753b7fefc763b27b99ed61351c29f24a6c Mon Sep 17 00:00:00 2001 From: Balint-H Date: Mon, 24 Mar 2025 12:02:11 +0000 Subject: [PATCH 05/82] Remove trailing whitespaces --- unity/Editor/Importer/MjImporterWithAssets.cs | 4 ++-- unity/Editor/Importer/ObjMeshImportUtility.cs.meta | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/unity/Editor/Importer/MjImporterWithAssets.cs b/unity/Editor/Importer/MjImporterWithAssets.cs index 3227a960..c724e3a0 100644 --- a/unity/Editor/Importer/MjImporterWithAssets.cs +++ b/unity/Editor/Importer/MjImporterWithAssets.cs @@ -159,8 +159,8 @@ public class MjImporterWithAssets : MjcfImporter { $"Attempted to load: {sourceFilePath}"); } - var targetFilePath = Path.Combine(_targetMeshesDir, assetReferenceName - + Path.GetExtension(sourceFilePath)); + var targetFilePath = + Path.Combine(_targetMeshesDir, assetReferenceName + Path.GetExtension(sourceFilePath)); if (File.Exists(targetFilePath)) { File.Delete(targetFilePath); } diff --git a/unity/Editor/Importer/ObjMeshImportUtility.cs.meta b/unity/Editor/Importer/ObjMeshImportUtility.cs.meta index d36dd1bb..195f099c 100644 --- a/unity/Editor/Importer/ObjMeshImportUtility.cs.meta +++ b/unity/Editor/Importer/ObjMeshImportUtility.cs.meta @@ -6,6 +6,6 @@ MonoImporter: defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: + userData: + assetBundleName: + assetBundleVariant: From 047df4e87109e25e0579f72f9d4945da3409c851 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Wed, 4 Jun 2025 13:15:06 -0700 Subject: [PATCH 06/82] Add texture to rotating cylinders in pulley example. PiperOrigin-RevId: 767260834 Change-Id: I3030be314b5b197163d75ef88a5fd99a940d9c67 --- model/flex/pulley.xml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/model/flex/pulley.xml b/model/flex/pulley.xml index 5e07430d..38223546 100644 --- a/model/flex/pulley.xml +++ b/model/flex/pulley.xml @@ -22,6 +22,12 @@ + + + + + @@ -30,12 +36,12 @@ - + - + From dd18dd202448be934d206feaa87d8839f6d8a541 Mon Sep 17 00:00:00 2001 From: Google DeepMind Date: Thu, 5 Jun 2025 03:02:18 -0700 Subject: [PATCH 07/82] Apply metallic and roughness scaling factors. PiperOrigin-RevId: 767518801 Change-Id: I7eff7b7a56cfbb9c6e46ac22011559eabf401c07 --- doc/XMLreference.rst | 12 ++++++++---- src/user/user_init.c | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 588235ab..71963d33 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -1776,15 +1776,19 @@ properties are grouped together. .. _asset-material-metallic: -:at:`metallic`: :at-val:`real, "0"` +:at:`metallic`: :at-val:`real, "-1"` This attribute corresponds to uniform metallicity coefficient applied to the entire material. This attribute has no - effect in MuJoCo's native renderer, but it can be useful when rendering scenes with an external renderer. + effect in MuJoCo's native renderer, but it can be useful when rendering scenes with a physically-based renderer. In + this case, if a non-negative value is specified, this metallic value should be multiplied by the metallic texture + sampled value to obtain the final metallicity of the material. .. _asset-material-roughness: -:at:`roughness`: :at-val:`real, "1"` +:at:`roughness`: :at-val:`real, "-1"` This attribute corresponds to uniform roughness coefficient applied to the entire material. This attribute has no - effect in MuJoCo's native renderer, but it can be useful when rendering scenes with an external renderer. + effect in MuJoCo's native renderer, but it can be useful when rendering scenes with a physically-based renderer. In + this case, if a non-negative value is specified, this roughness value should be multiplied by the roughness texture + sampled value to obtain the final roughness of the material. .. _asset-material-rgba: diff --git a/src/user/user_init.c b/src/user/user_init.c index 34199812..575f15d6 100644 --- a/src/user/user_init.c +++ b/src/user/user_init.c @@ -291,8 +291,8 @@ void mjs_defaultMaterial(mjsMaterial* material) { material->texrepeat[0] = material->texrepeat[1] = 1; material->specular = 0.5; material->shininess = 0.5; - material->metallic = 0.0; - material->roughness = 1.0; + material->metallic = -1.0; + material->roughness = -1.0; material->rgba[0] = material->rgba[1] = material->rgba[2] = material->rgba[3] = 1; } From f75772587ac08a71dc4fe3a9cd9fdffd6ec2e2be Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Mon, 9 Jun 2025 02:15:48 -0700 Subject: [PATCH 08/82] Add functions for setting an `mjsActuator` to a specific shortcut type. PiperOrigin-RevId: 769049331 Change-Id: If5bb04966bd29a57569a2c17d712b9ae3d33324d --- doc/includes/references.h | 13 ++ include/mujoco/mujoco.h | 32 ++++ python/mujoco/introspect/functions.py | 260 ++++++++++++++++++++++++++ python/mujoco/specs.cc | 89 +++++++++ python/mujoco/specs_test.py | 56 ++++++ src/user/user_api.cc | 182 ++++++++++++++++++ src/user/user_api.h | 31 +++ src/xml/xml_native_reader.cc | 199 ++++++-------------- 8 files changed, 723 insertions(+), 139 deletions(-) diff --git a/doc/includes/references.h b/doc/includes/references.h index 3f725ad9..1d539d56 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -3438,6 +3438,19 @@ mjsTuple* mjs_addTuple(mjSpec* s); mjsKey* mjs_addKey(mjSpec* s); mjsPlugin* mjs_addPlugin(mjSpec* s); mjsDefault* mjs_addDefault(mjSpec* s, const char* classname, const mjsDefault* parent); +const char* mjs_setToMotor(mjsActuator* actuator); +const char* mjs_setToPosition(mjsActuator* actuator, double kp, double kv[1], + double dampratio[1], double timeconst[1], double inheritrange); +const char* mjs_setToIntVelocity(mjsActuator* actuator, double kp, double kv[1], + double dampratio[1], double timeconst[1], double inheritrange); +const char* mjs_setToVelocity(mjsActuator* actuator, double kv); +const char* mjs_setToDamper(mjsActuator* actuator, double kv); +const char* mjs_setToCylinder(mjsActuator* actuator, double timeconst, + double bias, double area, double diameter); +const char* mjs_setToMuscle(mjsActuator* actuator, double timeconst[2], double tausmooth, + double range[2], double force, double scale, double lmin, + double lmax, double vmax, double fpmax, double fvmax); +const char* mjs_setToAdhesion(mjsActuator* actuator, double gain); mjsMesh* mjs_addMesh(mjSpec* s, const mjsDefault* def); mjsHField* mjs_addHField(mjSpec* s); mjsSkin* mjs_addSkin(mjSpec* s); diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index 59283756..bc9242a2 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -1501,6 +1501,38 @@ MJAPI mjsPlugin* mjs_addPlugin(mjSpec* s); MJAPI mjsDefault* mjs_addDefault(mjSpec* s, const char* classname, const mjsDefault* parent); +//---------------------------------- Set actuator parameters --------------------------------------- + +// Set actuator to motor, return error if any. +MJAPI const char* mjs_setToMotor(mjsActuator* actuator); + +// Set actuator to position, return error if any. +MJAPI const char* mjs_setToPosition(mjsActuator* actuator, double kp, double kv[1], + double dampratio[1], double timeconst[1], double inheritrange); + +// Set actuator to integrated velocity, return error if any. +MJAPI const char* mjs_setToIntVelocity(mjsActuator* actuator, double kp, double kv[1], + double dampratio[1], double timeconst[1], double inheritrange); + +// Set actuator to velocity servo, return error if any. +MJAPI const char* mjs_setToVelocity(mjsActuator* actuator, double kv); + +// Set actuator to activate damper, return error if any. +MJAPI const char* mjs_setToDamper(mjsActuator* actuator, double kv); + +// Set actuator to hydraulic or pneumatic cylinder, return error if any. +MJAPI const char* mjs_setToCylinder(mjsActuator* actuator, double timeconst, + double bias, double area, double diameter); + +// Set actuator to muscle, return error if any.a +MJAPI const char* mjs_setToMuscle(mjsActuator* actuator, double timeconst[2], double tausmooth, + double range[2], double force, double scale, double lmin, + double lmax, double vmax, double fpmax, double fvmax); + +// Set actuator to active adhesion, return error if any. +MJAPI const char* mjs_setToAdhesion(mjsActuator* actuator, double gain); + + //---------------------------------- Assets -------------------------------------------------------- // Add mesh. diff --git a/python/mujoco/introspect/functions.py b/python/mujoco/introspect/functions.py index 1c4877b9..680122a1 100644 --- a/python/mujoco/introspect/functions.py +++ b/python/mujoco/introspect/functions.py @@ -9551,6 +9551,266 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), doc='Add default.', )), + ('mjs_setToMotor', + FunctionDecl( + name='mjs_setToMotor', + return_type=PointerType( + inner_type=ValueType(name='char', is_const=True), + ), + parameters=( + FunctionParameterDecl( + name='actuator', + type=PointerType( + inner_type=ValueType(name='mjsActuator'), + ), + ), + ), + doc='Set actuator to motor, return error if any.', + )), + ('mjs_setToPosition', + FunctionDecl( + name='mjs_setToPosition', + return_type=PointerType( + inner_type=ValueType(name='char', is_const=True), + ), + parameters=( + FunctionParameterDecl( + name='actuator', + type=PointerType( + inner_type=ValueType(name='mjsActuator'), + ), + ), + FunctionParameterDecl( + name='kp', + type=ValueType(name='double'), + ), + FunctionParameterDecl( + name='kv', + type=ArrayType( + inner_type=ValueType(name='double'), + extents=(1,), + ), + ), + FunctionParameterDecl( + name='dampratio', + type=ArrayType( + inner_type=ValueType(name='double'), + extents=(1,), + ), + ), + FunctionParameterDecl( + name='timeconst', + type=ArrayType( + inner_type=ValueType(name='double'), + extents=(1,), + ), + ), + FunctionParameterDecl( + name='inheritrange', + type=ValueType(name='double'), + ), + ), + doc='Set actuator to position, return error if any.', + )), + ('mjs_setToIntVelocity', + FunctionDecl( + name='mjs_setToIntVelocity', + return_type=PointerType( + inner_type=ValueType(name='char', is_const=True), + ), + parameters=( + FunctionParameterDecl( + name='actuator', + type=PointerType( + inner_type=ValueType(name='mjsActuator'), + ), + ), + FunctionParameterDecl( + name='kp', + type=ValueType(name='double'), + ), + FunctionParameterDecl( + name='kv', + type=ArrayType( + inner_type=ValueType(name='double'), + extents=(1,), + ), + ), + FunctionParameterDecl( + name='dampratio', + type=ArrayType( + inner_type=ValueType(name='double'), + extents=(1,), + ), + ), + FunctionParameterDecl( + name='timeconst', + type=ArrayType( + inner_type=ValueType(name='double'), + extents=(1,), + ), + ), + FunctionParameterDecl( + name='inheritrange', + type=ValueType(name='double'), + ), + ), + doc='Set actuator to integrated velocity, return error if any.', + )), + ('mjs_setToVelocity', + FunctionDecl( + name='mjs_setToVelocity', + return_type=PointerType( + inner_type=ValueType(name='char', is_const=True), + ), + parameters=( + FunctionParameterDecl( + name='actuator', + type=PointerType( + inner_type=ValueType(name='mjsActuator'), + ), + ), + FunctionParameterDecl( + name='kv', + type=ValueType(name='double'), + ), + ), + doc='Set actuator to velocity servo, return error if any.', + )), + ('mjs_setToDamper', + FunctionDecl( + name='mjs_setToDamper', + return_type=PointerType( + inner_type=ValueType(name='char', is_const=True), + ), + parameters=( + FunctionParameterDecl( + name='actuator', + type=PointerType( + inner_type=ValueType(name='mjsActuator'), + ), + ), + FunctionParameterDecl( + name='kv', + type=ValueType(name='double'), + ), + ), + doc='Set actuator to activate damper, return error if any.', + )), + ('mjs_setToCylinder', + FunctionDecl( + name='mjs_setToCylinder', + return_type=PointerType( + inner_type=ValueType(name='char', is_const=True), + ), + parameters=( + FunctionParameterDecl( + name='actuator', + type=PointerType( + inner_type=ValueType(name='mjsActuator'), + ), + ), + FunctionParameterDecl( + name='timeconst', + type=ValueType(name='double'), + ), + FunctionParameterDecl( + name='bias', + type=ValueType(name='double'), + ), + FunctionParameterDecl( + name='area', + type=ValueType(name='double'), + ), + FunctionParameterDecl( + name='diameter', + type=ValueType(name='double'), + ), + ), + doc='Set actuator to hydraulic or pneumatic cylinder, return error if any.', # pylint: disable=line-too-long + )), + ('mjs_setToMuscle', + FunctionDecl( + name='mjs_setToMuscle', + return_type=PointerType( + inner_type=ValueType(name='char', is_const=True), + ), + parameters=( + FunctionParameterDecl( + name='actuator', + type=PointerType( + inner_type=ValueType(name='mjsActuator'), + ), + ), + FunctionParameterDecl( + name='timeconst', + type=ArrayType( + inner_type=ValueType(name='double'), + extents=(2,), + ), + ), + FunctionParameterDecl( + name='tausmooth', + type=ValueType(name='double'), + ), + FunctionParameterDecl( + name='range', + type=ArrayType( + inner_type=ValueType(name='double'), + extents=(2,), + ), + ), + FunctionParameterDecl( + name='force', + type=ValueType(name='double'), + ), + FunctionParameterDecl( + name='scale', + type=ValueType(name='double'), + ), + FunctionParameterDecl( + name='lmin', + type=ValueType(name='double'), + ), + FunctionParameterDecl( + name='lmax', + type=ValueType(name='double'), + ), + FunctionParameterDecl( + name='vmax', + type=ValueType(name='double'), + ), + FunctionParameterDecl( + name='fpmax', + type=ValueType(name='double'), + ), + FunctionParameterDecl( + name='fvmax', + type=ValueType(name='double'), + ), + ), + doc='Set actuator to muscle, return error if any.a', + )), + ('mjs_setToAdhesion', + FunctionDecl( + name='mjs_setToAdhesion', + return_type=PointerType( + inner_type=ValueType(name='char', is_const=True), + ), + parameters=( + FunctionParameterDecl( + name='actuator', + type=PointerType( + inner_type=ValueType(name='mjsActuator'), + ), + ), + FunctionParameterDecl( + name='gain', + type=ValueType(name='double'), + ), + ), + doc='Set actuator to active adhesion, return error if any.', + )), ('mjs_addMesh', FunctionDecl( name='mjs_addMesh', diff --git a/python/mujoco/specs.cc b/python/mujoco/specs.cc index 51846d2a..b2d081e6 100644 --- a/python/mujoco/specs.cc +++ b/python/mujoco/specs.cc @@ -1010,6 +1010,95 @@ PYBIND11_MODULE(_specs, m) { [](raw::MjsActuator& self, raw::MjsDefault& default_) -> void { mjs_setDefault(self.element, &default_); }); + mjsActuator.def("set_to_motor", [](raw::MjsActuator* self) { + std::string err = mjs_setToMotor(self); + if (!err.empty()) { + throw pybind11::value_error(err); + } + }); + mjsActuator.def( + "set_to_position", + [](raw::MjsActuator* self, double kp, double kv, double dampratio, + double timeconst, bool inheritrange) { + std::string err = mjs_setToPosition( + self, kp, kv == -1 ? nullptr : &kv, + dampratio == -1 ? nullptr : &dampratio, + timeconst == -1 ? nullptr : &timeconst, inheritrange); + if (!err.empty()) { + throw pybind11::value_error(err); + } + }, + py::arg("kp"), py::arg("kv") = -1, py::arg("dampratio") = -1, + py::arg("timeconst") = -1, py::arg("inheritrange") = false); + mjsActuator.def( + "set_to_intvelocity", + [](raw::MjsActuator* self, double kp, double kv, double dampratio, + double timeconst, bool inheritrange) { + std::string err = mjs_setToIntVelocity( + self, kp, kv == -1 ? nullptr : &kv, + dampratio == -1 ? nullptr : &dampratio, + timeconst == -1 ? nullptr : &timeconst, inheritrange); + if (!err.empty()) { + throw pybind11::value_error(err); + } + }, + py::arg("kp"), py::arg("kv") = -1, py::arg("dampratio") = -1, + py::arg("timeconst") = -1, py::arg("inheritrange") = false); + mjsActuator.def( + "set_to_velocity", + [](raw::MjsActuator* self, double kv) { + std::string err = mjs_setToVelocity(self, kv); + if (!err.empty()) { + throw pybind11::value_error(err); + } + }, + py::arg("kv")); + mjsActuator.def( + "set_to_damper", + [](raw::MjsActuator* self, double kv) { + std::string err = mjs_setToDamper(self, kv); + if (!err.empty()) { + throw pybind11::value_error(err); + } + }, + py::arg("kv")); + mjsActuator.def( + "set_to_cylinder", + [](raw::MjsActuator* self, double timeconst, double bias, double area, + double diameter) { + std::string err = + mjs_setToCylinder(self, timeconst, bias, area, diameter); + if (!err.empty()) { + throw pybind11::value_error(err); + } + }, + py::arg("timeconst"), py::arg("bias"), py::arg("area"), + py::arg("diameter") = -1); + mjsActuator.def( + "set_to_muscle", + [](raw::MjsActuator* self, double timeconst[2], double tausmooth, + double range[2], double force, double scale, double lmin, double lmax, + double vmax, double fpmax, double fvmax) { + std::string err = + mjs_setToMuscle(self, timeconst, tausmooth, range, force, scale, + lmin, lmax, vmax, fpmax, fvmax); + if (!err.empty()) { + throw pybind11::value_error(err); + } + }, + py::arg("timeconst") = -1, py::arg("tausmooth"), + py::arg("range") = std::array{-1, -1}, py::arg("force") = -1, + py::arg("scale") = -1, py::arg("lmin") = -1, py::arg("lmax") = -1, + py::arg("vmax") = -1, py::arg("fpmax") = -1, py::arg("fvmax") = -1); + mjsActuator.def( + "set_to_adhesion", + [](raw::MjsActuator* self, double gain) { + std::string err = mjs_setToAdhesion(self, gain); + if (!err.empty()) { + throw pybind11::value_error(err); + } + }, + py::arg("gain")); // ============================= MJSTENDON =================================== mjsTendon.def("delete", diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py index 41a2043c..bf712505 100644 --- a/python/mujoco/specs_test.py +++ b/python/mujoco/specs_test.py @@ -1232,5 +1232,61 @@ class SpecsTest(absltest.TestCase): self.assertGreater(spec3._address, 0) self.assertLen({spec1._address, spec2._address, spec3._address}, 3) + def test_actuator_shortname(self): + spec = mujoco.MjSpec() + actuator = spec.add_actuator( + gainprm=np.zeros((10, 1)), + dyntype=mujoco.mjtDyn.mjDYN_FILTER, + gaintype=mujoco.mjtGain.mjGAIN_AFFINE, + biastype=mujoco.mjtBias.mjBIAS_AFFINE, + ) + actuator.set_to_motor() + self.assertEqual(actuator.gainprm[0], 1) + self.assertEqual(actuator.dyntype, mujoco.mjtDyn.mjDYN_NONE) + self.assertEqual(actuator.gaintype, mujoco.mjtGain.mjGAIN_FIXED) + self.assertEqual(actuator.biastype, mujoco.mjtBias.mjBIAS_NONE) + + actuator.set_to_position(kp=2.0, kv=3.0, timeconst=4.0, inheritrange=True) + self.assertEqual(actuator.gainprm[0], 2) + self.assertEqual(actuator.biasprm[1], -2) + self.assertEqual(actuator.biasprm[2], -3) + self.assertEqual(actuator.dynprm[0], 4) + self.assertEqual(actuator.dyntype, mujoco.mjtDyn.mjDYN_FILTEREXACT) + self.assertEqual(actuator.gaintype, mujoco.mjtGain.mjGAIN_FIXED) + self.assertEqual(actuator.biastype, mujoco.mjtBias.mjBIAS_AFFINE) + self.assertEqual(actuator.inheritrange, True) + + actuator.set_to_intvelocity( + kp=2.0, kv=3.0, timeconst=4.0, inheritrange=True + ) + self.assertEqual(actuator.gainprm[0], 2) + self.assertEqual(actuator.biasprm[1], -2) + self.assertEqual(actuator.biasprm[2], -3) + self.assertEqual(actuator.dynprm[0], 4) + self.assertEqual(actuator.dyntype, mujoco.mjtDyn.mjDYN_INTEGRATOR) + self.assertEqual(actuator.gaintype, mujoco.mjtGain.mjGAIN_FIXED) + self.assertEqual(actuator.biastype, mujoco.mjtBias.mjBIAS_AFFINE) + self.assertEqual(actuator.inheritrange, True) + + actuator.set_to_velocity(kv=5.0) + self.assertEqual(actuator.gainprm[0], 5) + self.assertEqual(actuator.biasprm[2], -5) + self.assertEqual(actuator.dyntype, mujoco.mjtDyn.mjDYN_NONE) + self.assertEqual(actuator.gaintype, mujoco.mjtGain.mjGAIN_FIXED) + self.assertEqual(actuator.biastype, mujoco.mjtBias.mjBIAS_AFFINE) + + actuator.set_to_damper(kv=6.0) + self.assertEqual(actuator.gainprm[0], 0) + self.assertEqual(actuator.gainprm[2], -6) + self.assertEqual(actuator.dyntype, mujoco.mjtDyn.mjDYN_NONE) + self.assertEqual(actuator.gaintype, mujoco.mjtGain.mjGAIN_AFFINE) + self.assertEqual(actuator.biastype, mujoco.mjtBias.mjBIAS_NONE) + + actuator.set_to_adhesion(gain=7.0) + self.assertEqual(actuator.gainprm[0], 7) + self.assertEqual(actuator.dyntype, mujoco.mjtDyn.mjDYN_NONE) + self.assertEqual(actuator.gaintype, mujoco.mjtGain.mjGAIN_FIXED) + self.assertEqual(actuator.biastype, mujoco.mjtBias.mjBIAS_NONE) + if __name__ == '__main__': absltest.main() diff --git a/src/user/user_api.cc b/src/user/user_api.cc index c25049b1..eea4aa93 100644 --- a/src/user/user_api.cc +++ b/src/user/user_api.cc @@ -695,6 +695,188 @@ mjsDefault* mjs_addDefault(mjSpec* s, const char* classname, const mjsDefault* p +// set actuator to motor +const char* mjs_setToMotor(mjsActuator* actuator) { + // unit gain + actuator->gainprm[0] = 1; + + // implied parameters + actuator->dyntype = mjDYN_NONE; + actuator->gaintype = mjGAIN_FIXED; + actuator->biastype = mjBIAS_NONE; + return ""; +} + + + +// set to position actuator +const char* mjs_setToPosition(mjsActuator* actuator, double kp, double kv[1], + double dampratio[1], double timeconst[1], double inheritrange) { + actuator->gainprm[0] = kp; + actuator->biasprm[1] = -kp; + + // set biasprm[2]; negative: regular damping, positive: dampratio + if (dampratio && kv) { + return "kv and dampratio cannot both be defined"; + } + + if (kv) { + if (*kv < 0) return "kv cannot be negative"; + actuator->biasprm[2] = -(*kv); + } + if (dampratio) { + if (*dampratio < 0) return "dampratio cannot be negative"; + actuator->biasprm[2] = *dampratio; + } + if (timeconst) { + if (*timeconst < 0) return "timeconst cannot be negative"; + actuator->dynprm[0] = *timeconst; + actuator->dyntype = *timeconst == 0 ? mjDYN_NONE : mjDYN_FILTEREXACT; + } + actuator->inheritrange = inheritrange; + + if (inheritrange > 0) { + if (actuator->ctrlrange[0] || actuator->ctrlrange[1]) { + return "ctrlrange and inheritrange cannot both be defined"; + } + } + + actuator->gaintype = mjGAIN_FIXED; + actuator->biastype = mjBIAS_AFFINE; + return ""; +} + + + +// Set to integrated velocity actuator. +const char* mjs_setToIntVelocity(mjsActuator* actuator, double kp, double kv[1], + double dampratio[1], double timeconst[1], double inheritrange) { + mjs_setToPosition(actuator, kp, kv, dampratio, timeconst, inheritrange); + actuator->dyntype = mjDYN_INTEGRATOR; + actuator->actlimited = 1; + + if (inheritrange > 0) { + if (actuator->actrange[0] || actuator->actrange[1]) { + return "actrange and inheritrange cannot both be defined"; + } + } + return ""; +} + + + +// Set to velocity actuator. +const char* mjs_setToVelocity(mjsActuator* actuator, double kv) { + mjuu_zerovec(actuator->biasprm, mjNBIAS); + actuator->gainprm[0] = kv; + actuator->biasprm[2] = -kv; + actuator->dyntype = mjDYN_NONE; + actuator->gaintype = mjGAIN_FIXED; + actuator->biastype = mjBIAS_AFFINE; + return ""; +} + + + +// Set to damper actuator. +const char* mjs_setToDamper(mjsActuator* actuator, double kv) { + mjuu_zerovec(actuator->gainprm, mjNGAIN); + actuator->gainprm[2] = -kv; + actuator->ctrllimited = 1; + actuator->dyntype = mjDYN_NONE; + actuator->gaintype = mjGAIN_AFFINE; + actuator->biastype = mjBIAS_NONE; + + if (kv < 0) { + return "damping coefficient cannot be negative"; + } + if (actuator->ctrlrange[0] < 0 || actuator->ctrlrange[1] < 0) { + return "damper control range cannot be negative"; + } + return ""; +} + + + +// Set to cylinder actuator. +const char* mjs_setToCylinder(mjsActuator* actuator, double timeconst, double bias, + double area, double diameter) { + actuator->dynprm[0] = timeconst; + actuator->biasprm[0] = bias; + actuator->gainprm[0] = area; + if (diameter >= 0) { + actuator->gainprm[0] = mjPI / 4 * diameter*diameter; + } + actuator->dyntype = mjDYN_FILTER; + actuator->gaintype = mjGAIN_FIXED; + actuator->biastype = mjBIAS_AFFINE; + return ""; +} + + + +// Set to muscle actuator. +const char* mjs_setToMuscle(mjsActuator* actuator, double timeconst[2], double tausmooth, + double range[2], double force, double scale, double lmin, + double lmax, double vmax, double fpmax, double fvmax) { + // set muscle defaults if same as global defaults + if (actuator->dynprm[0] == 1) actuator->dynprm[0] = 0.01; // tau act + if (actuator->dynprm[1] == 0) actuator->dynprm[1] = 0.04; // tau deact + if (actuator->gainprm[0] == 1) actuator->gainprm[0] = 0.75; // range[0] + if (actuator->gainprm[1] == 0) actuator->gainprm[1] = 1.05; // range[1] + if (actuator->gainprm[2] == 0) actuator->gainprm[2] = -1; // force + if (actuator->gainprm[3] == 0) actuator->gainprm[3] = 200; // scale + if (actuator->gainprm[4] == 0) actuator->gainprm[4] = 0.5; // lmin + if (actuator->gainprm[5] == 0) actuator->gainprm[5] = 1.6; // lmax + if (actuator->gainprm[6] == 0) actuator->gainprm[6] = 1.5; // vmax + if (actuator->gainprm[7] == 0) actuator->gainprm[7] = 1.3; // fpmax + if (actuator->gainprm[8] == 0) actuator->gainprm[8] = 1.2; // fvmax + + if (tausmooth < 0) + return "muscle tausmooth cannot be negative"; + + actuator->dynprm[2] = tausmooth; + if (timeconst[0] >= 0) actuator->dynprm[0] = timeconst[0]; + if (timeconst[1] >= 0) actuator->dynprm[1] = timeconst[1]; + if (range[0] >= 0) actuator->gainprm[0] = range[0]; + if (range[1] >= 0) actuator->gainprm[1] = range[1]; + if (force >= 0) actuator->gainprm[2] = force; + if (scale >= 0) actuator->gainprm[3] = scale; + if (lmin >= 0) actuator->gainprm[4] = lmin; + if (lmax >= 0) actuator->gainprm[5] = lmax; + if (vmax >= 0) actuator->gainprm[6] = vmax; + if (fpmax >= 0) actuator->gainprm[7] = fpmax; + if (fvmax >= 0) actuator->gainprm[8] = fvmax; + + // biasprm = gainprm + for (int n=0; n < 9; n++) { + actuator->biasprm[n] = actuator->gainprm[n]; + } + + actuator->dyntype = mjDYN_MUSCLE; + actuator->gaintype = mjGAIN_MUSCLE; + actuator->biastype = mjBIAS_MUSCLE; + return ""; +} + + + +// Set to adhesion actuator. +const char* mjs_setToAdhesion(mjsActuator* actuator, double gain) { + actuator->gainprm[0] = gain; + actuator->ctrllimited = 1; + actuator->gaintype = mjGAIN_FIXED; + actuator->biastype = mjBIAS_NONE; + + if (gain < 0) + return "adhesion gain cannot be negative"; + if (actuator->ctrlrange[0] < 0 || actuator->ctrlrange[1] < 0) + return "adhesion control range cannot be negative"; + return ""; +} + + + // get spec from body mjSpec* mjs_getSpec(mjsElement* element) { return &(static_cast(element)->model->spec); diff --git a/src/user/user_api.h b/src/user/user_api.h index 04cdf13e..37825a76 100644 --- a/src/user/user_api.h +++ b/src/user/user_api.h @@ -166,6 +166,37 @@ MJAPI mjsPlugin* mjs_addPlugin(mjSpec* s); MJAPI mjsDefault* mjs_addDefault(mjSpec* s, const char* classname, const mjsDefault* parent); +//---------------------------------- Set actuator parameters --------------------------------------- + +// Set actuator to motor, return error on failure. +MJAPI const char* mjs_setToMotor(mjsActuator* actuator); + +// Set actuator to position, return error on failure. +MJAPI const char* mjs_setToPosition(mjsActuator* actuator, double kp, double kv[1], + double dampratio[1], double timeconst[1], double inheritrange); + +// Set actuator to integrated velocity, return error on failure. +MJAPI const char* mjs_setToIntVelocity(mjsActuator* actuator, double kp, double kv[1], + double dampratio[1], double timeconst[1], double inheritrange); + +// Set actuator to velocity, return error on failure. +MJAPI const char* mjs_setToVelocity(mjsActuator* actuator, double kv); + +// Set actuator to damper, return error on failure. +MJAPI const char* mjs_setToDamper(mjsActuator* actuator, double kv); + +// Set actuator to cylinder actuator, return error on failure. +MJAPI const char* mjs_setToCylinder(mjsActuator* actuator, double timeconst, + double bias, double area, double diameter); + +// Set actuator to muscle, return error on failure. +MJAPI const char* mjs_setToMuscle(mjsActuator* actuator, double timeconst[2], double tausmooth, + double range[2], double force, double scale, double lmin, + double lmax, double vmax, double fpmax, double fvmax); + +// Set actuator to adhesion, return error on failure. +MJAPI const char* mjs_setToAdhesion(mjsActuator* actuator, double gain); + //---------------------------------- Add assets ---------------------------------------------------- // Add mesh. diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index c5ad9c8f..e8a7dc13 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -2210,6 +2210,7 @@ void mjXReader::OneActuator(XMLElement* elem, mjsActuator* actuator) { type = elem->Value(); // explicit attributes + string err; if (type == "general") { // explicit attributes int n; @@ -2233,183 +2234,98 @@ void mjXReader::OneActuator(XMLElement* elem, mjsActuator* actuator) { // direct drive motor else if (type == "motor") { - // unit gain - actuator->gainprm[0] = 1; - - // implied parameters - actuator->dyntype = mjDYN_NONE; - actuator->gaintype = mjGAIN_FIXED; - actuator->biastype = mjBIAS_NONE; + err = mjs_setToMotor(actuator); } // position or integrated velocity servo else if (type == "position" || type == "intvelocity") { - // explicit attributes - ReadAttr(elem, "kp", 1, actuator->gainprm, text); - actuator->biasprm[1] = -actuator->gainprm[0]; + double kp = actuator->gainprm[0]; + ReadAttr(elem, "kp", 1, &kp, text); // read kv - double kv = -1; // -1: undefined - if (ReadAttr(elem, "kv", 1, &kv, text)) { - if (kv < 0) throw mjXError(elem, "kv cannot be negative"); + double kv_data; + double *kv = &kv_data; + if (!ReadAttr(elem, "kv", 1, kv, text)) { + kv = nullptr; } // read dampratio - double dampratio = -1; // -1: undefined - if (ReadAttr(elem, "dampratio", 1, &dampratio, text)) { - if (dampratio < 0) throw mjXError(elem, "dampratio cannot be negative"); + double dampratio_data; + double *dampratio = &dampratio_data; + if (!ReadAttr(elem, "dampratio", 1, dampratio, text)) { + dampratio = nullptr; } - // set biasprm[2]; negative: regular damping, positive: dampratio - if (dampratio > 0 && kv > 0) { - throw mjXError(elem, "kv and dampratio cannot both be defined"); - } - if (kv > 0) actuator->biasprm[2] = -kv; - if (dampratio > 0) actuator->biasprm[2] = dampratio; - // read timeconst, set dyntype - if (ReadAttr(elem, "timeconst", 1, actuator->dynprm, text)) { - if (actuator->dynprm[0] < 0) - throw mjXError(elem, "timeconst cannot be negative"); - actuator->dyntype = actuator->dynprm[0] ? mjDYN_FILTEREXACT : mjDYN_NONE; + double timeconst_data; + double *timeconst = &timeconst_data; + if (!ReadAttr(elem, "timeconst", 1, timeconst, text)) { + timeconst = nullptr; } // handle inheritrange - ReadAttr(elem, "inheritrange", 1, &actuator->inheritrange, text); - if (actuator->inheritrange > 0) { - if (type == "position") { - if (actuator->ctrlrange[0] || actuator->ctrlrange[1]) { - throw mjXError(elem, "ctrlrange and inheritrange cannot both be defined"); - } - } else { - if (actuator->actrange[0] || actuator->actrange[1]) { - throw mjXError(elem, "actrange and inheritrange cannot both be defined"); - } - } - } + double inheritrange = actuator->inheritrange; + ReadAttr(elem, "inheritrange", 1, &inheritrange, text); - // implied parameters - actuator->gaintype = mjGAIN_FIXED; - actuator->biastype = mjBIAS_AFFINE; - - if (type == "intvelocity") { - actuator->dyntype = mjDYN_INTEGRATOR; - actuator->actlimited = 1; + if (type == "position") { + err = mjs_setToPosition(actuator, kp, kv, dampratio, timeconst, inheritrange); + } else { + err = mjs_setToIntVelocity(actuator, kp, kv, dampratio, timeconst, inheritrange); } } // velocity servo else if (type == "velocity") { - // clear bias - mjuu_zerovec(actuator->biasprm, mjNBIAS); - - // explicit attributes - ReadAttr(elem, "kv", 1, actuator->gainprm, text); - actuator->biasprm[2] = -actuator->gainprm[0]; - - // implied parameters - actuator->dyntype = mjDYN_NONE; - actuator->gaintype = mjGAIN_FIXED; - actuator->biastype = mjBIAS_AFFINE; + double kv = actuator->gainprm[0]; + ReadAttr(elem, "kv", 1, &kv, text); + err = mjs_setToVelocity(actuator, kv); } // damper else if (type == "damper") { - // clear gain - mjuu_zerovec(actuator->gainprm, mjNGAIN); - - // explicit attributes - ReadAttr(elem, "kv", 1, actuator->gainprm+2, text); - if (actuator->gainprm[2] < 0) - throw mjXError(elem, "damping coefficient cannot be negative"); - actuator->gainprm[2] = -actuator->gainprm[2]; - - // require nonnegative range - if (actuator->ctrlrange[0] < 0 || actuator->ctrlrange[1] < 0) { - throw mjXError(elem, "damper control range cannot be negative"); - } - - // implied parameters - actuator->ctrllimited = 1; - actuator->dyntype = mjDYN_NONE; - actuator->gaintype = mjGAIN_AFFINE; - actuator->biastype = mjBIAS_NONE; + double kv = 0; + ReadAttr(elem, "kv", 1, &kv, text); + err = mjs_setToDamper(actuator, kv); } // cylinder else if (type == "cylinder") { - // explicit attributes - ReadAttr(elem, "timeconst", 1, actuator->dynprm, text); - ReadAttr(elem, "bias", 3, actuator->biasprm, text); - ReadAttr(elem, "area", 1, actuator->gainprm, text); - double diameter; - if (ReadAttr(elem, "diameter", 1, &diameter, text)) { - actuator->gainprm[0] = mjPI / 4 * diameter*diameter; - } - - // implied parameters - actuator->dyntype = mjDYN_FILTER; - actuator->gaintype = mjGAIN_FIXED; - actuator->biastype = mjBIAS_AFFINE; + double timeconst = actuator->dynprm[0]; + double bias = actuator->biasprm[0]; + double area = actuator->gainprm[0]; + double diameter = -1; + ReadAttr(elem, "timeconst", 1, &timeconst, text); + ReadAttr(elem, "bias", 3, &bias, text); + ReadAttr(elem, "area", 1, &area, text); + ReadAttr(elem, "diameter", 1, &diameter, text); + err = mjs_setToCylinder(actuator, timeconst, bias, area, diameter); } // muscle else if (type == "muscle") { - // set muscle defaults if same as global defaults - if (actuator->dynprm[0] == 1)actuator->dynprm[0] = 0.01; // tau act - if (actuator->dynprm[1] == 0)actuator->dynprm[1] = 0.04; // tau deact - if (actuator->gainprm[0] == 1)actuator->gainprm[0] = 0.75; // range[0] - if (actuator->gainprm[1] == 0)actuator->gainprm[1] = 1.05; // range[1] - if (actuator->gainprm[2] == 0)actuator->gainprm[2] = -1; // force - if (actuator->gainprm[3] == 0)actuator->gainprm[3] = 200; // scale - if (actuator->gainprm[4] == 0)actuator->gainprm[4] = 0.5; // lmin - if (actuator->gainprm[5] == 0)actuator->gainprm[5] = 1.6; // lmax - if (actuator->gainprm[6] == 0)actuator->gainprm[6] = 1.5; // vmax - if (actuator->gainprm[7] == 0)actuator->gainprm[7] = 1.3; // fpmax - if (actuator->gainprm[8] == 0)actuator->gainprm[8] = 1.2; // fvmax - - // explicit attributes - ReadAttr(elem, "timeconst", 2, actuator->dynprm, text); - ReadAttr(elem, "tausmooth", 1, actuator->dynprm+2, text); - if (actuator->dynprm[2] < 0) - throw mjXError(elem, "muscle tausmooth cannot be negative"); - ReadAttr(elem, "range", 2, actuator->gainprm, text); - ReadAttr(elem, "force", 1, actuator->gainprm+2, text); - ReadAttr(elem, "scale", 1, actuator->gainprm+3, text); - ReadAttr(elem, "lmin", 1, actuator->gainprm+4, text); - ReadAttr(elem, "lmax", 1, actuator->gainprm+5, text); - ReadAttr(elem, "vmax", 1, actuator->gainprm+6, text); - ReadAttr(elem, "fpmax", 1, actuator->gainprm+7, text); - ReadAttr(elem, "fvmax", 1, actuator->gainprm+8, text); - - // biasprm = gainprm - for (int n=0; n < 9; n++) { - actuator->biasprm[n] = actuator->gainprm[n]; - } - - // implied parameters - actuator->dyntype = mjDYN_MUSCLE; - actuator->gaintype = mjGAIN_MUSCLE; - actuator->biastype = mjBIAS_MUSCLE; + double tausmooth = actuator->dynprm[2]; + double force = -1, scale = -1, lmin = -1, lmax = -1, vmax = -1, fpmax = -1, fvmax = -1; + double range[2] = {-1, -1}, timeconst[2] = {-1, -1}; + ReadAttr(elem, "timeconst", 2, timeconst, text); + ReadAttr(elem, "tausmooth", 1, &tausmooth, text); + ReadAttr(elem, "range", 2, range, text); + ReadAttr(elem, "force", 1, &force, text); + ReadAttr(elem, "scale", 1, &scale, text); + ReadAttr(elem, "lmin", 1, &lmin, text); + ReadAttr(elem, "lmax", 1, &lmax, text); + ReadAttr(elem, "vmax", 1, &vmax, text); + ReadAttr(elem, "fpmax", 1, &fpmax, text); + ReadAttr(elem, "fvmax", 1, &fvmax, text); + err = mjs_setToMuscle(actuator, timeconst, tausmooth, range, force, scale, + lmin, lmax, vmax, fpmax, fvmax); } // adhesion else if (type == "adhesion") { - // explicit attributes - ReadAttr(elem, "gain", 1, actuator->gainprm, text); - if (actuator->gainprm[0] < 0) - throw mjXError(elem, "adhesion gain cannot be negative"); - - // require nonnegative range + double gain = actuator->gainprm[0]; + ReadAttr(elem, "gain", 1, &gain, text); ReadAttr(elem, "ctrlrange", 2, actuator->ctrlrange, text); - if (actuator->ctrlrange[0] < 0 || actuator->ctrlrange[1] < 0) { - throw mjXError(elem, "adhesion control range cannot be negative"); - } - - // implied parameters - actuator->ctrllimited = 1; - actuator->gaintype = mjGAIN_FIXED; - actuator->biastype = mjBIAS_NONE; + err = mjs_setToAdhesion(actuator, gain); } else if (type == "plugin") { @@ -2429,6 +2345,11 @@ void mjXReader::OneActuator(XMLElement* elem, mjsActuator* actuator) { throw mjXError(elem, "unrecognized actuator type: %s", type.c_str()); } + // throw error if any of the above failed + if (!err.empty()) { + throw mjXError(elem, err.c_str()); + } + // read userdata std::vector userdata; if (ReadVector(elem, "user", userdata, text)) { From b8768aa1cd973bca207429258c910c897217d261 Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Mon, 9 Jun 2025 10:04:47 -0700 Subject: [PATCH 09/82] Allow to fusestatic a body if it doesn't generate a referencing error. PiperOrigin-RevId: 769189929 Change-Id: I62f512813ea330da088b28d39373df39be10a602 --- doc/XMLreference.rst | 14 ++-- doc/changelog.rst | 2 + src/user/user_model.cc | 129 ++++++++++++++++++++++++----------- src/user/user_model.h | 8 +++ src/user/user_objects.cc | 66 ++++++++++-------- src/user/user_objects.h | 2 + test/user/user_model_test.cc | 94 +++++++++++++++++++++++++ 7 files changed, 242 insertions(+), 73 deletions(-) diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 71963d33..4e867448 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -792,12 +792,14 @@ has any effect. The settings here are global and apply to the entire model. :at:`fusestatic`: :at-val:`[false, true], "false" for MJCF, "true" for URDF` This attribute controls a compiler optimization feature where static bodies are fused with their parent, and any - elements defined in those bodies are reassigned to the parent. This feature can only be used in models which do not - have elements capable of named references inside the kinematic tree - namely skins, contact pairs, excludes, tendons, - actuators, sensors, tuples, cameras, lights. If a model has any these elements, fusestatic does nothing even if - enabled. This optimization is particularly useful when importing URDF models which often have many dummy bodies, but - can also be used to optimize MJCF models. After optimization, the new model has identical kinematics and dynamics as - the original but is faster to simulate. + elements defined in those bodies are reassigned to the parent. Static bodies are fused with their parent unless + + - They are referenced by another element in the model. + - They contain a site which is referenced by a :ref:`force` or :ref:`torque` sensor. + + This optimization is particularly useful when importing URDF models which often have many dummy bodies, but can also + be used to optimize MJCF models. After optimization, the new model has identical kinematics and dynamics as the + original but is faster to simulate. .. _compiler-inertiafromgeom: diff --git a/doc/changelog.rst b/doc/changelog.rst index 324d41e7..8efd9e54 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -18,6 +18,8 @@ General introduction in 3.3.1 of :ref:`tendon armature`. In addition to the traditional ``mjData.qM``, :ref:`mj_makeM` also computes ``mjData.M``, a CSR representation of the same matrix. - Added a new function :ref:`mj_copyBack` to copy real-valued arrays in an mjModel to a compatible mjSpec. +- Removed the limitation of :ref:`fusestatic` to models which contain no references. The fusestatic + flag will now fuse all bodies which are not referenced and ignore bodies which are referenced. Simulate ^^^^^^^^ diff --git a/src/user/user_model.cc b/src/user/user_model.cc index 8d4e2b5a..8ce68cd7 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -3824,24 +3824,77 @@ void mjCModel::FuseReindex(mjCBody* body) { +template +void mjCModel::ReassignChild(std::vector& dest, std::vector& list, + mjCBody* parent, mjCBody* body) { + for (int j=0; j < list.size(); j++) { + // assign + list[j]->body = parent; + dest.push_back(list[j]); + + // change frame + changeframe(list[j]->pos, list[j]->quat, body->pos, body->quat); + } + list.clear(); +} + + + +template +void mjCModel::ResolveReferences(std::vector& list, mjCBody* body) { + for (auto& item : list) { + item->CopyFromSpec(); + item->ResolveReferences(this); + } +} + + + +template <> +void mjCModel::ResolveReferences(std::vector& list, mjCBody* body) { + for (auto& item : list) { + item->CopyFromSpec(); + item->ResolveReferences(this); + } + for (mjCSensor* sensor : list) { + if (sensor->objtype == mjOBJ_SITE && + (sensor->type == mjSENS_FORCE || sensor->type == mjSENS_TORQUE) && + static_cast(sensor->obj)->body == body) { + throw mjCError(sensor, "cannot fuse a body used by a force/torque sensor"); + } + } +} + + + // fuse static bodies with their parent void mjCModel::FuseStatic(void) { - // skip if model has potential to reference elements with changed ids - if (!skins_.empty() || - !pairs_.empty() || - !excludes_.empty() || - !equalities_.empty() || - !tendons_.empty() || - !actuators_.empty() || - !sensors_.empty() || - !tuples_.empty() || - !cameras_.empty() || - !lights_.empty()) { - return; - } - - // process fusable bodies for (int i=1; i < bodies_.size(); i++) { + // check if the body can be fused + if (!bodies_[i]->name.empty()) { + ids[mjOBJ_BODY].erase(bodies_[i]->name); + + // try to resolve references without the name of this body, if it fails, skip + try { + ResolveReferences(cameras_); + ResolveReferences(lights_); + ResolveReferences(skins_); + ResolveReferences(pairs_); + ResolveReferences(excludes_); + ResolveReferences(equalities_); + ResolveReferences(tendons_); + ResolveReferences(actuators_); + ResolveReferences(sensors_, bodies_[i]); + ResolveReferences(tuples_); + } catch (mjCError err) { + ids[mjOBJ_BODY].insert({bodies_[i]->name, i}); + continue; + } + + // put body back the body name in the map + ids[mjOBJ_BODY].insert({bodies_[i]->name, i}); + } + // get body and parent mjCBody* body = bodies_[i]; mjCBody* par = body->parent; @@ -3891,25 +3944,8 @@ void mjCModel::FuseStatic(void) { //------------- assign geoms and sites to parent, change frames - // geoms - for (int j=0; j < body->geoms.size(); j++) { - // assign - body->geoms[j]->body = par; - par->geoms.push_back(body->geoms[j]); - - // change frame - changeframe(body->geoms[j]->pos, body->geoms[j]->quat, body->pos, body->quat); - } - - // sites - for (int j=0; j < body->sites.size(); j++) { - // assign - body->sites[j]->body = par; - par->sites.push_back(body->sites[j]); - - // change frame - changeframe(body->sites[j]->pos, body->sites[j]->quat, body->pos, body->quat); - } + ReassignChild(par->geoms, body->geoms, par, body); + ReassignChild(par->sites, body->sites, par, body); //------------- remove from global body list, reduce global counts @@ -3960,15 +3996,21 @@ void mjCModel::FuseStatic(void) { //------------- delete body (without deleting children) + // remove body name from map + if (!body->name.empty()) { + ids[mjOBJ_BODY].erase(body->name); + } + // delete allocation body->bodies.clear(); - body->geoms.clear(); - body->sites.clear(); delete body; // check index i again (we have a new body at this index) i--; } + + // remove empty names + processlist(ids, bodies_, mjOBJ_BODY, true); } @@ -4392,6 +4434,17 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) { bodies_[i]->Compile(); // also compiles joints, geoms, sites, cameras, lights, frames } + // fuse static if enabled + if (compiler.fusestatic) { + FuseStatic(); + for (int i=0; i < lights_.size(); i++) { + lights_[i]->Compile(); + } + for (int i=0; i < cameras_.size(); i++) { + cameras_[i]->Compile(); + } + } + // compile all other objects except for keyframes for (auto flex : flexes_) flex->Compile(vfs); for (auto skin : skins_) skin->Compile(vfs); @@ -4423,10 +4476,6 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) { // resolve asset references, compute sizes IndexAssets(compiler.discardvisual); SetSizes(); - // fuse static if enabled - if (compiler.fusestatic) { - FuseStatic(); - } // set nmocap and body.mocapid for (mjCBody* body : bodies_) { diff --git a/src/user/user_model.h b/src/user/user_model.h index cfd8082e..4bb1927e 100644 --- a/src/user/user_model.h +++ b/src/user/user_model.h @@ -444,6 +444,14 @@ class mjCModel : public mjCModel_, private mjSpec { // generate a signature for the model uint64_t Signature(); + // reassign children of a body to a new parent + template + void ReassignChild(std::vector& dest, std::vector& list, mjCBody* parent, mjCBody* body); + + // resolve references in a list of objects + template + void ResolveReferences(std::vector& list, mjCBody* body = nullptr); + mjListKeyMap ids; // map from object names to ids mjCError errInfo; // last error info std::vector key_pending_; // attached keyframes diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 6a1b2451..d925a3a3 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -3562,6 +3562,19 @@ void mjCCamera::CopyFromSpec() { +void mjCCamera::ResolveReferences(const mjCModel* m) { + if (!targetbody_.empty()) { + mjCBody* tb = (mjCBody*)m->FindObject(mjOBJ_BODY, targetbody_); + if (tb) { + targetbodyid = tb->id; + } else { + throw mjCError(this, "unknown target body in camera"); + } + } +} + + + // compiler void mjCCamera::Compile(void) { CopyFromSpec(); @@ -3587,14 +3600,7 @@ void mjCCamera::Compile(void) { mjuu_normvec(quat, 4); // get targetbodyid - if (!targetbody_.empty()) { - mjCBody* tb = (mjCBody*)model->FindObject(mjOBJ_BODY, targetbody_); - if (tb) { - targetbodyid = tb->id; - } else { - throw mjCError(this, "unknown target body in camera"); - } - } + ResolveReferences(model); // make sure the image size is finite if (fovy >= 180) { @@ -3716,6 +3722,27 @@ void mjCLight::CopyFromSpec() { +void mjCLight::ResolveReferences(const mjCModel* m) { + if (!targetbody_.empty()) { + mjCBody* tb = (mjCBody*)m->FindObject(mjOBJ_BODY, targetbody_); + if (tb) { + targetbodyid = tb->id; + } else { + throw mjCError(this, "unknown target body in light"); + } + } + if (!texture_.empty()) { + mjCTexture* tex = (mjCTexture*)m->FindObject(mjOBJ_TEXTURE, texture_); + if (tex) { + texid = tex->id; + } else { + throw mjCError(this, "unknown texture in light"); + } + } +} + + + // compiler void mjCLight::Compile(void) { CopyFromSpec(); @@ -3735,25 +3762,8 @@ void mjCLight::Compile(void) { throw mjCError(this, "zero direction in light"); } - // get targetbodyid - if (!targetbody_.empty()) { - mjCBody* tb = (mjCBody*)model->FindObject(mjOBJ_BODY, targetbody_); - if (tb) { - targetbodyid = tb->id; - } else { - throw mjCError(this, "unknown target body in light"); - } - } - - // get texture - if (!texture_.empty()) { - mjCTexture* tex = (mjCTexture*)model->FindObject(mjOBJ_TEXTURE, texture_); - if (tex) { - texid = tex->id; - } else { - throw mjCError(this, "unknown target body in light"); - } - } + // get targetbodyid and texid + ResolveReferences(model); } @@ -6493,6 +6503,8 @@ void mjCSensor::CopyPlugin() { void mjCSensor::ResolveReferences(const mjCModel* m) { + obj = nullptr; + ref = nullptr; objname_ = prefix + objname_ + suffix; refname_ = prefix + refname_ + suffix; diff --git a/src/user/user_objects.h b/src/user/user_objects.h index 18158c5c..fc8cf459 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -786,6 +786,7 @@ class mjCCamera : public mjCCamera_, private mjsCamera { void CopyFromSpec(void); void PointToLocal(void); void NameSpace(const mjCModel* m); + void ResolveReferences(const mjCModel* m); }; @@ -831,6 +832,7 @@ class mjCLight : public mjCLight_, private mjsLight { void CopyFromSpec(void); void PointToLocal(void); void NameSpace(const mjCModel* m); + void ResolveReferences(const mjCModel* m); }; diff --git a/test/user/user_model_test.cc b/test/user/user_model_test.cc index 8b00753d..3293b257 100644 --- a/test/user/user_model_test.cc +++ b/test/user/user_model_test.cc @@ -440,6 +440,100 @@ TEST_F(FuseStaticTest, FuseStaticEquivalent) { mj_deleteModel(m_no_fuse); } +TEST_F(FuseStaticTest, FuseStaticActuatorReferencedBody) { + static constexpr char xml_template[] = R"( + + + + + + + + + + + + + + + + + + + + + + + )"; + std::array error; + mjModel* m = LoadModelFromString(xml_template, error.data(), error.size()); + ASSERT_THAT(m, NotNull()) << error.data(); + EXPECT_EQ(m->nbody, 3) << "Expecting a world body and two others"; + mj_deleteModel(m); +} + +TEST_F(FuseStaticTest, FuseStaticLightReferencedBody) { + static constexpr char xml_template[] = R"( + + + + + + + + + + + + + + + + + + + + )"; + std::array error; + mjModel* m = LoadModelFromString(xml_template, error.data(), error.size()); + ASSERT_THAT(m, NotNull()) << error.data(); + EXPECT_EQ(m->nbody, 3) << "Expecting a world body and two others"; + mj_deleteModel(m); +} + +TEST_F(FuseStaticTest, FuseStaticForceSensorReferencedBody) { + static constexpr char xml_template[] = R"( + + + + + + + + + + + + + + + + + + + + + + + + )"; + std::array error; + mjModel* m = LoadModelFromString(xml_template, error.data(), error.size()); + ASSERT_THAT(m, NotNull()) << error.data(); + EXPECT_EQ(m->nbody, 3) << "Expecting a world body and two others"; + mj_deleteModel(m); +} + // ------------- test discardvisual -------------------------------------------- using DiscardVisualTest = MujocoTest; From caaf7b3a69d674c98572c0244dce1081abe49ca1 Mon Sep 17 00:00:00 2001 From: Erik Frey Date: Mon, 9 Jun 2025 14:26:18 -0700 Subject: [PATCH 10/82] Add tendon armature to MJX. PiperOrigin-RevId: 769294284 Change-Id: Idfbd4f355eb26fba5035973ab989a9d3af05c85f --- doc/changelog.rst | 4 + mjx/mujoco/mjx/__init__.py | 2 + mjx/mujoco/mjx/_src/forward.py | 2 + mjx/mujoco/mjx/_src/smooth.py | 152 +++++++++++++++++++++++++++++ mjx/mujoco/mjx/_src/smooth_test.py | 53 ++++++++++ mjx/mujoco/mjx/_src/support.py | 37 +++++++ 6 files changed, 250 insertions(+) diff --git a/doc/changelog.rst b/doc/changelog.rst index 8efd9e54..f82052b1 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -36,6 +36,10 @@ Python bindings - Added examples of procedural terrain generation to the Model Editing tutorial: |mjspec_colab| +MJX +^^^ +- Added tendon armature. + Version 3.3.2 (April 28, 2025) ------------------------------ diff --git a/mjx/mujoco/mjx/__init__.py b/mjx/mujoco/mjx/__init__.py index c60c785c..d71d6a24 100644 --- a/mjx/mujoco/mjx/__init__.py +++ b/mjx/mujoco/mjx/__init__.py @@ -48,6 +48,8 @@ from mujoco.mjx._src.smooth import rne from mujoco.mjx._src.smooth import rne_postconstraint from mujoco.mjx._src.smooth import subtree_vel from mujoco.mjx._src.smooth import tendon +from mujoco.mjx._src.smooth import tendon_armature +from mujoco.mjx._src.smooth import tendon_bias from mujoco.mjx._src.smooth import transmission from mujoco.mjx._src.solver import solve from mujoco.mjx._src.support import apply_ft diff --git a/mjx/mujoco/mjx/_src/forward.py b/mjx/mujoco/mjx/_src/forward.py index 62535122..d8faf293 100644 --- a/mjx/mujoco/mjx/_src/forward.py +++ b/mjx/mujoco/mjx/_src/forward.py @@ -73,6 +73,7 @@ def fwd_position(m: Model, d: Data) -> Data: d = smooth.camlight(m, d) d = smooth.tendon(m, d) d = smooth.crb(m, d) + d = smooth.tendon_armature(m, d) d = smooth.factor_m(m, d) d = collision_driver.collision(m, d) d = constraint.make_constraint(m, d) @@ -93,6 +94,7 @@ def fwd_velocity(m: Model, d: Data) -> Data: d = smooth.com_vel(m, d) d = passive.passive(m, d) d = smooth.rne(m, d) + d = smooth.tendon_bias(m, d) return d diff --git a/mjx/mujoco/mjx/_src/smooth.py b/mjx/mujoco/mjx/_src/smooth.py index f87c2ed3..fba3d3ce 100644 --- a/mjx/mujoco/mjx/_src/smooth.py +++ b/mjx/mujoco/mjx/_src/smooth.py @@ -1184,3 +1184,155 @@ def transmission(m: Model, d: Data) -> Data: {'_impl.actuator_length': length, '_impl.actuator_moment': moment} ) return d + + +def tendon_armature(m: Model, d: Data) -> Data: + """Add tendon armature to qM.""" + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('tendon_armature requires JAX backend implementation.') + + if not support.is_sparse(m): + return d.tree_replace({ + '_impl.qM': ( + d._impl.qM + + d._impl.ten_J.T + @ jax.vmap(jp.multiply)(d._impl.ten_J, m.tendon_armature) + ) + }) + else: + # TODO(taylorhowell): implement tendon armature with sparse qM + raise NotImplementedError( + 'Tendon armature with sparse qM is not implemented.' + ) + + +def tendon_dot(m: Model, d: Data) -> jax.Array: + """Compute time derivative of dense tendon Jacobian for one tendon.""" + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('tendon_dot requires JAX backend implementation.') + + ten_Jdot = jp.zeros((m.ntendon, m.nv)) # pylint: disable=invalid-name + + if not m.ntendon: + return ten_Jdot + + # process pulleys + (wrap_id_pulley,) = np.nonzero(m.wrap_type == WrapType.PULLEY) + + divisor = np.ones(m.nwrap) + for adr, num in zip(m.tendon_adr, m.tendon_num): + for id_pulley in wrap_id_pulley: + if adr <= id_pulley < adr + num: + divisor[id_pulley : adr + num] = np.maximum( + mujoco.mjMINVAL, m.wrap_prm[id_pulley] + ) + + # process spatial tendon sites + (wrap_id_site,) = np.nonzero(m.wrap_type == WrapType.SITE) + + # find consecutive sites, skipping tendon transitions + (pair_id,) = np.nonzero(np.diff(wrap_id_site) == 1) + wrap_id_site_pair = np.setdiff1d(wrap_id_site[pair_id], m.tendon_adr[1:] - 1) + wrap_objid_site0 = m.wrap_objid[wrap_id_site_pair] + wrap_objid_site1 = m.wrap_objid[wrap_id_site_pair + 1] + site_bodyid0 = m.site_bodyid[wrap_objid_site0] + site_bodyid1 = m.site_bodyid[wrap_objid_site1] + site_xpos0 = d.site_xpos[wrap_objid_site0] + site_xpos1 = d.site_xpos[wrap_objid_site1] + subtree_com0 = d.subtree_com[m.body_rootid[site_bodyid0]] + subtree_com1 = d.subtree_com[m.body_rootid[site_bodyid1]] + site_vel0 = jax.vmap(lambda a, b: a[3:] - jp.cross(b, a[:3]))( + d.cvel[site_bodyid0], site_xpos0 - subtree_com0 + ) + site_vel1 = jax.vmap(lambda a, b: a[3:] - jp.cross(b, a[:3]))( + d.cvel[site_bodyid1], site_xpos1 - subtree_com1 + ) + + @jax.vmap + def _momentdot(wpnt0, wpnt1, wvel0, wvel1, body0, body1): + # dpnt = 3D position difference, normalize + dpnt = wpnt1 - wpnt0 + norm = math.norm(dpnt) + dpnt = jp.where( + norm < mujoco.mjMINVAL, jp.array([1.0, 0.0, 0.0]), dpnt / norm + ) + + # dvel = d / dt(dpnt) + dvel = wvel1 - wvel0 + dot = jp.dot(dpnt, dvel) + dvel += dpnt * -dot + dvel = jp.where(norm > mujoco.mjMINVAL, dvel / norm, 0.0) + + # get endpoint JacobianDots, subtract + jacp1, _ = support.jac_dot(m, d, wpnt0, body0) + jacp2, _ = support.jac_dot(m, d, wpnt1, body1) + jacdif = jacp2 - jacp1 + + # chain rule, first term: Jdot += d / dt(jac2 - jac1) * dpnt + tmp0 = jacdif @ dpnt + + # get endpoint Jacobians, subtract + jacp1, _ = support.jac(m, d, wpnt0, body0) + jacp2, _ = support.jac(m, d, wpnt1, body1) + jacdif = jacp2 - jacp1 + + # chain rule, second term: Jdot += (jac2 - jac1) * d/dt (dpnt) + tmp1 = jacdif @ dvel + + return jp.where(body0 != body1, tmp0 + tmp1, jp.zeros(m.nv)) + + momentdots = _momentdot( + site_xpos0, + site_xpos1, + site_vel0, + site_vel1, + site_bodyid0, + site_bodyid1, + ) + + if wrap_id_site_pair.size: + divisor_site_pair = divisor[wrap_id_site_pair] + momentdots /= divisor_site_pair[:, None] + + tendon_nsite = np.array([ + sum((wrap_id_site_pair >= adr) & (wrap_id_site_pair < adr + num)) + for adr, num in zip(m.tendon_adr, m.tendon_num) + ]) + tendon_has_site = tendon_nsite > 0 + (tendon_id_site,) = np.nonzero(tendon_has_site) + tendon_nsite = tendon_nsite[tendon_has_site] + tendon_with_site = tendon_nsite.size + ten_site_id = np.repeat(np.arange(tendon_with_site), tendon_nsite) + + momentdot = jax.ops.segment_sum(momentdots, ten_site_id, tendon_with_site) + ten_Jdot = ten_Jdot.at[tendon_id_site].set(momentdot) # pylint: disable=invalid-name + + # TODO(taylorhowell): time derivatives for geoms + + return ten_Jdot + + +def tendon_bias(m: Model, d: Data) -> Data: + """Add bias force due to tendon armature.""" + if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): + raise ValueError('tendon_bias requires JAX backend implementation.') + + if not m.ntendon: + return d + + # get dense d/dt(tendon Jacobian) for each tendon + ten_Jdot = tendon_dot(m, d) # pylint: disable=invalid-name + + # add bias term: qfrc += ten_J * armature * ten_Jdot @ qvel + coef = m.tendon_armature * jp.dot(ten_Jdot, d.qvel) + + if not support.is_sparse(m): + return d.tree_replace({ + 'qfrc_bias': ( + d.qfrc_bias + + jp.sum(jax.vmap(jp.multiply)(d._impl.ten_J, coef), axis=0) + ) + }) + else: + # TODO(taylorhowell): implement tendon bias with sparse qM + raise NotImplementedError('Tendon bias with sparse qM is not implemented.') diff --git a/mjx/mujoco/mjx/_src/smooth_test.py b/mjx/mujoco/mjx/_src/smooth_test.py index 96470a21..6c038522 100644 --- a/mjx/mujoco/mjx/_src/smooth_test.py +++ b/mjx/mujoco/mjx/_src/smooth_test.py @@ -17,6 +17,7 @@ from absl.testing import absltest from absl.testing import parameterized import jax +from jax import numpy as jp import mujoco from mujoco import mjx from mujoco.mjx._src import test_util @@ -316,6 +317,58 @@ class TendonTest(parameterized.TestCase): _assert_eq(d.wrap_obj, dx._impl.wrap_obj, 'wrap_obj') _assert_eq(d.wrap_xpos, dx._impl.wrap_xpos, 'wrap_xpos') + def test_tendon_armature(self): + """Tests MJX tendon armature matches MuJoCo.""" + m = mujoco.MjModel.from_xml_string(""" + + + + + + + + + + + + + + + + + + + + + + + + + """) + + d = mujoco.MjData(m) + mujoco.mj_resetDataKeyframe(m, d, 0) + mujoco.mj_forward(m, d) + + qM = np.zeros((m.nv, m.nv)) # pylint: disable=invalid-name + mujoco.mj_fullM(m, qM, d.qM) + + mx = mjx.put_model(m) + dx = mjx.put_data(m, d) + + dx = dx.tree_replace( + {'_impl.qM': jp.zeros((m.nv, m.nv)), 'qfrc_bias': jp.zeros(m.nv)} + ) + + dx = mjx.crb(mx, dx) + dx = mjx.tendon_armature(mx, dx) + + _assert_eq(dx._impl.qM, qM, 'qM') + + dx = mjx.rne(mx, dx) + dx = mjx.tendon_bias(mx, dx) + _assert_eq(dx.qfrc_bias, d.qfrc_bias, 'qfrc_bias') + if __name__ == '__main__': absltest.main() diff --git a/mjx/mujoco/mjx/_src/support.py b/mjx/mujoco/mjx/_src/support.py index 15715021..ba62d9e3 100644 --- a/mjx/mujoco/mjx/_src/support.py +++ b/mjx/mujoco/mjx/_src/support.py @@ -142,6 +142,7 @@ def jac( m: Model, d: Data, point: jax.Array, body_id: jax.Array ) -> Tuple[jax.Array, jax.Array]: """Compute pair of (NV, 3) Jacobians of global point attached to body.""" + # TODO(taylorhowell): statically construct mask fn = lambda carry, b: b if carry is None else b + carry mask = (jp.arange(m.nbody) == body_id) * 1 mask = scan.body_tree(m, fn, 'b', 'b', mask, reverse=True) @@ -155,6 +156,42 @@ def jac( return jacp, jacr +def jac_dot( + m: Model, d: Data, point: jax.Array, body_id: jax.Array +) -> Tuple[jax.Array, jax.Array]: + """Compute pair of (NV, 3) Jacobian time derivatives of global point attached to body.""" + # TODO(taylorhowell): statically construct mask + fn = lambda carry, b: b if carry is None else b + carry + mask = (jp.arange(m.nbody) == body_id) * 1 + mask = scan.body_tree(m, fn, 'b', 'b', mask, reverse=True) + mask = mask[jp.array(m.dof_bodyid)] > 0 + + offset = point - d.subtree_com[jp.array(m.body_rootid)[body_id]] + pvel_lin = d.cvel[body_id][3:] - jp.cross(offset, d.cvel[body_id][:3]) + + cdof = d._impl.cdof + cdof_dot = d._impl.cdof_dot + + # check for quaternion + jnt_type = m.jnt_type[m.dof_jntid] + dof_adr = m.jnt_dofadr[m.dof_jntid] + is_quat = (jnt_type == JointType.BALL) | ( + jnt_type == JointType.FREE & (np.arange(m.nv) >= dof_adr + 3) + ) + + # compute cdof_dot for quaternion (use current body cvel) + cdof_dot_quat = jax.vmap(math.motion_cross)(d.cvel[m.dof_bodyid], cdof) + cdof_dot = jp.where(is_quat[:, None], cdof_dot_quat, cdof_dot) + + jacp = jax.vmap( + lambda a, b: a[3:] + jp.cross(a[:3], offset) + jp.cross(b[:3], pvel_lin) + )(cdof_dot, cdof) + jacp = jax.vmap(jp.multiply)(jacp, mask) + jacr = jax.vmap(jp.multiply)(cdof_dot[:, :3], mask) # pytype: disable=attribute-error + + return jacp, jacr + + def apply_ft( m: Model, d: Data, From bf56312c9d9f4b0d40bc81f6f55de20c7645cf74 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 10 Jun 2025 03:33:27 -0700 Subject: [PATCH 11/82] Increase size of image in changelog PiperOrigin-RevId: 769549620 Change-Id: I8666771ecf50b91342694deeb357b3edfa3f62d9 --- doc/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index f82052b1..589fb67b 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -28,7 +28,7 @@ Simulate and :ref:`mjv_copyData`, which don't copy arrays which are not required for visualization. .. image:: images/changelog/procedural_terrain_generation.png - :width: 25% + :width: 33% :align: right Python bindings From 5f42078dc1491b08917606ad17f8a37038c1945d Mon Sep 17 00:00:00 2001 From: Alessio Quaglino Date: Tue, 10 Jun 2025 10:14:42 -0700 Subject: [PATCH 12/82] Do not write nameless frames whose class is "main". PiperOrigin-RevId: 769685151 Change-Id: I3d993d7521db2b2adbf611c3e4eb5b4b293bdc30 --- src/xml/xml_native_writer.cc | 3 ++- test/xml/xml_native_writer_test.cc | 8 +++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index de2aef96..1b82b9fb 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -1595,7 +1595,8 @@ XMLElement* mjXWriter::OneFrame(XMLElement* elem, mjCFrame* frame) { return elem; } - if (frame->name.empty() && frame->classname.empty()) { + // TODO: empty classname should not occur (but does) + if (frame->name.empty() && (frame->classname.empty() || frame->classname == "main")) { return elem; } diff --git a/test/xml/xml_native_writer_test.cc b/test/xml/xml_native_writer_test.cc index 2ad13bc7..6876245a 100644 --- a/test/xml/xml_native_writer_test.cc +++ b/test/xml/xml_native_writer_test.cc @@ -762,7 +762,7 @@ TEST_F(XMLWriterTest, WritesFrameDefaults) { - + @@ -791,16 +791,14 @@ TEST_F(XMLWriterTest, WritesFrameDefaults) { - + - - - + From e5631c0bab16b6995722054b1700b74b7c055fec Mon Sep 17 00:00:00 2001 From: Erik Frey Date: Tue, 10 Jun 2025 12:45:49 -0700 Subject: [PATCH 13/82] [MJX] Don't raise sparse qM error in tendon_armature if there are no tendons. PiperOrigin-RevId: 769753590 Change-Id: I5dd7a953f08e0566c5e22c2c926d7bd9c2c57bd6 --- mjx/mujoco/mjx/_src/smooth.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mjx/mujoco/mjx/_src/smooth.py b/mjx/mujoco/mjx/_src/smooth.py index fba3d3ce..bef176d6 100644 --- a/mjx/mujoco/mjx/_src/smooth.py +++ b/mjx/mujoco/mjx/_src/smooth.py @@ -1191,6 +1191,9 @@ def tendon_armature(m: Model, d: Data) -> Data: if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): raise ValueError('tendon_armature requires JAX backend implementation.') + if not m.ntendon: + return d + if not support.is_sparse(m): return d.tree_replace({ '_impl.qM': ( From f978b4eea368450267dd5e8825347653bec7355e Mon Sep 17 00:00:00 2001 From: Kevin Sayed Date: Tue, 10 Jun 2025 14:48:28 -0700 Subject: [PATCH 14/82] Release notes for 3.3.3 + update git hashes. - Upgraded dependencies versions. - Added release date for 3.3.3 - Reordered changelog to be a numbered list. PiperOrigin-RevId: 769808336 Change-Id: Iffa074f52ef2e3f9d450ce6973a7e85980417367 --- cmake/MujocoDependencies.cmake | 10 ++++----- doc/changelog.rst | 40 +++++++++++++++++----------------- python/mujoco/CMakeLists.txt | 4 ++-- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/cmake/MujocoDependencies.cmake b/cmake/MujocoDependencies.cmake index 9d5dc01c..78522705 100644 --- a/cmake/MujocoDependencies.cmake +++ b/cmake/MujocoDependencies.cmake @@ -15,7 +15,7 @@ # Build configuration for third party libraries used in MuJoCo. set(MUJOCO_DEP_VERSION_lodepng - b4ed2cd7ecf61d29076169b49199371456d4f90b + 17d08dd26cac4d63f43af217ebd70318bfb8189c CACHE STRING "Version of `lodepng` to be fetched." ) set(MUJOCO_DEP_VERSION_tinyxml2 @@ -35,21 +35,21 @@ set(MUJOCO_DEP_VERSION_ccd CACHE STRING "Version of `ccd` to be fetched." ) set(MUJOCO_DEP_VERSION_qhull - 0c8fc90d2037588024d9964515c1e684f6007ecc + c7bee59d068a69f427b1273e71cdc5bc455a5bdd CACHE STRING "Version of `qhull` to be fetched." ) set(MUJOCO_DEP_VERSION_Eigen3 - 464c1d097891a1462ab28bf8bb763c1683883892 + d0b490ee091629068e0c11953419eb089f9e6bb2 CACHE STRING "Version of `Eigen3` to be fetched." ) set(MUJOCO_DEP_VERSION_abseil - d9e4955c65cd4367dd6bf46f4ccb8cd3d100540b # LTS 20250127.1 + bc257a88f7c1939f24e0379f14a3589e926c950c # LTS 20250512.0 CACHE STRING "Version of `abseil` to be fetched." ) set(MUJOCO_DEP_VERSION_gtest - 6910c9d9165801d8827d628cb72eb7ea9dd538c5 # v1.16.0 + 52eb8108c5bdec04579160ae17225d66034bd723 # v1.17.0 CACHE STRING "Version of `gtest` to be fetched." ) diff --git a/doc/changelog.rst b/doc/changelog.rst index 589fb67b..6469e9f4 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -2,30 +2,30 @@ Changelog ========= -Upcoming version (not yet release) ----------------------------------- +Version 3.3.3 (June 10, 2025) +----------------------------- General ^^^^^^^ -- Refactored island implementation so that island data is memory-contiguous. This speeds up island processing in the - solver and clears the way for the addition of the Newton and PGS solvers (currently only CG is supported). -- Removed the :at:`shell` plugin. This is now supported by :ref:`flexcomp` and is active depending on - the :ref:`elastic2d` attribute (off by default). -- Replaced the :ref:`directional` (boolean) field for lights with a - :ref:`type` field (of type :ref:`mjtLightType`) to allow for additional lighting - types. -- Added new sub-component :ref:`mj_makeM` which combines the :ref:`mj_crb` call with additional logic to support the - introduction in 3.3.1 of :ref:`tendon armature`. In addition to the traditional - ``mjData.qM``, :ref:`mj_makeM` also computes ``mjData.M``, a CSR representation of the same matrix. -- Added a new function :ref:`mj_copyBack` to copy real-valued arrays in an mjModel to a compatible mjSpec. -- Removed the limitation of :ref:`fusestatic` to models which contain no references. The fusestatic - flag will now fuse all bodies which are not referenced and ignore bodies which are referenced. +1. Refactored island implementation so that island data is memory-contiguous. This speeds up island processing in the + solver and clears the way for the addition of the Newton and PGS solvers (currently only CG is supported). +2. Removed the :at:`shell` plugin. This is now supported by :ref:`flexcomp` and is active depending on + the :ref:`elastic2d` attribute (off by default). +3. Replaced the :ref:`directional` (boolean) field for lights with a + :ref:`type` field (of type :ref:`mjtLightType`) to allow for additional lighting + types. +4. Added new sub-component :ref:`mj_makeM` which combines the :ref:`mj_crb` call with additional logic to support the + introduction in 3.3.1 of :ref:`tendon armature`. In addition to the traditional + ``mjData.qM``, :ref:`mj_makeM` also computes ``mjData.M``, a CSR representation of the same matrix. +5. Added a new function :ref:`mj_copyBack` to copy real-valued arrays in an mjModel to a compatible mjSpec. +6. Removed the limitation of :ref:`fusestatic` to models which contain no references. The fusestatic + flag will now fuse all bodies which are not referenced and ignore bodies which are referenced. Simulate ^^^^^^^^ -- The struct ``mjv_sceneState`` has been removed. This struct was used for partial synchronization of ``mjModel`` and - ``mjData`` when the Python viewer is used in passive mode. This functionality is now provided by :ref:`mjv_copyModel` - and :ref:`mjv_copyData`, which don't copy arrays which are not required for visualization. +7. The struct ``mjv_sceneState`` has been removed. This struct was used for partial synchronization of ``mjModel`` and + ``mjData`` when the Python viewer is used in passive mode. This functionality is now provided by :ref:`mjv_copyModel` + and :ref:`mjv_copyData`, which don't copy arrays which are not required for visualization. .. image:: images/changelog/procedural_terrain_generation.png :width: 33% @@ -34,11 +34,11 @@ Simulate Python bindings ^^^^^^^^^^^^^^^ -- Added examples of procedural terrain generation to the Model Editing tutorial: |mjspec_colab| +8. Added examples of procedural terrain generation to the Model Editing tutorial: |mjspec_colab| MJX ^^^ -- Added tendon armature. +9. Added tendon armature. Version 3.3.2 (April 28, 2025) ------------------------------ diff --git a/python/mujoco/CMakeLists.txt b/python/mujoco/CMakeLists.txt index b1aec9ed..e103f9c0 100644 --- a/python/mujoco/CMakeLists.txt +++ b/python/mujoco/CMakeLists.txt @@ -140,7 +140,7 @@ findorfetch( GIT_REPO https://github.com/abseil/abseil-cpp GIT_TAG - d9e4955c65cd4367dd6bf46f4ccb8cd3d100540b # LTS 20250127.1 + bc257a88f7c1939f24e0379f14a3589e926c950c # LTS 20250512.0 TARGETS ${MUJOCO_PYTHON_ABSL_TARGETS} EXCLUDE_FROM_ALL @@ -173,7 +173,7 @@ findorfetch( GIT_REPO https://gitlab.com/libeigen/eigen GIT_TAG - 464c1d097891a1462ab28bf8bb763c1683883892 + d0b490ee091629068e0c11953419eb089f9e6bb2 TARGETS Eigen3::Eigen EXCLUDE_FROM_ALL From 65deedbc755fecd8ae73ba2070fa9b99337405f9 Mon Sep 17 00:00:00 2001 From: Baruch Tabanpour Date: Wed, 11 Jun 2025 18:04:51 -0700 Subject: [PATCH 15/82] Rename backend_impl to impl. Remove _full_compat from callsites. PiperOrigin-RevId: 770400560 Change-Id: I7bec79aeae07322a30b15cc04d4dd05077741a8b --- mjx/mujoco/mjx/_src/io.py | 186 +++++++++++++-------------- mjx/mujoco/mjx/_src/io_test.py | 228 ++++++++++++++++----------------- mjx/mujoco/mjx/_src/sensor.py | 2 +- mjx/mujoco/mjx/_src/types.py | 18 +-- 4 files changed, 217 insertions(+), 217 deletions(-) diff --git a/mjx/mujoco/mjx/_src/io.py b/mjx/mujoco/mjx/_src/io.py index f8aaf565..e851d1e2 100644 --- a/mjx/mujoco/mjx/_src/io.py +++ b/mjx/mujoco/mjx/_src/io.py @@ -41,51 +41,51 @@ def _is_cuda_gpu_device(device: jax.Device) -> bool: return device in cuda_devices -def _resolve_backend_impl( +def _resolve_impl( device: jax.Device, -) -> types.BackendImpl: - """Pick a default backend impl based on the device specified.""" +) -> types.Impl: + """Pick a default implementation based on the device specified.""" if _is_cuda_gpu_device(device): # TODO(btaba): Remove flag once Warp is ready to launch. mjx_warp_enabled = os.environ.get('MJX_WARP_ENABLED', 'f').lower() == 'true' if mjx_warp_enabled: - logging.debug('Picking default backend implementation: Warp.') - return types.BackendImpl.WARP + logging.debug('Picking default implementation: Warp.') + return types.Impl.WARP logging.info('MJX Warp is disabled via MJX_WARP_ENABLED=false.') if device.platform in ('gpu', 'tpu'): - logging.debug('Picking default backend implementation: JAX.') - return types.BackendImpl.JAX + logging.debug('Picking default implementation: JAX.') + return types.Impl.JAX if device.platform == 'cpu': mjx_c_default = ( os.environ.get('MJX_C_DEFAULT_ENABLED', 'f').lower() == 'true' ) if mjx_c_default: - logging.debug('Picking default backend implementation: C.') - return types.BackendImpl.C - return types.BackendImpl.JAX + logging.debug('Picking default implementation: C.') + return types.Impl.C + return types.Impl.JAX raise ValueError(f'Unsupported device: {device}') def _resolve_device( - backend_impl: types.BackendImpl, + impl: types.Impl, ) -> jax.Device: - """Resolves a device based on the backend implementation.""" - backend_impl = types.BackendImpl(backend_impl) - if backend_impl == types.BackendImpl.JAX: + """Resolves a device based on the implementation.""" + impl = types.Impl(impl) + if impl == types.Impl.JAX: device_0 = jax.devices()[0] logging.debug('Picking default device: %s.', device_0) return device_0 - if backend_impl == types.BackendImpl.C: + if impl == types.Impl.C: cpu_0 = jax.devices('cpu')[0] logging.debug('Picking default device: %s', cpu_0) return cpu_0 - if backend_impl == types.BackendImpl.WARP: - # WARP backend requires a CUDA GPU. + if impl == types.Impl.WARP: + # WARP implementation requires a CUDA GPU. cuda_gpus = [d for d in jax.devices('cuda')] if not cuda_gpus: raise AssertionError( @@ -96,64 +96,64 @@ def _resolve_device( logging.debug('Picking default device: %s', cuda_gpus[0]) return cuda_gpus[0] - raise ValueError(f'Unsupported backend implementation: {backend_impl}') + raise ValueError(f'Unsupported implementation: {impl}') -def _check_backend_impl_device_compatibility( - backend_impl: Union[str, types.BackendImpl], +def _check_impl_device_compatibility( + impl: Union[str, types.Impl], device: jax.Device, ) -> None: - """Checks that the backend implementation is compatible with the device.""" - if backend_impl is None: - raise ValueError('No backend implementation specified.') + """Checks that the implementation is compatible with the device.""" + if impl is None: + raise ValueError('No implementation specified.') - backend_impl = types.BackendImpl(backend_impl) + impl = types.Impl(impl) - if backend_impl == types.BackendImpl.WARP: + if impl == types.Impl.WARP: if not _is_cuda_gpu_device(device): raise AssertionError( - 'Warp backend implementation requires a CUDA GPU device, got ' + 'Warp implementation requires a CUDA GPU device, got ' f'{device}.' ) mjx_warp_enabled = os.environ.get('MJX_WARP_ENABLED', 'f').lower() == 'true' if not mjx_warp_enabled: raise AssertionError( - 'Warp backend implementation is disabled via MJX_WARP_ENABLED=false.' + 'Warp implementation is disabled via MJX_WARP_ENABLED=false.' ) is_cpu_device = device.platform == 'cpu' - if backend_impl == types.BackendImpl.C: + if impl == types.Impl.C: if not is_cpu_device: raise AssertionError( - f'C backend implementation requires a CPU device, got {device}.' + f'C implementation requires a CPU device, got {device}.' ) - # NB: JAX backend works with any device. + # NB: JAX implementation works with any device. -def _resolve_backend_impl_and_device( - backend_impl: Optional[Union[str, types.BackendImpl]], +def _resolve_impl_and_device( + impl: Optional[Union[str, types.Impl]], device: Optional[jax.Device] = None, -) -> Tuple[types.BackendImpl, jax.Device]: - """Resolves a backend implementation and device.""" - if backend_impl: - backend_impl = types.BackendImpl(backend_impl) +) -> Tuple[types.Impl, jax.Device]: + """Resolves a implementation and device.""" + if impl: + impl = types.Impl(impl) - has_backend_impl, has_device = backend_impl is not None, device is not None - if (has_backend_impl, has_device) == (True, True): + has_impl, has_device = impl is not None, device is not None + if (has_impl, has_device) == (True, True): pass - elif (has_backend_impl, has_device) == (True, False): - device = _resolve_device(backend_impl) - elif (has_backend_impl, has_device) == (False, True): - backend_impl = _resolve_backend_impl(device) + elif (has_impl, has_device) == (True, False): + device = _resolve_device(impl) + elif (has_impl, has_device) == (False, True): + impl = _resolve_impl(device) else: device = jax.devices(jax.default_backend())[0] logging.info('Using JAX default device: %s.', device) - backend_impl = _resolve_backend_impl(device) + impl = _resolve_impl(device) - _check_backend_impl_device_compatibility(backend_impl, device) - return backend_impl, device # pytype: disable=bad-return-type + _check_impl_device_compatibility(impl, device) + return impl, device # pytype: disable=bad-return-type def _strip_weak_type(tree): @@ -167,7 +167,7 @@ def _strip_weak_type(tree): def _put_option( o: mujoco.MjOption, - backend_impl: types.BackendImpl, + impl: types.Impl, impl_fields: Optional[dict[str, Any]] = None, ) -> types.Option: """Returns mjx.Option given mujoco.MjOption.""" @@ -195,7 +195,7 @@ def _put_option( fields['disableflags'] = types.DisableBit(o.disableflags) fields['enableflags'] = types.EnableBit(o.enableflags) - if backend_impl == types.BackendImpl.JAX: + if impl == types.Impl.JAX: has_fluid_params = o.density > 0 or o.viscosity > 0 or o.wind.any() implicitfast = o.integrator == mujoco.mjtIntegrator.mjINT_IMPLICITFAST if implicitfast and has_fluid_params: @@ -203,12 +203,12 @@ def _put_option( fields['has_fluid_params'] = has_fluid_params return types.OptionJAX(**fields, **(impl_fields or {})) - if backend_impl == types.BackendImpl.C: + if impl == types.Impl.C: c_field_keys = types.OptionC.__annotations__.keys() - fields.keys() c_fields = {k: getattr(o, k, None) for k in c_field_keys} return types.OptionC(**fields, **c_fields, **(impl_fields or {})) - raise NotImplementedError(f'Unsupported backend: {backend_impl}') + raise NotImplementedError(f'Unsupported implementation: {impl}') def _put_statistic(s: mujoco.MjStatistic) -> types.Statistic: @@ -283,7 +283,7 @@ def _put_model_jax( mj_field_names = {f.name for f in types.Model.fields() if f.name != '_impl'} fields = {f: getattr(m, f) for f in mj_field_names} fields['cam_mat0'] = fields['cam_mat0'].reshape((-1, 3, 3)) - fields['opt'] = _put_option(m.opt, types.BackendImpl.JAX) + fields['opt'] = _put_option(m.opt, types.Impl.JAX) fields['stat'] = _put_statistic(m.stat) fields_jax = {} @@ -340,7 +340,7 @@ def _put_model_c( mj_field_names = {f.name for f in types.Model.fields() if f.name != '_impl'} fields = {f: getattr(m, f) for f in mj_field_names} fields['cam_mat0'] = fields['cam_mat0'].reshape((-1, 3, 3)) - fields['opt'] = _put_option(m.opt, backend_impl=types.BackendImpl.C) + fields['opt'] = _put_option(m.opt, impl=types.Impl.C) fields['stat'] = _put_statistic(m.stat) c_impl_keys = ( @@ -359,7 +359,7 @@ def _put_model_c( def put_model( m: mujoco.MjModel, device: Optional[jax.Device] = None, - backend_impl: Optional[Union[str, types.BackendImpl]] = None, + impl: Optional[Union[str, types.Impl]] = None, _full_compat: bool = False, # pylint: disable=invalid-name ) -> types.Model: """Puts mujoco.MjModel onto a device, resulting in mjx.Model. @@ -367,7 +367,7 @@ def put_model( Args: m: the model to put onto device device: which device to use - if unspecified picks the default device - backend_impl: backend implementation to use + impl: implementation to use _full_compat: put all MjModel fields onto device irrespective of MJX support This is an experimental feature. Avoid using it for now. @@ -375,28 +375,29 @@ def put_model( an mjx.Model placed on device Raises: - ValueError: if backend_impl is not supported + ValueError: if impl is not supported DeprecationWarning: if _full_compat is True """ if _full_compat: warnings.warn( - 'mjx.put_model(..., _full_compat=True) is deprecated. Use' - ' mjx.put_model(..., backend_impl=types.BackendImpl.C) instead.', + 'mjx.put_model(..., _full_compat=True) is deprecated and will be' + ' removed in MuJoCo >=3.4. Use mjx.put_model(..., impl=types.Impl.C)' + ' instead.', DeprecationWarning, stacklevel=2, ) - backend_impl = types.BackendImpl.C + impl = types.Impl.C - backend_impl, device = _resolve_backend_impl_and_device(backend_impl, device) - if backend_impl == types.BackendImpl.JAX: + impl, device = _resolve_impl_and_device(impl, device) + if impl == types.Impl.JAX: return _put_model_jax(m, device) - elif backend_impl == types.BackendImpl.C: + elif impl == types.Impl.C: return _put_model_c(m, device) - elif backend_impl == types.BackendImpl.WARP: - raise NotImplementedError('Warp backend not implemented yet.') + elif impl == types.Impl.WARP: + raise NotImplementedError('Warp implementation not implemented yet.') else: - raise ValueError(f'Unsupported backend implementation: {backend_impl}') + raise ValueError(f'Unsupported implementation: {impl}') def _make_data_public_fields(m: types.Model) -> Dict[str, Any]: @@ -696,7 +697,7 @@ def _make_data_c( def make_data( m: Union[types.Model, mujoco.MjModel], device: Optional[jax.Device] = None, - backend_impl: Optional[Union[str, types.BackendImpl]] = None, + impl: Optional[Union[str, types.Impl]] = None, _full_compat: bool = False, # pylint: disable=invalid-name ) -> types.Data: """Allocate and initialize Data. @@ -704,7 +705,7 @@ def make_data( Args: m: the model to use device: which device to use - if unspecified picks the default device - backend_impl: backend implementation to use + impl: implementation to use ('jax', 'warp') _full_compat: put all fields onto device irrespective of MJX support This is an experimental feature. Avoid using it for now. If using this flag, also use _full_compat for put_model. @@ -713,35 +714,34 @@ def make_data( an initialized mjx.Data placed on device Raises: - ValueError: if the model's backend_impl does not match the make_data - backend_impl - NotImplementedError: if the backend_impl is not implemented yet + ValueError: if the model's impl does not match the make_data impl + NotImplementedError: if the impl is not implemented yet DeprecationWarning: if _full_compat is used """ if _full_compat: warnings.warn( 'mjx.make_data(..., _full_compat=True) is deprecated. Use' - ' mjx.make_data(..., backend_impl=types.BackendImpl.C) instead.', + ' mjx.make_data(..., impl=types.Impl.C) instead.', DeprecationWarning, stacklevel=2, ) - backend_impl = types.BackendImpl.C + impl = types.Impl.C - backend_impl, device = _resolve_backend_impl_and_device(backend_impl, device) + impl, device = _resolve_impl_and_device(impl, device) - if isinstance(m, types.Model) and m.backend_impl != backend_impl: + if isinstance(m, types.Model) and m.impl != impl: raise ValueError( - f'Model backend_impl {m.backend_impl} does not match make_data ' - f'backend_impl {backend_impl}.' + f'Model impl {m.impl} does not match make_data ' + f'implementation {impl}.' ) - if backend_impl == types.BackendImpl.JAX: + if impl == types.Impl.JAX: return _make_data_jax(m, device) - elif backend_impl == types.BackendImpl.C: + elif impl == types.Impl.C: return _make_data_c(m, device) raise NotImplementedError( - f'make_data for backend_impl "{backend_impl}" not implemented yet.' + f'make_data for implementation "{impl}" not implemented yet.' ) @@ -951,7 +951,7 @@ def _put_data_c( if hasattr(d, f.name) } - # TODO(stunya): support islanding via C backend impl. + # TODO(stunya): support islanding via C impl. impl_fields['solver_niter'] = impl_fields['solver_niter'][0] # TODO(btaba): remove dense actuator moment. @@ -1039,7 +1039,7 @@ def put_data( m: mujoco.MjModel, d: mujoco.MjData, device: Optional[jax.Device] = None, - backend_impl: Optional[Union[str, types.BackendImpl]] = None, + impl: Optional[Union[str, types.Impl]] = None, _full_compat: bool = False, # pylint: disable=invalid-name ) -> types.Data: """Puts mujoco.MjData onto a device, resulting in mjx.Data. @@ -1048,7 +1048,7 @@ def put_data( m: the model to use d: the data to put on device device: which device to use - if unspecified picks the default device - backend_impl: backend implementation to use + impl: implementation to use ('jax', 'warp') _full_compat: put all MjModel fields onto device irrespective of MJX support This is an experimental feature. Avoid using it for now. If using this flag, also use _full_compat for put_model. @@ -1059,20 +1059,20 @@ def put_data( if _full_compat: warnings.warn( 'mjx.put_data(..., _full_compat=True) is deprecated. Use' - ' mjx.put_data(..., backend_impl=types.BackendImpl.C) instead.', + ' mjx.put_data(..., impl=types.Impl.C) instead.', DeprecationWarning, stacklevel=2, ) - backend_impl = types.BackendImpl.C + impl = types.Impl.C - backend_impl, device = _resolve_backend_impl_and_device(backend_impl, device) - if backend_impl == types.BackendImpl.JAX: + impl, device = _resolve_impl_and_device(impl, device) + if impl == types.Impl.JAX: return _put_data_jax(m, d, device) - elif backend_impl == types.BackendImpl.C: + elif impl == types.Impl.C: return _put_data_c(m, d, device) raise NotImplementedError( - f'put_data for backend_impl "{backend_impl}" not implemented yet.' + f'put_data for implementation "{impl}" not implemented yet.' ) @@ -1097,7 +1097,7 @@ def _get_data_into( batch_size = d.qpos.shape[0] if batched else 1 dof_i, dof_j = [], [] - if d.backend_impl == types.BackendImpl.JAX: + if d.impl == types.Impl.JAX: for i in range(m.nv): j = i while j > -1: @@ -1116,13 +1116,13 @@ def _get_data_into( if ncon != result_i.ncon or nefc != result_i.nefc or nj != result_i.nJ: mujoco._functions._realloc_con_efc(result_i, ncon=ncon, nefc=nefc, nJ=nj) # pylint: disable=protected-access - if d.backend_impl == types.BackendImpl.JAX: + if d.impl == types.Impl.JAX: all_fields = types.Data.fields() + types.DataJAX.fields() - elif d.backend_impl == types.BackendImpl.C: + elif d.impl == types.Impl.C: all_fields = types.Data.fields() + types.DataC.fields() else: raise NotImplementedError( - f'get_data_into for backend_impl "{d.backend_impl}" not implemented' + f'get_data_into for implementation "{d.impl}" not implemented' ' yet.' ) @@ -1188,7 +1188,7 @@ def _get_data_into( value = value.reshape(-1) elif field.name.startswith('efc_'): value = value[efc_active] - if d.backend_impl == types.BackendImpl.JAX: + if d.impl == types.Impl.JAX: if field.name == 'qM' and not support.is_sparse(m): value = value[dof_i, dof_j] elif field.name == 'qLD' and not support.is_sparse(m): @@ -1226,12 +1226,12 @@ def get_data_into( d = jax.device_get(d) - if d.backend_impl in (types.BackendImpl.JAX, types.BackendImpl.C): + if d.impl in (types.Impl.JAX, types.Impl.C): # TODO(stunya): Split out _get_data_into once codepaths diverge enough. return _get_data_into(result, m, d) raise NotImplementedError( - f'get_data_into for backend_impl "{d.backend_impl}" not implemented yet.' + f'get_data_into for implementation "{d.impl}" not implemented yet.' ) diff --git a/mjx/mujoco/mjx/_src/io_test.py b/mjx/mujoco/mjx/_src/io_test.py index 33565999..4bac5e3c 100644 --- a/mjx/mujoco/mjx/_src/io_test.py +++ b/mjx/mujoco/mjx/_src/io_test.py @@ -26,8 +26,8 @@ from mujoco.mjx._src import io as mjx_io from mujoco.mjx._src import test_util # pylint: disable=g-importing-member -from mujoco.mjx._src.types import BackendImpl from mujoco.mjx._src.types import ConeType +from mujoco.mjx._src.types import Impl # pylint: enable=g-importing-member import numpy as np @@ -114,11 +114,11 @@ class ModelIOTest(parameterized.TestCase): @parameterized.product( xml=(_MULTIPLE_CONVEX_OBJECTS, _MULTIPLE_CONSTRAINTS), - backend_impl=('jax', 'c'), + impl=('jax', 'c'), ) - def test_put_model(self, xml, backend_impl): + def test_put_model(self, xml, impl): m = mujoco.MjModel.from_xml_string(xml) - mx = mjx.put_model(m, backend_impl=backend_impl) + mx = mjx.put_model(m, impl=impl) def assert_not_weak_type(x): if isinstance(x, jax.Array): @@ -140,10 +140,10 @@ class ModelIOTest(parameterized.TestCase): self.assertEqual(mx.nM, m.nM) self.assertAlmostEqual(mx.opt.timestep, m.opt.timestep) - if backend_impl == 'jax': + if impl == 'jax': # fields restricted to MuJoCo should not be populated self.assertFalse(hasattr(mx, 'bvh_aabb')) - elif backend_impl == 'c': + elif impl == 'c': # Options specific to C are populated. self.assertEqual(mx.opt.apirate, m.opt.apirate) # Fields private to C backend impl are populated. @@ -177,7 +177,7 @@ class ModelIOTest(parameterized.TestCase): mujoco.MjModel.from_xml_string( '' ), - backend_impl='jax', + impl='jax', ) self.assertTrue(m.opt.has_fluid_params) @@ -218,7 +218,7 @@ class ModelIOTest(parameterized.TestCase): """), - backend_impl='jax', + impl='jax', ) def test_implicitfast_fluid_not_implemented(self): @@ -229,18 +229,18 @@ class ModelIOTest(parameterized.TestCase):