Merge branch 'usd-integration' of github.com:awesome-aj0123/mujoco into usd-integration

This commit is contained in:
Abhishek Joshi
2023-10-19 12:25:52 -05:00
185 changed files with 118687 additions and 4357 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ set(MSVC_INCREMENTAL_DEFAULT ON)
project(
mujoco
VERSION 2.3.8
VERSION 3.0.1
DESCRIPTION "MuJoCo Physics Simulator"
HOMEPAGE_URL "https://mujoco.org"
)
+4 -3
View File
@@ -59,7 +59,7 @@ set(MUJOCO_DEP_VERSION_benchmark
)
set(MUJOCO_DEP_VERSION_sdflib
492847fa81e46653114da48e8886730ccefed377
7c49cfba9bbec763b5d0f7b90b26555f3dde8088
CACHE STRING "Version of `SdfLib` to be fetched."
)
@@ -184,6 +184,9 @@ findorfetch(
EXCLUDE_FROM_ALL
)
option(SDFLIB_USE_ASSIMP OFF)
option(SDFLIB_USE_OPENMP OFF)
option(SDFLIB_USE_ENOKI OFF)
findorfetch(
USE_SYSTEM_PACKAGE
OFF
@@ -195,8 +198,6 @@ findorfetch(
https://github.com/UPC-ViRVIG/SdfLib.git
GIT_TAG
${MUJOCO_DEP_VERSION_sdflib}
PATCH_COMMAND
git apply --reject --whitespace=fix ${mujoco_SOURCE_DIR}/cmake/sdflib-optional-dependencies.patch
TARGETS
SdfLib
EXCLUDE_FROM_ALL
-585
View File
@@ -1,585 +0,0 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 20551cf..0d2a364 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -70,15 +70,25 @@ add_custom_target(copyShaders ALL SOURCES ${SHADER_FILES})
# Add dependencies
add_subdirectory(libs)
+if(SDFLIB_USE_ENOKI)
+ target_link_libraries(${PROJECT_NAME} PUBLIC enoki)
+ target_link_libraries(${PROJECT_NAME} PUBLIC fcpw)
+ target_compile_definitions(${PROJECT_NAME} PUBLIC -DENOKI_AVAILABLE)
+endif()
+
+if(SDFLIB_USE_ASSIMP)
+ target_link_libraries(${PROJECT_NAME} PUBLIC assimp)
+ target_compile_definitions(${PROJECT_NAME} PUBLIC -DASSIMP_AVAILABLE)
+endif()
+
+if(SDFLIB_BUILD_APPS OR SDFLIB_BUILD_DEBUG_APPS)
+ target_link_libraries(${PROJECT_NAME} PUBLIC args)
+ target_link_libraries(${PROJECT_NAME} PUBLIC stb_image)
+endif()
+
target_link_libraries(${PROJECT_NAME} PUBLIC glm)
-target_link_libraries(${PROJECT_NAME} PUBLIC assimp)
-target_link_libraries(${PROJECT_NAME} PUBLIC args)
target_link_libraries(${PROJECT_NAME} PUBLIC spdlog)
target_link_libraries(${PROJECT_NAME} PUBLIC cereal)
-target_link_libraries(${PROJECT_NAME} PUBLIC enoki)
-target_link_libraries(${PROJECT_NAME} PUBLIC eigen)
-target_link_libraries(${PROJECT_NAME} PUBLIC fcpw)
-target_link_libraries(${PROJECT_NAME} PUBLIC stb_image)
target_link_libraries(${PROJECT_NAME} PUBLIC icg)
if(CMAKE_CXX_COMPILER_ID MATCHES GNU)
@@ -86,17 +96,22 @@ if(CMAKE_CXX_COMPILER_ID MATCHES GNU)
endif()
# Add openMP
-if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
- message("Enabling openmp llvm extension")
- set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /openmp:llvm")
-else()
- find_package(OpenMP)
- if(NOT OpenMP_CXX_FOUND)
- message(FATAL_ERROR "OpenMP not found")
- endif()
- message("OpenMP version ${OpenMP_CXX_VERSION}")
- target_link_libraries(${PROJECT_NAME} PUBLIC OpenMP::OpenMP_CXX)
-endif()
+if(SDFLIB_USE_OPENMP)
+ if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
+ message("Enabling openmp llvm extension")
+ set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /openmp:llvm")
+ target_compile_definitions(${PROJECT_NAME} PUBLIC -DOPENMP_AVAILABLE)
+ else()
+ find_package(OpenMP)
+ if(OpenMP_CXX_FOUND)
+ message("OpenMP version ${OpenMP_CXX_VERSION}")
+ target_link_libraries(${PROJECT_NAME} PUBLIC OpenMP::OpenMP_CXX)
+ target_compile_definitions(${PROJECT_NAME} PUBLIC -DOPENMP_AVAILABLE)
+ else()
+ message("Disabling openmp")
+ endif()
+ endif()
+endif()
# Add executable
if (NOT UNIX)
@@ -160,6 +175,7 @@ if(SDFLIB_BUILD_DEBUG_APPS)
add_executable(GJKtest src/tools/GJKtest/main.cpp)
target_link_libraries(GJKtest PUBLIC ${PROJECT_NAME})
+ target_link_libraries(${PROJECT_NAME} PUBLIC eigen)
add_executable(CalculateInterpolationParameters src/tools/CalculateInterpolationParameters/main.cpp)
target_link_libraries(CalculateInterpolationParameters PUBLIC ${PROJECT_NAME})
diff --git a/include/SdfLib/ExactOctreeSdf.h b/include/SdfLib/ExactOctreeSdf.h
index 79ad82d..06dd7ac 100644
--- a/include/SdfLib/ExactOctreeSdf.h
+++ b/include/SdfLib/ExactOctreeSdf.h
@@ -214,6 +214,8 @@ private:
};
}
+#ifdef OPENMP_AVAILABLE
#include "ExactOctreeSdfDepthFirst.h"
+#endif
#endif
\ No newline at end of file
diff --git a/include/SdfLib/InterpolationMethods.h b/include/SdfLib/InterpolationMethods.h
index 077dfb2..f707d5b 100644
--- a/include/SdfLib/InterpolationMethods.h
+++ b/include/SdfLib/InterpolationMethods.h
@@ -4,7 +4,10 @@
#include <array>
#include "utils/TriangleUtils.h"
+
+#ifdef ENOKI_AVAILABLE
#include "enoki/array.h"
+#endif
namespace sdflib
{
@@ -236,15 +239,6 @@ struct TriLinearInterpolation
// outCoeff[63] = 8 * inValues[0][0] + 4 * inValues[0][1] * nodeSize + 4 * inValues[0][2] * nodeSize + 4 * inValues[0][3] * nodeSize + -8 * inValues[1][0] + 4 * inValues[1][1] * nodeSize + -4 * inValues[1][2] * nodeSize + -4 * inValues[1][3] * nodeSize + -8 * inValues[2][0] + -4 * inValues[2][1] * nodeSize + 4 * inValues[2][2] * nodeSize + -4 * inValues[2][3] * nodeSize + 8 * inValues[3][0] + -4 * inValues[3][1] * nodeSize + -4 * inValues[3][2] * nodeSize + 4 * inValues[3][3] * nodeSize + -8 * inValues[4][0] + -4 * inValues[4][1] * nodeSize + -4 * inValues[4][2] * nodeSize + 4 * inValues[4][3] * nodeSize + 8 * inValues[5][0] + -4 * inValues[5][1] * nodeSize + 4 * inValues[5][2] * nodeSize + -4 * inValues[5][3] * nodeSize + 8 * inValues[6][0] + 4 * inValues[6][1] * nodeSize + -4 * inValues[6][2] * nodeSize + -4 * inValues[6][3] * nodeSize + -8 * inValues[7][0] + 4 * inValues[7][1] * nodeSize + 4 * inValues[7][2] * nodeSize + 4 * inValues[7][3] * nodeSize + 0.0f;
// }
-// inline static float interpolateValue(const std::array<float, NUM_COEFFICIENTS>& values, glm::vec3 fracPart)
-// {
-// return 0.0f
-// + values[0] + values[1] * fracPart[0] + values[2] * fracPart[0] * fracPart[0] + values[3] * fracPart[0] * fracPart[0] * fracPart[0] + values[4] * fracPart[1] + values[5] * fracPart[0] * fracPart[1] + values[6] * fracPart[0] * fracPart[0] * fracPart[1] + values[7] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] + values[8] * fracPart[1] * fracPart[1] + values[9] * fracPart[0] * fracPart[1] * fracPart[1] + values[10] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] + values[11] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] + values[12] * fracPart[1] * fracPart[1] * fracPart[1] + values[13] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] + values[14] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] + values[15] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1]
-// + values[16] * fracPart[2] + values[17] * fracPart[0] * fracPart[2] + values[18] * fracPart[0] * fracPart[0] * fracPart[2] + values[19] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[2] + values[20] * fracPart[1] * fracPart[2] + values[21] * fracPart[0] * fracPart[1] * fracPart[2] + values[22] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[2] + values[23] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[2] + values[24] * fracPart[1] * fracPart[1] * fracPart[2] + values[25] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[2] + values[26] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[2] + values[27] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[2] + values[28] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] + values[29] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] + values[30] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] + values[31] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2]
-// + values[32] * fracPart[2] * fracPart[2] + values[33] * fracPart[0] * fracPart[2] * fracPart[2] + values[34] * fracPart[0] * fracPart[0] * fracPart[2] * fracPart[2] + values[35] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[2] * fracPart[2] + values[36] * fracPart[1] * fracPart[2] * fracPart[2] + values[37] * fracPart[0] * fracPart[1] * fracPart[2] * fracPart[2] + values[38] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[2] * fracPart[2] + values[39] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[2] * fracPart[2] + values[40] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] + values[41] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] + values[42] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] + values[43] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] + values[44] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] + values[45] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] + values[46] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] + values[47] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2]
-// + values[48] * fracPart[2] * fracPart[2] * fracPart[2] + values[49] * fracPart[0] * fracPart[2] * fracPart[2] * fracPart[2] + values[50] * fracPart[0] * fracPart[0] * fracPart[2] * fracPart[2] * fracPart[2] + values[51] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[2] * fracPart[2] * fracPart[2] + values[52] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[53] * fracPart[0] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[54] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[55] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[56] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[57] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[58] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[59] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[60] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[61] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[62] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[63] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2];
-// }
-
// inline static void interpolateVertexValues(const std::array<float, NUM_COEFFICIENTS>& values, glm::vec3 fracPart, float nodeSize, std::array<float, VALUES_PER_VERTEX>& outValues)
// {
// outValues[0] = 0.0f
@@ -383,6 +377,7 @@ struct TriCubicInterpolation
outCoeff[63] = 8 * inValues[0][0] + 4 * inValues[0][1] + 4 * inValues[0][2] + 4 * inValues[0][3] + 2 * inValues[0][4] + 2 * inValues[0][5] + 2 * inValues[0][6] + 1 * inValues[0][7] + -8 * inValues[1][0] + 4 * inValues[1][1] + -4 * inValues[1][2] + -4 * inValues[1][3] + 2 * inValues[1][4] + 2 * inValues[1][5] + -2 * inValues[1][6] + 1 * inValues[1][7] + -8 * inValues[2][0] + -4 * inValues[2][1] + 4 * inValues[2][2] + -4 * inValues[2][3] + 2 * inValues[2][4] + -2 * inValues[2][5] + 2 * inValues[2][6] + 1 * inValues[2][7] + 8 * inValues[3][0] + -4 * inValues[3][1] + -4 * inValues[3][2] + 4 * inValues[3][3] + 2 * inValues[3][4] + -2 * inValues[3][5] + -2 * inValues[3][6] + 1 * inValues[3][7] + -8 * inValues[4][0] + -4 * inValues[4][1] + -4 * inValues[4][2] + 4 * inValues[4][3] + -2 * inValues[4][4] + 2 * inValues[4][5] + 2 * inValues[4][6] + 1 * inValues[4][7] + 8 * inValues[5][0] + -4 * inValues[5][1] + 4 * inValues[5][2] + -4 * inValues[5][3] + -2 * inValues[5][4] + 2 * inValues[5][5] + -2 * inValues[5][6] + 1 * inValues[5][7] + 8 * inValues[6][0] + 4 * inValues[6][1] + -4 * inValues[6][2] + -4 * inValues[6][3] + -2 * inValues[6][4] + -2 * inValues[6][5] + 2 * inValues[6][6] + 1 * inValues[6][7] + -8 * inValues[7][0] + 4 * inValues[7][1] + 4 * inValues[7][2] + 4 * inValues[7][3] + -2 * inValues[7][4] + -2 * inValues[7][5] + -2 * inValues[7][6] + 1 * inValues[7][7];
}
+#ifdef ENOKI_AVAILABLE
using vec4 = enoki::Array<float, 4>;
inline static float interpolateValue(const std::array<float, NUM_COEFFICIENTS>& values, glm::vec3 fracPart)
@@ -433,6 +428,16 @@ struct TriCubicInterpolation
return sum;
}
+#else
+ inline static float interpolateValue(const std::array<float, NUM_COEFFICIENTS>& values, glm::vec3 fracPart)
+ {
+ return 0.0f
+ + values[0] + values[1] * fracPart[0] + values[2] * fracPart[0] * fracPart[0] + values[3] * fracPart[0] * fracPart[0] * fracPart[0] + values[4] * fracPart[1] + values[5] * fracPart[0] * fracPart[1] + values[6] * fracPart[0] * fracPart[0] * fracPart[1] + values[7] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] + values[8] * fracPart[1] * fracPart[1] + values[9] * fracPart[0] * fracPart[1] * fracPart[1] + values[10] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] + values[11] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] + values[12] * fracPart[1] * fracPart[1] * fracPart[1] + values[13] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] + values[14] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] + values[15] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1]
+ + values[16] * fracPart[2] + values[17] * fracPart[0] * fracPart[2] + values[18] * fracPart[0] * fracPart[0] * fracPart[2] + values[19] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[2] + values[20] * fracPart[1] * fracPart[2] + values[21] * fracPart[0] * fracPart[1] * fracPart[2] + values[22] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[2] + values[23] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[2] + values[24] * fracPart[1] * fracPart[1] * fracPart[2] + values[25] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[2] + values[26] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[2] + values[27] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[2] + values[28] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] + values[29] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] + values[30] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] + values[31] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2]
+ + values[32] * fracPart[2] * fracPart[2] + values[33] * fracPart[0] * fracPart[2] * fracPart[2] + values[34] * fracPart[0] * fracPart[0] * fracPart[2] * fracPart[2] + values[35] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[2] * fracPart[2] + values[36] * fracPart[1] * fracPart[2] * fracPart[2] + values[37] * fracPart[0] * fracPart[1] * fracPart[2] * fracPart[2] + values[38] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[2] * fracPart[2] + values[39] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[2] * fracPart[2] + values[40] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] + values[41] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] + values[42] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] + values[43] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] + values[44] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] + values[45] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] + values[46] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] + values[47] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2]
+ + values[48] * fracPart[2] * fracPart[2] * fracPart[2] + values[49] * fracPart[0] * fracPart[2] * fracPart[2] * fracPart[2] + values[50] * fracPart[0] * fracPart[0] * fracPart[2] * fracPart[2] * fracPart[2] + values[51] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[2] * fracPart[2] * fracPart[2] + values[52] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[53] * fracPart[0] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[54] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[55] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[56] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[57] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[58] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[59] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[60] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[61] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[62] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2] + values[63] * fracPart[0] * fracPart[0] * fracPart[0] * fracPart[1] * fracPart[1] * fracPart[1] * fracPart[2] * fracPart[2] * fracPart[2];
+ }
+#endif
inline static glm::vec3 interpolateGradient(const std::array<float, NUM_COEFFICIENTS>& values, glm::vec3 fracPart)
{
@@ -493,4 +498,4 @@ struct TriCubicInterpolation
};
}
-#endif
\ No newline at end of file
+#endif
diff --git a/include/SdfLib/TrianglesInfluence.h b/include/SdfLib/TrianglesInfluence.h
index 3f3d33f..fc2ca52 100644
--- a/include/SdfLib/TrianglesInfluence.h
+++ b/include/SdfLib/TrianglesInfluence.h
@@ -1,7 +1,10 @@
#ifndef TRIANGLES_INFLUENCE_H
#define TRIANGLES_INFLUENCE_H
+#ifdef ENOKI_AVAILABLE
#include <fcpw/fcpw.h>
+#endif
+
#include "utils/Mesh.h"
#include "utils/TriangleUtils.h"
#include "OctreeSdfUtils.h"
@@ -1008,6 +1011,7 @@ struct VHQueries
}
};
+#ifdef ENOKI_AVAILABLE
template<typename T>
struct FCPWQueries
{
@@ -1118,6 +1122,8 @@ struct FCPWQueries
{
}
};
+#endif
+
}
-#endif
\ No newline at end of file
+#endif
diff --git a/include/SdfLib/utils/Mesh.h b/include/SdfLib/utils/Mesh.h
index 28d5486..7d21e44 100644
--- a/include/SdfLib/utils/Mesh.h
+++ b/include/SdfLib/utils/Mesh.h
@@ -4,9 +4,11 @@
#include <string>
#include <vector>
#include <glm/glm.hpp>
+#ifdef ASSIMP_AVAILABLE
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
+#endif
#include "SdfLib/utils/UsefullSerializations.h"
namespace sdflib
@@ -43,6 +45,23 @@ struct BoundingBox
return glm::length(glm::max(q,glm::vec3(0.0f))) + glm::min(glm::max(q.x, glm::max(q.y,q.z)),0.0f);
}
+ float getDistance(glm::vec3 point, glm::vec3& outGradient) const
+ {
+ glm::vec3 a = glm::abs(point) - getSize();
+ int k = a[0] > a[1] ? 0 : 1;
+ int l = a[2] > a[k] ? 2 : k;
+ if (a[l] < 0) {
+ outGradient[l] = point[l] / glm::abs(point[l]);
+ } else {
+ glm::vec3 b = glm::max(a, glm::vec3(0.0f));
+ float c = glm::length(b);
+ outGradient[0] = a[0] > 0 ? b[0] / c * point[0] / glm::abs(point[0]) : 0;
+ outGradient[1] = a[1] > 0 ? b[1] / c * point[1] / glm::abs(point[1]) : 0;
+ outGradient[2] = a[2] > 0 ? b[2] / c * point[2] / glm::abs(point[2]) : 0;
+ }
+ return getDistance(point);
+ }
+
template<class Archive>
void serialize(Archive & archive)
{
@@ -54,8 +73,10 @@ class Mesh
{
public:
Mesh() {}
+#ifdef ASSIMP_AVAILABLE
Mesh(std::string filePath);
Mesh(const aiMesh* mesh);
+#endif
Mesh(glm::vec3* vertices, uint32_t numVertices,
uint32_t* indices, uint32_t numIndices);
@@ -74,7 +95,9 @@ public:
void computeNormals();
void applyTransform(glm::mat4 trans);
private:
+#ifdef ASSIMP_AVAILABLE
void initMesh(const aiMesh* mesh);
+#endif
std::vector<glm::vec3> mVertices;
std::vector<uint32_t> mIndices;
@@ -83,4 +106,4 @@ private:
};
}
-#endif
\ No newline at end of file
+#endif
diff --git a/include/SdfLib/utils/TriangleUtils.h b/include/SdfLib/utils/TriangleUtils.h
index 9f930ed..6ee2304 100644
--- a/include/SdfLib/utils/TriangleUtils.h
+++ b/include/SdfLib/utils/TriangleUtils.h
@@ -2,6 +2,7 @@
#define TRIANGLE_UTILS_H
#include <glm/glm.hpp>
+#include <algorithm>
#include <vector>
#include <array>
#include <map>
@@ -401,4 +402,4 @@ namespace TriangleUtils
}
}
-#endif
\ No newline at end of file
+#endif
diff --git a/libs/CMakeLists.txt b/libs/CMakeLists.txt
index b48bf39..ea22b9a 100644
--- a/libs/CMakeLists.txt
+++ b/libs/CMakeLists.txt
@@ -14,37 +14,41 @@ if(NOT glm_lib_POPULATED)
endif()
# assimp
-FetchContent_Declare(assimp_lib
- GIT_REPOSITORY https://github.com/assimp/assimp.git
- GIT_TAG 9519a62dd20799c5493c638d1ef5a6f484e5faf1 # 5.2.5
-)
-
-if(NOT assimp_lib)
- FetchContent_Populate(assimp_lib)
-
- set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
- set(BUILD_SHARED_LIBS OFF)
- set(ASSIMP_BUILD_ASSIMP_TOOLS OFF)
- set(ASSIMP_BUILD_TESTS OFF)
- set(ASSIMP_INSTALL OFF)
- set(ASSIMP_INJECT_DEBUG_POSTFIX OFF)
- set(ASSIMP_BUILD_ASSIMP_VIEW OFF)
+if(SDF_USE_ASSIMP)
+ FetchContent_Declare(assimp_lib
+ GIT_REPOSITORY https://github.com/assimp/assimp.git
+ GIT_TAG 9519a62dd20799c5493c638d1ef5a6f484e5faf1 # 5.2.5
+ )
- add_subdirectory(${assimp_lib_SOURCE_DIR} ${assimp_lib_BINARY_DIR})
+ if(NOT assimp_lib)
+ FetchContent_Populate(assimp_lib)
+
+ set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
+ set(BUILD_SHARED_LIBS OFF)
+ set(ASSIMP_BUILD_ASSIMP_TOOLS OFF)
+ set(ASSIMP_BUILD_TESTS OFF)
+ set(ASSIMP_INSTALL OFF)
+ set(ASSIMP_INJECT_DEBUG_POSTFIX OFF)
+ set(ASSIMP_BUILD_ASSIMP_VIEW OFF)
+
+ add_subdirectory(${assimp_lib_SOURCE_DIR} ${assimp_lib_BINARY_DIR})
+ endif()
endif()
# args
-FetchContent_Declare(args_lib
- GIT_REPOSITORY https://github.com/Taywee/args.git
- GIT_TAG a48e1f880813b367d2354963a58dedbf2b708584 # 6.3.0
-)
-
-FetchContent_GetProperties(args_lib)
-if(NOT args_lib_POPULATED)
- FetchContent_Populate(args_lib)
- add_library(args INTERFACE)
- target_include_directories(args INTERFACE ${args_lib_SOURCE_DIR})
-endif()
+if(SDFLIB_BUILD_APPS OR SDFLIB_BUILD_DEBUG_APPS)
+ FetchContent_Declare(args_lib
+ GIT_REPOSITORY https://github.com/Taywee/args.git
+ GIT_TAG a48e1f880813b367d2354963a58dedbf2b708584 # 6.3.0
+ )
+
+ FetchContent_GetProperties(args_lib)
+ if(NOT args_lib_POPULATED)
+ FetchContent_Populate(args_lib)
+ add_library(args INTERFACE)
+ target_include_directories(args INTERFACE ${args_lib_SOURCE_DIR})
+ endif()
+ endif()
# spdlog
FetchContent_Declare(spdlog_lib
@@ -76,47 +80,53 @@ if(NOT cereal_lib_POPULATED)
endif()
# Enoki
-FetchContent_Declare(enoki_lib
- GIT_REPOSITORY https://github.com/mitsuba-renderer/enoki.git
- GIT_TAG 2a18afa
-)
-FetchContent_GetProperties(enoki_lib)
-if(NOT enoki_lib_POPULATED)
- FetchContent_Populate(enoki_lib)
- add_library(enoki INTERFACE)
- add_subdirectory(${enoki_lib_SOURCE_DIR} ${enoki_lib_BINARY_DIR})
- target_include_directories(enoki INTERFACE ${enoki_lib_SOURCE_DIR}/include)
-endif()
-
-# eigen
-FetchContent_Declare(eigen_lib
-GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git
-GIT_TAG 46126273552afe13692929523d34006f54c19719 # 3.4
-)
+if(SDFLIB_USE_ENOKI)
+ FetchContent_Declare(enoki_lib
+ GIT_REPOSITORY https://github.com/mitsuba-renderer/enoki.git
+ GIT_TAG 2a18afa
+ )
+ FetchContent_GetProperties(enoki_lib)
+ if(NOT enoki_lib_POPULATED)
+ FetchContent_Populate(enoki_lib)
+ add_library(enoki INTERFACE)
+ add_subdirectory(${enoki_lib_SOURCE_DIR} ${enoki_lib_BINARY_DIR})
+ target_include_directories(enoki INTERFACE ${enoki_lib_SOURCE_DIR}/include)
+ endif()
-FetchContent_GetProperties(eigen_lib)
-if(NOT eigen_lib_POPULATED)
- FetchContent_Populate(eigen_lib)
- add_library(eigen INTERFACE)
- target_include_directories(eigen INTERFACE ${eigen_lib_SOURCE_DIR})
+ # FCPW
+ FetchContent_Declare(fcpw_lib
+ GIT_REPOSITORY https://github.com/rohan-sawhney/fcpw.git
+ GIT_TAG dd65ec2
+ )
+
+ FetchContent_GetProperties(fcpw_lib)
+ if(NOT fcpw_lib_POPULATED)
+ FetchContent_Populate(fcpw_lib)
+ add_subdirectory(${fcpw_lib_SOURCE_DIR} ${fcpw_lib_BINARY_DIR})
+ target_include_directories(fcpw INTERFACE ${fcpw_lib_SOURCE_DIR})
+ endif()
endif()
-# FCPW
-FetchContent_Declare(fcpw_lib
- GIT_REPOSITORY https://github.com/rohan-sawhney/fcpw.git
- GIT_TAG dd65ec2
-)
-
-FetchContent_GetProperties(fcpw_lib)
-if(NOT fcpw_lib_POPULATED)
- FetchContent_Populate(fcpw_lib)
- add_subdirectory(${fcpw_lib_SOURCE_DIR} ${fcpw_lib_BINARY_DIR})
- target_include_directories(fcpw INTERFACE ${fcpw_lib_SOURCE_DIR})
-endif()
+# eigen
+if(SDFLIB_BUILD_DEBUG_APPS)
+ FetchContent_Declare(eigen_lib
+ GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git
+ GIT_TAG 46126273552afe13692929523d34006f54c19719 # 3.4
+ )
+
+ FetchContent_GetProperties(eigen_lib)
+ if(NOT eigen_lib_POPULATED)
+ FetchContent_Populate(eigen_lib)
+ add_library(eigen INTERFACE)
+ target_include_directories(eigen INTERFACE ${eigen_lib_SOURCE_DIR})
+ endif()
+ endif()
# stb
-add_library(stb_image INTERFACE)
-target_include_directories(stb_image INTERFACE stb)
+if(SDFLIB_BUILD_APPS OR SDFLIB_BUILD_DEBUG_APPS)
+ add_library(stb_image INTERFACE)
+ target_include_directories(stb_image INTERFACE stb)
+ endif()
# icg
add_library(icg INTERFACE)
diff --git a/src/sdf/OctreeSdf.cpp b/src/sdf/OctreeSdf.cpp
index ef8ed4d..0e1eb97 100644
--- a/src/sdf/OctreeSdf.cpp
+++ b/src/sdf/OctreeSdf.cpp
@@ -6,7 +6,9 @@
#include "SdfLib/InterpolationMethods.h"
#include "sdf/OctreeSdfDepthFirst.h"
#include "sdf/OctreeSdfBreadthFirst.h"
+#ifdef OPENMP_AVAILABLE
#include "sdf/OctreeSdfBreadthFirstNoDelay.h"
+#endif
#include <array>
#include <stack>
@@ -46,8 +48,11 @@ OctreeSdf::OctreeSdf(const Mesh& mesh, BoundingBox box,
break;
case OctreeSdf::InitAlgorithm::CONTINUITY:
//initOctreeWithContinuity<PerNodeRegionTrianglesInfluence<InterpolationMethod>>(mesh, startDepth, depth, terminationThreshold, terminationRule);
- // initOctreeWithContinuity<VHQueries<InterpolationMethod>>(mesh, startDepth, depth, terminationThreshold, terminationRule);
+#ifdef OPENMP_AVAILABLE
initOctreeWithContinuityNoDelay<VHQueries<InterpolationMethod>>(mesh, startDepth, depth, terminationThreshold, terminationRule, numThreads);
+#else
+ initOctreeWithContinuity<VHQueries<InterpolationMethod>>(mesh, startDepth, depth, terminationThreshold, terminationRule);
+#endif
break;
// case OctreeSdf::InitAlgorithm::GPU_IMPLEMENTATION:
// Timer time;
@@ -78,7 +83,7 @@ float OctreeSdf::getDistance(glm::vec3 sample) const
startArrayPos.y < 0 || startArrayPos.y >= mStartGridSize ||
startArrayPos.z < 0 || startArrayPos.z >= mStartGridSize)
{
- return mBox.getDistance(sample) + glm::sqrt(3.0f) * mBox.getSize().x;
+ return mBox.getDistance(sample) + mMinBorderValue;
}
const OctreeNode* currentNode = &mOctreeData[startArrayPos.z * mStartGridXY + startArrayPos.y * mStartGridSize + startArrayPos.x];
@@ -108,7 +113,7 @@ float OctreeSdf::getDistance(glm::vec3 sample, glm::vec3& outGradient) const
startArrayPos.y < 0 || startArrayPos.y >= mStartGridSize ||
startArrayPos.z < 0 || startArrayPos.z >= mStartGridSize)
{
- return mBox.getDistance(sample) + mMinBorderValue;
+ return mBox.getDistance(sample, outGradient) + mMinBorderValue;
}
const OctreeNode* currentNode = &mOctreeData[startArrayPos.z * mStartGridXY + startArrayPos.y * mStartGridSize + startArrayPos.x];
@@ -253,4 +258,4 @@ void OctreeSdf::getDepthDensity(std::vector<float>& depthsDensity)
size *= 0.125f;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/sdf/OctreeSdfDepthFirst.h b/src/sdf/OctreeSdfDepthFirst.h
index 53ee4b2..196d191 100644
--- a/src/sdf/OctreeSdfDepthFirst.h
+++ b/src/sdf/OctreeSdfDepthFirst.h
@@ -7,7 +7,10 @@
#include "SdfLib/OctreeSdfUtils.h"
#include <array>
#include <stack>
+#ifdef OPENMP_AVAILABLE
#include <omp.h>
+#endif
+
namespace sdflib
{
@@ -381,7 +384,9 @@ void OctreeSdf::initOctree(const Mesh& mesh, uint32_t startDepth, uint32_t maxDe
};
const uint32_t voxlesPerAxis = 1 << startDepth;
+#ifdef OPENMP_AVAILABLE
if(numThreads < 2)
+#endif
{
// Create the grid
mOctreeData.resize(voxlesPerAxis * voxlesPerAxis * voxlesPerAxis);
@@ -402,7 +407,8 @@ void OctreeSdf::initOctree(const Mesh& mesh, uint32_t startDepth, uint32_t maxDe
mValueRange = mainThread.valueRange;
}
- else
+#ifdef OPENMP_AVAILABLE
+ else
{
std::vector<ThreadContext> threadsContext(numThreads, mainThread);
@@ -511,6 +517,7 @@ void OctreeSdf::initOctree(const Mesh& mesh, uint32_t startDepth, uint32_t maxDe
}
#endif
}
+#endif
#ifdef SDFLIB_PRINT_STATISTICS
SPDLOG_INFO("Used an octree of max depth {}", maxDepth);
@@ -544,4 +551,4 @@ void OctreeSdf::initOctree(const Mesh& mesh, uint32_t startDepth, uint32_t maxDe
}
}
-#endif
\ No newline at end of file
+#endif
diff --git a/src/utils/Mesh.cpp b/src/utils/Mesh.cpp
index b407d38..6fff5fe 100644
--- a/src/utils/Mesh.cpp
+++ b/src/utils/Mesh.cpp
@@ -5,6 +5,7 @@
namespace sdflib
{
+#ifdef ASSIMP_AVAILABLE
Mesh::Mesh(std::string filePath)
{
Assimp::Importer import;
@@ -28,6 +29,7 @@ Mesh::Mesh(const aiMesh* mesh)
{
initMesh(mesh);
}
+#endif
Mesh::Mesh(glm::vec3* vertices, uint32_t numVertices,
uint32_t* indices, uint32_t numIndices)
@@ -39,7 +41,7 @@ Mesh::Mesh(glm::vec3* vertices, uint32_t numVertices,
std::memcpy(mIndices.data(), indices, sizeof(uint32_t) * numIndices);
}
-
+#ifdef ASSIMP_AVAILABLE
void Mesh::initMesh(const aiMesh* mesh)
{
if(!(mesh->mPrimitiveTypes & aiPrimitiveType_TRIANGLE))
@@ -83,6 +85,7 @@ void Mesh::initMesh(const aiMesh* mesh)
computeNormals();
}
}
+#endif
void Mesh::computeBoundingBox()
{
@@ -134,4 +137,4 @@ void Mesh::applyTransform(glm::mat4 trans)
computeBoundingBox();
}
-}
\ No newline at end of file
+}
+4 -4
View File
@@ -1,6 +1,6 @@
1 VERSIONINFO
FILEVERSION 2,3,8,0
PRODUCTVERSION 2,3,8,0
FILEVERSION 3,0,1,0
PRODUCTVERSION 3,0,1,0
FILEOS 0x4
FILETYPE 0x1
{
@@ -9,9 +9,9 @@ FILETYPE 0x1
BLOCK "040904b0"
{
VALUE "ProductName", "MuJoCo"
VALUE "ProductVersion", "2.3.8"
VALUE "ProductVersion", "3.0.1"
VALUE "FileDescription", "MuJoCo"
VALUE "FileVersion", "2.3.8"
VALUE "FileVersion", "3.0.1"
VALUE "InternalName", "mujoco.dll"
VALUE "OriginalFilename", "mujoco.dll"
VALUE "CompanyName", "Google DeepMind"
+4 -4
View File
@@ -1,8 +1,8 @@
MUJOCO ICON "mujoco.ico"
1 VERSIONINFO
FILEVERSION 2,3,8,0
PRODUCTVERSION 2,3,8,0
FILEVERSION 3,0,1,0
PRODUCTVERSION 3,0,1,0
FILEOS 0x4
FILETYPE 0x1
{
@@ -11,9 +11,9 @@ FILETYPE 0x1
BLOCK "040904b0"
{
VALUE "ProductName", "MuJoCo"
VALUE "ProductVersion", "2.3.8"
VALUE "ProductVersion", "3.0.1"
VALUE "FileDescription", "MuJoCo"
VALUE "FileVersion", "2.3.8"
VALUE "FileVersion", "3.0.1"
VALUE "InternalName", "simulate.exe"
VALUE "OriginalFilename", "simulate.exe"
VALUE "CompanyName", "Google DeepMind"
+1 -1
View File
@@ -522,7 +522,7 @@ shown in the table below. Their names are in the format ``mjKEY_XXX``. They corr
- Maximum number of UI rectangles.
Defined in `mjui.h <https://github.com/google-deepmind/mujoco/blob/main/include/mujoco/mjui.h>`_.
* - ``mjVERSION_HEADER``
- 238
- 301
- The version of the MuJoCo headers; changes with every release. This is an integer equal to 100x the software
version, so 210 corresponds to version 2.1. Defined in mujoco.h. The API function :ref:`mj_version` returns a
number with the same meaning but for the compiled library.
+633 -293
View File
File diff suppressed because it is too large Load Diff
+27 -9
View File
@@ -485,19 +485,19 @@
| :ref:`flexcomp | \* | :class: mjcf-attributes |
| <body-flexcomp>` | | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`name<body-flexcomp-name>` | :ref:`class<body-flexcomp-class>` | :ref:`type<body-flexcomp-type>` | :ref:`dim<body-flexcomp-dim>` | |
| | | | :ref:`name<body-flexcomp-name>` | :ref:`class<body-flexcomp-class>` | :ref:`type<body-flexcomp-type>` | :ref:`group<body-flexcomp-group>` | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`flatskin<body-flexcomp-flatskin>` | :ref:`count<body-flexcomp-count>` | :ref:`spacing<body-flexcomp-spacing>` | :ref:`radius<body-flexcomp-radius>` | |
| | | | :ref:`dim<body-flexcomp-dim>` | :ref:`count<body-flexcomp-count>` | :ref:`spacing<body-flexcomp-spacing>` | :ref:`radius<body-flexcomp-radius>` | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`rigid<body-flexcomp-rigid>` | :ref:`mass<body-flexcomp-mass>` | :ref:`inertiabox<body-flexcomp-inertiabox>` | :ref:`scale<body-flexcomp-scale>` | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`file<body-flexcomp-file>` | :ref:`point<body-flexcomp-point>` | :ref:`element<body-flexcomp-element>` | :ref:`texcoord<body-flexcomp-texcoord>` | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`material<body-flexcomp-material>` | :ref:`rgba<body-flexcomp-rgba>` | :ref:`selfcollide<body-flexcomp-selfcollide>` | :ref:`flatskin<body-flexcomp-flatskin>` | |
| | | | :ref:`material<body-flexcomp-material>` | :ref:`rgba<body-flexcomp-rgba>` | :ref:`flatskin<body-flexcomp-flatskin>` | :ref:`pos<body-flexcomp-pos>` | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`pos<body-flexcomp-pos>` | :ref:`quat<body-flexcomp-quat>` | :ref:`axisangle<body-flexcomp-axisangle>` | :ref:`xyaxes<body-flexcomp-xyaxes>` | |
| | | | :ref:`quat<body-flexcomp-quat>` | :ref:`axisangle<body-flexcomp-axisangle>` | :ref:`xyaxes<body-flexcomp-xyaxes>` | :ref:`zaxis<body-flexcomp-zaxis>` | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`zaxis<body-flexcomp-zaxis>` | :ref:`euler<body-flexcomp-euler>` | | | |
| | | | :ref:`euler<body-flexcomp-euler>` | | | | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
+------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| |_2| flexcomp |br| |_2| |L| | | .. table:: |
@@ -517,7 +517,9 @@
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`friction<flexcomp-contact-friction>` | :ref:`solmix<flexcomp-contact-solmix>` | :ref:`solref<flexcomp-contact-solref>` | :ref:`solimp<flexcomp-contact-solimp>` | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`margin<flexcomp-contact-margin>` | :ref:`gap<flexcomp-contact-gap>` | | | |
| | | | :ref:`margin<flexcomp-contact-margin>` | :ref:`gap<flexcomp-contact-gap>` | :ref:`internal<flexcomp-contact-internal>` | :ref:`selfcollide<flexcomp-contact-selfcollide>` | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`activelayers<flexcomp-contact-activelayers>` | | | | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
+------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| |_2| flexcomp |br| |_2| |L| | | .. table:: |
@@ -527,6 +529,20 @@
| | | | :ref:`id<flexcomp-pin-id>` | :ref:`range<flexcomp-pin-range>` | :ref:`grid<flexcomp-pin-grid>` | :ref:`gridrange<flexcomp-pin-gridrange>` | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
+------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| |_2| flexcomp |br| |_2| |L| | | .. table:: |
| :ref:`plugin | \* | :class: mjcf-attributes |
| <flexcomp-plugin>` | | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`plugin<flexcomp-plugin-plugin>` | :ref:`instance<flexcomp-plugin-instance>` | | | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
+------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| |_3| plugin |br| |_3| |L| | | .. table:: |
| :ref:`config | \* | :class: mjcf-attributes |
| <plugin-config>` | | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`key<plugin-config-key>` | :ref:`value<plugin-config-value>` | | | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
+------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| mujoco |br| |L| | | *no attributes* |
| :ref:`deformable<deformable>` | | |
+------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
@@ -536,9 +552,9 @@
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`name<deformable-flex-name>` | :ref:`group<deformable-flex-group>` | :ref:`dim<deformable-flex-dim>` | :ref:`radius<deformable-flex-radius>` | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`material<deformable-flex-material>` | :ref:`rgba<deformable-flex-rgba>` | :ref:`flatskin<deformable-flex-flatskin>` | :ref:`selfcollide<deformable-flex-selfcollide>` | |
| | | | :ref:`material<deformable-flex-material>` | :ref:`rgba<deformable-flex-rgba>` | :ref:`flatskin<deformable-flex-flatskin>` | :ref:`body<deformable-flex-body>` | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`body<deformable-flex-body>` | :ref:`vertex<deformable-flex-vertex>` | :ref:`element<deformable-flex-element>` | :ref:`texcoord<deformable-flex-texcoord>` | |
| | | | :ref:`vertex<deformable-flex-vertex>` | :ref:`element<deformable-flex-element>` | :ref:`texcoord<deformable-flex-texcoord>` | | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
+------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| |_2| flex |br| |_2| |L| | | .. table:: |
@@ -549,7 +565,9 @@
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`friction<flex-contact-friction>` | :ref:`solmix<flex-contact-solmix>` | :ref:`solref<flex-contact-solref>` | :ref:`solimp<flex-contact-solimp>` | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`margin<flex-contact-margin>` | :ref:`gap<flex-contact-gap>` | | | |
| | | | :ref:`margin<flex-contact-margin>` | :ref:`gap<flex-contact-gap>` | :ref:`internal<flex-contact-internal>` | :ref:`selfcollide<flex-contact-selfcollide>` | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`activelayers<flex-contact-activelayers>` | | | | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
+------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| |_2| flex |br| |_2| |L| | | .. table:: |
+104 -69
View File
@@ -5,18 +5,27 @@ Changelog
Upcoming version (not yet released)
-----------------------------------
Bug fixes
^^^^^^^^^
1. Fix in simulate: correct handling of "Pause update", "Fullscreen" and "VSync" buttons.
Version 3.0.0 (October 18, 2023)
--------------------------------
New features
^^^^^^^^^^^^
.. youtube:: Vc1tq0fFvQA
:align: right
:width: 240px
1. Added simulation on GPU and TPU via the new :doc:`mjx` (MJX) Python module. Python users can now
natively run MuJoCo simulations at millions of steps per second on Google TPU or their own accelerator hardware.
1. Added constraint island discovery with :ref:`mj_island`. Constraint islands are disjoint sets of constraints
and degrees-of-freedom that do not interact. The only solver which currently supports islands is
:ref:`CG<option-solver>`. Island discovery can be activated using a new :ref:`enable flag<option-flag-island>`.
If island discovery is enabled, geoms, contacts and tendons will be colored according to the corresponding island,
see video.
- MJX is designed to work with on-device reinforcement learning algorithms. This Colab notebook demonstrates using
MJX along with reinforcement learning to train humanoid and quadruped robots to locomote: |colab|
- The MJX API is compatible with MuJoCo but is missing some features in this release. See the outline of
:ref:`MJX feature parity <MjxFeatureParity>` for more details.
.. |colab| image:: https://colab.research.google.com/assets/colab-badge.svg
:target: https://colab.research.google.com/github/google-deepmind/mujoco/blob/main/mjx/tutorial.ipynb
.. youtube:: QewlEqIZi1o
:align: right
@@ -28,7 +37,37 @@ New features
- Added new SDF plugin for defining implicit geometries. The plugin must define methods computing an SDF and its
gradient at query points. See the :ref:`documentation<exWriting>` for more details.
3. Added :ref:`mjThreadPool` and :ref:`mjTask` which allow for multi-threaded operations within the MuJoCo engine
.. youtube:: ra2bTiZHGlw
:align: right
:width: 240px
3. Added new low-level model element called ``flex``, used to define deformable objects. These
`simplicial complexes <https://en.wikipedia.org/wiki/Simplicial_complex>`__ can be of dimension 1, 2
or 3, corresponding to stretchable lines, triangles or tetrahedra. Two new MJCF elements are used
to define flexes. The top-level :ref:`deformable<deformable>` section contains the low-level flex definition.
The :ref:`flexcomp<body-flexcomp>` element, similar to :ref:`composite<body-composite>` is a convenience macro for
creating deformables, and supports the GMSH tetrahedral file format.
- Added `shell <https://github.com/deepmind/mujoco/blob/main/plugin/elasticity/shell.cc>`__ passive force plugin,
computing bending forces using a constant precomputed Hessian (cotangent operator).
**Note**: This feature is still under development and subject to change. In particular, deformable object
functionality is currently available both via :ref:`deformable<CDeformable>` and :ref:`composite<CComposite>`,
and both are modifiable by the first-party
`elasticity plugins <https://github.com/google-deepmind/mujoco/tree/main/plugin/elasticity>`__. We expect some of
this functionality to be unified in the future.
.. youtube:: Vc1tq0fFvQA
:align: right
:width: 240px
4. Added constraint island discovery with :ref:`mj_island`. Constraint islands are disjoint sets of constraints
and degrees-of-freedom that do not interact. The only solver which currently supports islands is
:ref:`CG<option-solver>`. Island discovery can be activated using a new :ref:`enable flag<option-flag-island>`.
If island discovery is enabled, geoms, contacts and tendons will be colored according to the corresponding island,
see video. Island discovery is currently disabled for models that have deformable objects (see previous item).
5. Added :ref:`mjThreadPool` and :ref:`mjTask` which allow for multi-threaded operations within the MuJoCo engine
pipeline. If engine-internal threading is enabled, the following operations will be multi-threaded:
- Island constraint resolution, if island discovery is :ref:`enabled<option-flag-island>` and the
@@ -40,16 +79,8 @@ New features
Engine-internal threading is a work in progress and currently only available in first-party code via the
:ref:`testspeed<saTestspeed>` utility, exposed with the ``npoolthread`` flag.
.. youtube:: ra2bTiZHGlw
:align: right
:width: 240px
4. Added capability to initialize :ref:`composite<body-composite>` particles with arbitrary positions.
5. Added `shell <https://github.com/deepmind/mujoco/blob/main/plugin/elasticity/shell.cc>`__ passive force plugin:
- Collisions use spheres located at mesh vertices.
- Stretching as tendon constraints and bending using a constant precomputed Hessian (cotangent operator).
6. Added capability to initialize :ref:`composite<body-composite>` particles from OBJ files. Fixes :github:issue:`642`
and :github:issue:`674`.
General
^^^^^^^
@@ -57,32 +88,32 @@ General
.. admonition:: Breaking API changes
:class: attention
6. Removed the macros ``mjMARKSTACK`` and ``mjFREESTACK``.
7. Removed the macros ``mjMARKSTACK`` and ``mjFREESTACK``.
**Migration:** These macros have been replaced by new functions :ref:`mj_markStack` and
:ref:`mj_freeStack`. These functions manage the :ref:`mjData stack<siStack>` in a fully encapsulated way (i.e.,
without introducing a local variable at the call site).
7. Renamed ``mj_stackAlloc`` to :ref:`mj_stackAllocNum`. The new function :ref:`mj_stackAllocByte` allocates an
8. Renamed ``mj_stackAlloc`` to :ref:`mj_stackAllocNum`. The new function :ref:`mj_stackAllocByte` allocates an
arbitrary number of bytes and has an additional argument for specifying the alignment of the returned pointer.
**Migration:** The functionality for allocating ``mjtNum`` arrays is now available via :ref:`mj_stackAllocNum`.
8. Renamed the ``nstack`` field in :ref:`mjModel` and :ref:`mjData` to ``narena``. Changed ``narena``, ``pstack``,
9. Renamed the ``nstack`` field in :ref:`mjModel` and :ref:`mjData` to ``narena``. Changed ``narena``, ``pstack``,
and ``maxuse_stack`` to count number of bytes rather than number of :ref:`mjtNum` |-| s.
9. Changed :ref:`mjData.solver<mjData>`, the array used to collect solver diagnostic information.
This array of :ref:`mjSolverStat` structs is now of length ``mjNISLAND * mjNSOLVER``, interpreted as as a matrix.
Each row of length ``mjNSOLVER`` contains separate solver statistics for each constraint island.
If the solver does not use islands, only row 0 is filled.
10. Changed :ref:`mjData.solver<mjData>`, the array used to collect solver diagnostic information.
This array of :ref:`mjSolverStat` structs is now of length ``mjNISLAND * mjNSOLVER``, interpreted as as a matrix.
Each row of length ``mjNSOLVER`` contains separate solver statistics for each constraint island.
If the solver does not use islands, only row 0 is filled.
- The new constant :ref:`mjNISLAND<glNumeric>` was set to 20.
- :ref:`mjNSOLVER<glNumeric>` was reduced from 1000 to 200.
- Added :ref:`mjData.solver_nisland<mjData>`: the number of islands for which the solver ran.
- Renamed ``mjData.solver_iter`` to ``solver_niter``. Both this member and ``mjData.solver_nnz`` are now integer
vectors of length ``mjNISLAND``.
- The new constant :ref:`mjNISLAND<glNumeric>` was set to 20.
- :ref:`mjNSOLVER<glNumeric>` was reduced from 1000 to 200.
- Added :ref:`mjData.solver_nisland<mjData>`: the number of islands for which the solver ran.
- Renamed ``mjData.solver_iter`` to ``solver_niter``. Both this member and ``mjData.solver_nnz`` are now integer
vectors of length ``mjNISLAND``.
10. Removed ``mjOption.collision`` and the associated ``option/collision`` attribute.
11. Removed ``mjOption.collision`` and the associated ``option/collision`` attribute.
**Migration:**
@@ -93,40 +124,39 @@ General
:ref:`conaffinity<body-geom-conaffinity>` attributes in the model and then setting them globally to ``0`` using
|br| ``<default> <geom contype="0" conaffinity="0"/> </default>``.
11. Removed the :at:`rope` and :at:`cloth` composite objects.
12. Removed the :at:`rope` and :at:`cloth` composite objects.
**Migration:** Users should use the :at:`cable` and :at:`shell` elasticity plugins.
12. Added :ref:`mjData.eq_active<mjData>` user input variable, for enabling/disabling the state of equality
13. Added :ref:`mjData.eq_active<mjData>` user input variable, for enabling/disabling the state of equality
constraints. Renamed ``mjModel.eq_active`` to :ref:`mjModel.eq_active0<mjModel>`, which now has the semantic of
"initial value of ``mjData.eq_active``".
Fixes `#876 <https://github.com/google-deepmind/mujoco/discussions/876>`__
"initial value of ``mjData.eq_active``". Fixes :github:issue:`876`.
**Migration:** Replace uses of ``mjModel.eq_active`` with ``mjData.eq_active``.
13. Changed the default of :ref:`autolimits<compiler-autolimits>` from "false" to "true". This is a minor breaking
14. Changed the default of :ref:`autolimits<compiler-autolimits>` from "false" to "true". This is a minor breaking
change. The potential breakage applies to models which have elements with "range" defined and "limited" not set.
Such models cannot be loaded since version 2.2.2 (July 2022).
14. Added a new :ref:`dyntype<actuator-general-dyntype>`, ``filterexact``, which updates first-order filter states with
15. Added a new :ref:`dyntype<actuator-general-dyntype>`, ``filterexact``, which updates first-order filter states with
the exact formula rather than with Euler integration.
15. Added an actuator attribute, :ref:`actearly<actuator-general-actearly>`, which uses semi-implicit integration for
16. Added an actuator attribute, :ref:`actearly<actuator-general-actearly>`, which uses semi-implicit integration for
actuator forces: using the next step's actuator state to compute the current actuator forces.
16. Renamed ``actuatorforcerange`` and ``actuatorforcelimited``, introduced in the previous version to
17. Renamed ``actuatorforcerange`` and ``actuatorforcelimited``, introduced in the previous version to
:ref:`actuatorfrcrange<body-joint-actuatorfrcrange>` and
:ref:`actuatorfrclimited<body-joint-actuatorfrclimited>`, respectively.
17. Added the flag :ref:`eulerdamp<option-flag-eulerdamp>`, which disables implicit integration of joint damping in the
18. Added the flag :ref:`eulerdamp<option-flag-eulerdamp>`, which disables implicit integration of joint damping in the
Euler integrator. See the :ref:`Numerical Integration<geIntegration>` section for more details.
18. Added the flag :ref:`invdiscrete<option-flag-invdiscrete>`, which enables discrete-time inverse dynamics for all
19. Added the flag :ref:`invdiscrete<option-flag-invdiscrete>`, which enables discrete-time inverse dynamics for all
:ref:`integrators<option-integrator>` other than ``RK4``. See the flag documentation for more details.
19. Added :ref:`ls_iterations<option-ls_iterations>` and :ref:`ls_tolerance<option-ls_tolerance>` options for adjusting
20. Added :ref:`ls_iterations<option-ls_iterations>` and :ref:`ls_tolerance<option-ls_tolerance>` options for adjusting
linesearch stopping criteria in CG and Newton solvers. These can be useful for performance tuning.
20. Added ``mesh_pos`` and ``mesh_quat`` fields to :ref:`mjModel` to store the normalizing transformation applied to
mesh assets. Fixes `#409 <https://github.com/google-deepmind/mujoco/issues/409>`__ .
21. Added camera :ref:`resolution<body-camera-resolution>` attribute and :ref:`camprojection<sensor-camprojection>`
21. Added ``mesh_pos`` and ``mesh_quat`` fields to :ref:`mjModel` to store the normalizing transformation applied to
mesh assets. Fixes :github:issue:`409`.
22. Added camera :ref:`resolution<body-camera-resolution>` attribute and :ref:`camprojection<sensor-camprojection>`
sensor. If camera resolution is set to positive values, the camera projection sensor will report the location of a
target site, projected onto the camera image, in pixel coordinates.
22. Added :ref:`camera<body-camera>` calibration attributes:
23. Added :ref:`camera<body-camera>` calibration attributes:
- The new attributes are :ref:`resolution<body-camera-resolution>`, :ref:`focal<body-camera-focal>`,
:ref:`focalpixel<body-camera-focalpixel>`, :ref:`principal<body-camera-principal>`,
@@ -135,23 +165,21 @@ General
attributes are specified. See the following
`example model <https://github.com/deepmind/mujoco/blob/main/test/engine/testdata/vis_visualize/frustum.xml>`__.
- Note that these attributes only take effect for offline rendering and do not affect interactive visualisation.
23. Implemented reversed Z rendering for better depth precision. An enum :ref:`mjtDepthMap` was added with values
24. Implemented reversed Z rendering for better depth precision. An enum :ref:`mjtDepthMap` was added with values
``mjDEPTH_ZERONEAR`` and ``mjDEPTH_ZEROFAR``, which can be used to set the new ``readDepthMap`` attribute in
:ref:`mjrContext` to control how the depth returned by :ref:`mjr_readPixels` is mapped from ``znear`` to ``zfar``.
`Contribution <https://github.com/google-deepmind/mujoco/pull/978>`__ by
`Levi Burner <https://github.com/aftersomemath>`__.
24. Deleted the code sample ``testxml``. The functionality provided by this utility is implemented in the
Contribution :github:pull:`978` by `Levi Burner <https://github.com/aftersomemath>`__.
25. Deleted the code sample ``testxml``. The functionality provided by this utility is implemented in the
`WriteReadCompare <https://github.com/google-deepmind/mujoco/blob/main/test/xml/xml_native_writer_test.cc>`__ test.
25. Deleted the code sample ``derivative``. Functionality provided by :ref:`mjd_transitionFD`.
26. Deleted the code sample ``derivative``. Functionality provided by :ref:`mjd_transitionFD`.
Python bindings
^^^^^^^^^^^^^^^
26. Fixed `#870 <https://github.com/google-deepmind/mujoco/issues/870>`__ where calling ``update_scene`` with an invalid
camera name used the default camera.
27. Added ``user_scn`` to the :ref:`passive viewer<PyViewerPassive>` handle, which allows users to add custom
visualization geoms (`#1023 <https://github.com/google-deepmind/mujoco/issues/870>`__).
28. Added optional boolean keyword arguments ``show_left_ui`` and ``show_right_ui`` to the functions ``viewer.launch``
27. Fixed :github:issue:`870` where calling ``update_scene`` with an invalid camera name used the default camera.
28. Added ``user_scn`` to the :ref:`passive viewer<PyViewerPassive>` handle, which allows users to add custom
visualization geoms (:github:issue:`1023`).
29. Added optional boolean keyword arguments ``show_left_ui`` and ``show_right_ui`` to the functions ``viewer.launch``
and ``viewer.launch_passive``, which allow users to launch a viewer with UI panels hidden.
Simulate
@@ -161,21 +189,31 @@ Simulate
:align: right
:width: 240px
29. Added **state history** mechanism to :ref:`simulate<saSimulate>` and the managed
30. Added **state history** mechanism to :ref:`simulate<saSimulate>` and the managed
:ref:`Python viewer<PyViewerManaged>`. State history can be viewed by scrubbing the History slider and (more
precisely) with the left and right arrow keys. See screen capture:
30. The ``LOADING...`` label is now shown correctly.
`Contribution <https://github.com/google-deepmind/mujoco/pull/1070>`__ by
31. The ``LOADING...`` label is now shown correctly. Contribution :github:pull:`1070` by
`Levi Burner <https://github.com/aftersomemath>`__.
Documentation
^^^^^^^^^^^^^
.. youtube:: nljr0X79vI0
:align: right
:width: 240px
32. Added :doc:`detailed documentation <computation/fluid>` of fluid force modeling, and an illustrative example model
showing `tumbling cards <https://github.com/google-deepmind/mujoco/blob/main/model/cards/cards.xml>`__ using the
ellipsoid-based fluid model.
Bug fixes
^^^^^^^^^
31. Fixed a bug that was causing :ref:`geom margin<body-geom-margin>` to be ignored during the construction of
33. Fixed a bug that was causing :ref:`geom margin<body-geom-margin>` to be ignored during the construction of
midphase collision trees.
32. Fixed a bug that was generating incorrect values in ``efc_diagApprox`` for weld equality constraints.
34. Fixed a bug that was generating incorrect values in ``efc_diagApprox`` for weld equality constraints.
Version 2.3.7 (July 20, 2023)
@@ -204,13 +242,10 @@ Python bindings
7. The :ref:`passive viewer<PyViewerPassive>` handle now exposes ``update_hfield``, ``update_mesh``, and
``update_texture`` methods to allow users to update renderable assets.
(`#812 <https://github.com/google-deepmind/mujoco/issues/812>`_,
`#958 <https://github.com/google-deepmind/mujoco/issues/958>`_,
`#965 <https://github.com/google-deepmind/mujoco/issues/965>`_)
#. Allow a custom keyboard event callback to be specified in the :ref:`passive viewer<PyViewerPassive>`.
(`#766 <https://github.com/google-deepmind/mujoco/issues/766>`_)
#. Fix GLFW crash when Python exits while the passive viewer is running.
(`#790 <https://github.com/google-deepmind/mujoco/issues/790>`_)
(Issues :github:issue:`812`, :github:issue:`958`, :github:issue:`965`).
#. Allow a custom keyboard event callback to be specified in the :ref:`passive viewer<PyViewerPassive>`
(:github:issue:`766`).
#. Fix GLFW crash when Python exits while the passive viewer is running (:github:issue:`790`).
Models
^^^^^^
+91 -72
View File
@@ -26,36 +26,55 @@ to positive values. These parameters correspond to the density :math:`\rho` and
Inertia model
-------------
In this model, the shape of each body, for fluid dynamics purposes, is assumed to be the *equivalent inertia box*,
which can also be visualized. Each forward-facing (relative to the linear velocity) face of the box experiences force
along its normal direction. All faces also experience torque due to the angular velocity; this torque is obtained by
integrating the force resulting from the rotation over the surface area. In this sub-section, let :math:`v` and
:math:`\omega` denote the linear and angular body velocity in the body local frame (aligned with the equivalent
inertia box), and :math:`s` the 3D vector of box sizes. When the contributions from all faces are added, the resulting
force and torque applied to the body by a fluid of density :math:`\rho`, in local body coordinates, have the
:math:`i`-th component
In this model the shape of each body, for fluid dynamics purposes, is assumed to be the *equivalent inertia box*,
which can also be visualized. For a body with mass :math:`\mathcal{M}` and inertia matrix :math:`\mathcal{I}`, the
half-dimensions (i.e. half-width, half-depth and half-height) of the equivalent inertia box are
.. math::
\begin{align*}
r_x = \sqrt{\frac{3}{2 \mathcal{M}} \left(\mathcal{I}_{yy} + \mathcal{I}_{zz} - \mathcal{I}_{xx} \right)} \\
r_y = \sqrt{\frac{3}{2 \mathcal{M}} \left(\mathcal{I}_{zz} + \mathcal{I}_{xx} - \mathcal{I}_{yy} \right)} \\
r_z = \sqrt{\frac{3}{2 \mathcal{M}} \left(\mathcal{I}_{xx} + \mathcal{I}_{yy} - \mathcal{I}_{zz} \right)}
\end{align*}
Let :math:`\mathbf{v}` and :math:`\boldsymbol{\omega}` denote the linear and angular body velocity of the body in
the body-local frame (aligned with the equivalent inertia box). The force :math:`\mathbf{f}_{\text{inertia}}` and
torque :math:`\mathbf{g}_{\text{inertia}}` exerted by the fluid onto the solid are the sum of the terms
.. math::
\begin{align*}
\mathbf{f}_{\text{inertia}} &= \mathbf{f}_D + \mathbf{f}_V \\
\mathbf{g}_{\text{inertia}} &= \mathbf{g}_D + \mathbf{g}_V
\end{align*}
Here subscripts :math:`D` and :math:`V` denote quadratic Drag and Viscous resistance.
The quadratic drag terms depend on the density :math:`\rho` of the fluid, scale quadratically with the velocity
of the body, and are a valid approximation of the fluid forces at high Reynolds numbers.
The torque is obtained by integrating the force resulting from the rotation over the surface area.
The :math:`i`-th component of the force and torque can be written as
.. math::
\begin{aligned}
\text{density force} : \quad &- {1 \over 2} \rho s_j s_k |v_i| v_i \\
\text{density torque} : \quad &- {1 \over 64} \rho s_i \left(s_j^4 + s_k^4 \right) |\omega_i| \omega_i \\
f_{D, i} = \quad &- 2 \rho r_j r_k |v_i| v_i \\
g_{D, i} = \quad &- {1 \over 2} \rho r_i \left(r_j^4 + r_k^4 \right) |\omega_i| \omega_i \\
\end{aligned}
This model implicitly assumes high Reynolds numbers, with lift-to-drag ratio equal to the tangent of the angle of
attack. One can also specify a non-zero :ref:`wind<option-wind>`, which is a 3D vector subtracted from the body linear
velocity in the fluid dynamics computation.
Each body also experiences a force and a torque proportional to the viscosity :math:`\beta` and opposite to its linear and
angular velocity. Note that viscosity can be used independent of density, to make the simulation more damped. We use the
formulas for a sphere at low Reynolds numbers, with diameter :math:`d` equal to the average of the equivalent inertia
box sizes. The resulting 3D force and torque in local body coordinates are
The viscous resistance terms depend on the fluid viscosity :math:`\beta`, scale linearly with the body velocity, and
approximate the fluid forces at low Reynolds numbers. Note that viscosity can be used independent of density to make
the simulation more damped. We use the formulas for the equivalent sphere with radius
:math:`r_{eq} = (r_x + r_y + r_z) / 3` at low Reynolds numbers. The resulting 3D force and torque in local
body coordinates are
.. math::
\begin{aligned}
\text{viscosity force} : \quad &- 3 \beta \pi d v \\
\text{viscosity torque} : \quad &- \beta \pi d^3 \omega \\
f_{V, i} = \quad &- 6 \beta \pi r_{eq} v_i \\
g_{V, i} = \quad &- 8 \beta \pi r_{eq}^3 \omega_i \\
\end{aligned}
One can also affect these forces by specifing a non-zero :ref:`wind<option-wind>`, which is a 3D vector subtracted
from the body linear velocity in the fluid dynamics computation.
.. _flEllipsoid:
Ellipsoid model
@@ -116,35 +135,36 @@ also disables the inertia-based model for the parent body. The
- 1.0
Elements of the model are a generalization of :cite:t:`andersen2005b` to 3 dimensions.
The force :math:`\mathbf{f}_{\text{fluid}\rightarrow \text{solid}}` and torque
:math:`\mathbf{g}_{\text{fluid} \rightarrow \text{solid}}` exerted by the fluid onto the solid are
The force :math:`\mathbf{f}_{\text{ellipsoid}}` and torque
:math:`\mathbf{g}_{\text{ellipsoid}}` exerted by the fluid onto the solid are
the sum of of the terms
.. math::
\begin{align*}
\mathbf{f}_{\text{fluid} \rightarrow \text{solid}} &= \mathbf{f}_A + \mathbf{f}_D + \mathbf{f}_M + \mathbf{f}_K \\
\mathbf{g}_{\text{fluid} \rightarrow \text{solid}} &= \mathbf{g}_A + \mathbf{g}_D
\mathbf{f}_{\text{ellipsoid}} &= \mathbf{f}_A + \mathbf{f}_D + \mathbf{f}_M + \mathbf{f}_K + \mathbf{f}_V \\
\mathbf{g}_{\text{ellipsoid}} &= \mathbf{g}_A + \mathbf{g}_D + \mathbf{g}_V
\end{align*}
Where subscripts :math:`A`, :math:`D`, :math:`M` and :math:`K`, denote Added mass, viscous Drag, Magnus lift and
Kutta lift, respectively. The :math:`D`, :math:`M` and :math:`K` terms are scaled by the respective
:math:`C_D`, :math:`C_M` and :math:`C_K` coefficients above, while the added mass term cannot be scaled.
Where subscripts :math:`A`, :math:`D`, :math:`M`, :math:`K` and :math:`V` denote Added mass, viscous Drag, Magnus lift,
Kutta lift and Viscous resistance, respectively. The :math:`D`, :math:`M` and :math:`K` terms are scaled by the respective
:math:`C_D`, :math:`C_M` and :math:`C_K` coefficients above, the viscous resistance scales with the fluid viscosity
:math:`\beta`, while the added mass term cannot be scaled.
Notation
~~~~~~~~
We describe the motion of the object in an inviscid, incompressible quiescent fluid of density :math:`\rho`. The
arbitrarily-shaped object is described in the model as the equivalent ellipsoid of semi-axes
:math:`\mathbf{d} = \{d_x, d_y, d_z\}`.
:math:`\mathbf{r} = \{r_x, r_y, r_z\}`.
The problem is described in a reference frame aligned with the sides of the ellipsoid and moving with it. The
body has velocity :math:`\mathbf{v} = \{v_x, v_y, v_z\}` and angular velocity
:math:`\boldsymbol{\omega} = \{\omega_x, \omega_y, \omega_z\}`. We will also use
.. math::
\begin{align*}
d_\text{max} &= \max(d_x, d_y, d_z) \\
d_\text{min} &= \min(d_x, d_y, d_z) \\
d_\text{mid} &= d_x + d_y + d_z - d_\text{max} - d_\text{min}
r_\text{max} &= \max(r_x, r_y, r_z) \\
r_\text{min} &= \min(r_x, r_y, r_z) \\
r_\text{mid} &= r_x + r_y + r_z - r_\text{max} - r_\text{min}
\end{align*}
The Reynolds number is the ratio between inertial and viscous forces within a flow and is defined as :math:`Re=u~l/\beta`, where
@@ -178,12 +198,12 @@ We present the following result.
.. admonition:: Lemma
:class: note
Given an ellipsoid with semi-axes :math:`(d_x, d_y, d_z)` aligned with the coordinate axes :math:`(x, y, z)`, and a
Given an ellipsoid with semi-axes :math:`(r_x, r_y, r_z)` aligned with the coordinate axes :math:`(x, y, z)`, and a
unit vector :math:`\mathbf{u} = (u_x, u_y, u_z)`, the area projected by the ellipsoid onto the plane normal to
:math:`\mathbf{u}` is
.. math::
A^{\mathrm{proj}}_{\mathbf{u}} = \pi \sqrt{\frac{d_y^4 d_z^4 u_x^2 + d_z^4 d_x^4 u_y^2 + d_x^4 d_y^4 u_z^2}{d_y^2 d_z^2 u_x^2 + d_z^2 d_x^2 u_y^2 + d_x^2 d_y^2 u_z^2}}
A^{\mathrm{proj}}_{\mathbf{u}} = \pi \sqrt{\frac{r_y^4 r_z^4 u_x^2 + r_z^4 r_x^4 u_y^2 + r_x^4 r_y^4 u_z^2}{r_y^2 r_z^2 u_x^2 + r_z^2 r_x^2 u_y^2 + r_x^2 r_y^2 u_z^2}}
.. collapse:: Expand for derivation
@@ -202,10 +222,10 @@ We present the following result.
**Ellipsoid cross-section**
We begin by computing the area of the ellipse formed by intersecting an ellipsoid centered at the origin with the
plane :math:`\Pi_{\mathbf{n}}` through the origin with unit normal :math:`\mathbf{n} = (n_x, n_y, n_z)`. Let
:math:`(d_x, d_y, d_z)` be the semi-axis lengths of the ellipsoid. Without loss of generality, it is sufficient to
:math:`(r_x, r_y, r_z)` be the semi-axis lengths of the ellipsoid. Without loss of generality, it is sufficient to
assume that the axes of the ellipsoid are aligned with the coordinate axes. The ellipsoid can then be described as
:math:`\mathbf{x}^T Q \mathbf{x} = 1`, where
:math:`Q = \textrm{diag}\mathopen{}\left( \left. 1 \middle/ d_x^2 \right., \left. 1 \middle/ d_y^2 \right., \left. 1 \middle/ d_z^2 \right. \right)\mathclose{}`
:math:`Q = \textrm{diag}\mathopen{}\left( \left. 1 \middle/ r_x^2 \right., \left. 1 \middle/ r_y^2 \right., \left. 1 \middle/ r_z^2 \right. \right)\mathclose{}`
and :math:`\mathbf{x} = (x, y, z)` are the points on the ellipsoid.
We proceed by rotating the plane :math:`\Pi_{\mathbf{n}}` together with the ellipsoid so that the normal of the
@@ -266,9 +286,9 @@ We present the following result.
.. math::
\begin{align*}
Q'_{xx} &= \frac{1}{d_x^2} R_{xx}^2 + \frac{1}{d_y^2} R_{yx}^2 + \frac{1}{d_z^2} R_{zx}^2 , \\
Q'_{yy} &= \frac{1}{d_x^2} R_{xy}^2 + \frac{1}{d_y^2} R_{yy}^2 + \frac{1}{d_z^2} R_{zy}^2 , \\
Q'_{xy} &= \frac{1}{d_x^2} R_{xx} R_{xy} + \frac{1}{d_y^2} R_{yx} R_{yy} + \frac{1}{d_z^2} R_{zx} R_{zy} ,
Q'_{xx} &= \frac{1}{r_x^2} R_{xx}^2 + \frac{1}{r_y^2} R_{yx}^2 + \frac{1}{r_z^2} R_{zx}^2 , \\
Q'_{yy} &= \frac{1}{r_x^2} R_{xy}^2 + \frac{1}{r_y^2} R_{yy}^2 + \frac{1}{r_z^2} R_{zy}^2 , \\
Q'_{xy} &= \frac{1}{r_x^2} R_{xx} R_{xy} + \frac{1}{r_y^2} R_{yx} R_{yy} + \frac{1}{r_z^2} R_{zx} R_{zy} ,
\end{align*}
and the desired area is given by
@@ -277,7 +297,7 @@ We present the following result.
A^{\cap}_{\mathbf{n}}
= \frac{\pi}{\sqrt{\vphantom{Q'^2_{xy}} \det Q'}}
= \frac{\pi}{\sqrt{Q'_{xx} Q'_{yy} - Q'^2_{xy}}}
= \frac{\pi d_x d_y d_z}{\sqrt{d_x^2 n_x^2 + d_y^2 n_y^2 + d_z^2 n_z^2}},
= \frac{\pi r_x r_y r_z}{\sqrt{r_x^2 n_x^2 + r_y^2 n_y^2 + r_z^2 n_z^2}},
where the superscript :math:`\cap` denotes that the area pertains to the ellipse at the *intersection*
with :math:`\Pi_{\mathbf{n}}`.
@@ -293,9 +313,9 @@ We present the following result.
tangent to the ellipsoid :math:`\mathcal{E}` at every point on :math:`\mathcal{E}^{\mathrm{proj}}_{\mathbf{u}}`.
We can regard :math:`\mathcal{E}` as the image of the unit sphere :math:`\mathcal{S}` under a stretching
transformation :math:`T = \mathrm{diag}(d_x, d_y, d_z)`. Furthermore, if :math:`\mathbf{\tilde{u}}` is a vector
transformation :math:`T = \mathrm{diag}(r_x, r_y, r_z)`. Furthermore, if :math:`\mathbf{\tilde{u}}` is a vector
tangent to :math:`\mathcal{S}`, then its image
:math:`\mathbf{u}=T\mathbf{\tilde{u}}=(d_x \tilde{u}_x, d_y \tilde{u}_y, d_z \tilde{u}_z)` is tangent to the
:math:`\mathbf{u}=T\mathbf{\tilde{u}}=(r_x \tilde{u}_x, r_y \tilde{u}_y, r_z \tilde{u}_z)` is tangent to the
ellipsoid. The ellipse :math:`\mathcal{E}^{\mathrm{proj}}_{\mathbf{u}}` is therefore the image
under :math:`T` of the circle :math:`\mathcal{C}^{\cap}_{\mathbf{\tilde{u}}}` at the intersection between
:math:`\mathcal{S}` and :math:`\Pi_{\mathbf{\tilde{u}}}` (for spheres :math:`\mathcal{C}^{\cap}` and
@@ -303,15 +323,15 @@ We present the following result.
Let :math:`\mathbf{\tilde{v}}` and :math:`\mathbf{\tilde{w}}` be some orthogonal pair of vectors in the plane
:math:`\Pi_{\mathbf{\tilde{u}}}`, then :math:`\mathbf{\tilde{u}} = \mathbf{\tilde{v}} \times \mathbf{\tilde{w}}`.
Their images under :math:`T` are :math:`\mathbf{v} = (d_x \tilde{v}_x, d_y \tilde{v}_y, d_z \tilde {v}_z)` and
:math:`\mathbf{w} = (d_x \tilde{w}_x, d_y \tilde{w}_y, d_z \tilde {w}_z)` respectively, and they remain orthogonal
Their images under :math:`T` are :math:`\mathbf{v} = (r_x \tilde{v}_x, r_y \tilde{v}_y, r_z \tilde {v}_z)` and
:math:`\mathbf{w} = (r_x \tilde{w}_x, r_y \tilde{w}_y, r_z \tilde {w}_z)` respectively, and they remain orthogonal
vectors in the plane of :math:`\mathcal{E}^{\mathrm{proj}}_{\mathbf{u}}`. A (non-unit) normal to the ellipse
:math:`\mathcal{E}^{\mathrm{proj}}_{\mathbf{u}}` is therefore given by
.. math::
\mathbf{N} = \mathbf{v} \times \mathbf{w}
= (d_y d_z \tilde{u}_x, d_z d_x \tilde{u}_y, d_x d_y \tilde{u}_z)
= \left( \frac{d_y d_z}{d_x} u_x, \frac{d_z d_x}{d_y} u_y, \frac{d_x d_y}{d_z} u_z \right).
= (r_y r_z \tilde{u}_x, r_z r_x \tilde{u}_y, r_x r_y \tilde{u}_z)
= \left( \frac{r_y r_z}{r_x} u_x, \frac{r_z r_x}{r_y} u_y, \frac{r_x r_y}{r_z} u_z \right).
This shows that :math:`\mathcal{E}^{\mathrm{proj}}_{\mathbf{u}} = \mathcal{E}^{\cap}_{\mathbf{n}}`, where
:math:`\mathbf{n} = \mathbf{N} / \left\Vert\mathbf{N}\right\Vert`. Its area is given by the formula derived in the
@@ -359,11 +379,11 @@ Here :math:`\circ` denotes an element-wise product, :math:`\dot{\mathbf{v}}` is
:math:`\dot{\boldsymbol{\omega}}` is the angular acceleration. :math:`\mathbf{m}_A \circ \mathbf{v}` and
:math:`\mathbf{I}_A \circ \boldsymbol{\omega}` are the virtual linear and angular momentum respectively.
For an ellipsoid of semi-axis :math:`\mathbf{d} = \{d_x, d_y, d_z\}` and volume :math:`V = 4 \pi d_x d_y d_z / 3`, the
For an ellipsoid of semi-axis :math:`\mathbf{r} = \{r_x, r_y, r_z\}` and volume :math:`V = 4 \pi r_x r_y r_z / 3`, the
virtual inertia coefficients were derived by :cite:t:`tuckerman1925`. Let:
.. math::
\kappa_i = \int_0^\infty \frac{d_i d_j d_k}{\sqrt{(d_i^2 + \lambda)^3 (d_j^2 + \lambda) (d_k^2 + \lambda)}} \textrm{d} \lambda
\kappa_i = \int_0^\infty \frac{r_i r_j r_k}{\sqrt{(r_i^2 + \lambda)^3 (r_j^2 + \lambda) (r_k^2 + \lambda)}} \textrm{d} \lambda
It should be noted that these coefficients are non-dimensional (i.e. if all semi-axes are multiplied by the same scalar
@@ -375,7 +395,7 @@ the coefficients remain the same). The virtual masses of the ellipsoid are:
And the virtual moments of inertia are:
.. math::
I_{A, i} = \frac{\rho V}{5} \frac{(d_j^2 - d_k^2)^2 (\kappa_k-\kappa_j)}{2(d_j^2 - d_k^2) + (d_j^2 + d_k^2) (\kappa_j-\kappa_k)}
I_{A, i} = \frac{\rho V}{5} \frac{(r_j^2 - r_k^2)^2 (\kappa_k-\kappa_j)}{2(r_j^2 - r_k^2) + (r_j^2 + r_k^2) (\kappa_j-\kappa_k)}
Viscous drag
~~~~~~~~~~~~
@@ -411,7 +431,7 @@ bagheri2016`. See screen capture of the
We derive a formula for :math:`\mathbf{f}_\text{D}` based on two surfaces :math:`A^\text{proj}_\mathbf{v}` and
:math:`A_\text{max}`. The first, :math:`A^\text{proj}_\mathbf{v}`, is the cylindrical projection of the body onto a
plane normal to the velocity :math:`\mathbf{v}`. The second is the maximum projected surface
:math:`A_\text{max} = 4 \pi d_{max} d_{min}`.
:math:`A_\text{max} = 4 \pi r_{max} r_{min}`.
.. math::
\mathbf{f}_\text{D} = - \rho~ \big[ C_{D, \text{blunt}} ~ A^\text{proj}_\mathbf{v} ~ +
@@ -424,7 +444,7 @@ maximum swept ellipsoid obtained by the rotation of the body around the axis. Th
moment of inertia are:
.. math::
\mathbf{I}_{D,ii} = \frac{8\pi}{15} ~d_i ~\max(d_j, ~d_k)^4 .
\mathbf{I}_{D,ii} = \frac{8\pi}{15} ~r_i ~\max(r_j, ~r_k)^4 .
Given this reference moment of inertia, the angular drag torque is computed as:
@@ -435,22 +455,21 @@ Given this reference moment of inertia, the angular drag torque is computed as:
Here :math:`\mathbf{I}_\text{max}` is a vector with each entry equal to the maximal component of :math:`\mathbf{I}_D`.
The viscosity :math:`\beta`
For Reynolds numbers around or below :math:`O(10)`, the drag is best approximated as linear in the flow velocity
(e.g. Stokes' law). For example, for a sphere the drag force :cite:p:`stokes1850` and torque :cite:p:`lamb1932` are:
Finally the viscous resistance terms, also known as linear drag, well approvimate the fluid forces for Reynolds
numbers around or below :math:`O(10)`. These are computed for the equivalent sphere with Stokes' law
:cite:p:`stokes1850,lamb1932`:
.. math::
\begin{align*}
\mathbf{f}_\text{S} &= - 6 \pi r_D \rho ~ \beta \mathbf{v}\\
\mathbf{g}_\text{S} &= - 8 \pi r_D^3 \rho ~ \beta \boldsymbol{\omega}
\mathbf{f}_\text{V} &= - 6 \pi r_D \beta \mathbf{v}\\
\mathbf{g}_\text{V} &= - 8 \pi r_D^3 \beta \boldsymbol{\omega}
\end{align*}
Here, :math:`r_D` is the radius of the sphere and :math:`\beta` is the kinematic viscosity of the medium (e.g.
:math:`1.48~\times 10^{-5}~m^2/s` for ambient-temperature air and :math:`0.89 \times 10^{-4}~m^2/s` for water). Here,
for simplicity, we estimate the radius of the equivalent sphere as :math:`r_D = (d_x + d_y + d_z)/3`. To make a
quantitative example, Stokes' law become accurate for room-temperature air if
:math:`u\cdot l \lesssim 2 \times 10^{-4}~m^2/s`, where :math:`u` is the speed and :math:`l` a characteristic length of
the body.
Here, :math:`r_D = (r_x + r_y + r_z)/3` is the radius of the equivalent sphere and :math:`\beta` is the kinematic
viscosity of the medium (e.g. :math:`1.48~\times 10^{-5}~m^2/s` for ambient-temperature air and
:math:`0.89 \times 10^{-4}~m^2/s` for water). To make a quantitative example, Stokes' law become accurate for
room-temperature air if :math:`u\cdot l \lesssim 2 \times 10^{-4}~m^2/s`, where :math:`u` is the speed and
:math:`l` a characteristic length of the body.
Viscous lift
~~~~~~~~~~~~
@@ -487,7 +506,7 @@ It's worth making an example. To reduce the number of variables, suppose a body
sum of the force due to added mass and the force due to the Magnus effect along, for example, :math:`x` is:
.. math::
\frac{f}{\pi \rho d_z} = v_y \omega_z \left(2 d_x \min\{d_x, d_z\} - (d_x + d_z)^2\right)
\frac{f}{\pi \rho r_z} = v_y \omega_z \left(2 r_x \min\{r_x, r_z\} - (r_x + r_z)^2\right)
Note that the two terms have opposite signs.
@@ -512,13 +531,13 @@ as slender bodies or the trailing edges of airfoils.
upward force acting on the plate.
For a two-dimensional flow sketched in the figure above, the circulation due to the Kutta condition can be estimated as:
:math:`\Gamma_\text{K} = C_K ~ d_x ~ \| \mathbf{v}\| ~ \sin(2\alpha)`,
:math:`\Gamma_\text{K} = C_K ~ r_x ~ \| \mathbf{v}\| ~ \sin(2\alpha)`,
where :math:`C_K` is a lift coefficient, and :math:`\alpha` is the angle between the velocity vector and its projection
onto the surface. The lift force per unit length can be computed with the KuttaJoukowski theorem as
:math:`\mathbf{f}_K / L = \rho \Gamma_\text{K} \times \mathbf{v}`.
In order to extend the lift force equation to three-dimensional motions, we consider the normal
:math:`\mathbf{n}_{s, \mathbf{v}} = \{\frac{d_y d_z}{d_x}v_x, \frac{d_z d_x}{d_y}v_y, \frac{d_x d_x}{d_z}v_z\}`
:math:`\mathbf{n}_{s, \mathbf{v}} = \{\frac{r_y r_z}{r_x}v_x, \frac{r_z r_x}{r_y}v_y, \frac{r_x r_x}{r_z}v_z\}`
to the cross-section of the body which generates the body's projection :math:`A^\text{proj}_\mathbf{v}` onto a plane
normal to the velocity given in the :ref:`lemma<flProjection>` above and the corresponding unit vector
:math:`\hat{\mathbf{n}}_{s, \mathbf{v}}`.
@@ -539,25 +558,25 @@ Here, :math:`\hat{\mathbf{v}}` is the unit-normal along :math:`\mathbf{v}`. Note
example, for spherical bodies :math:`\hat{\mathbf{n}}_{s, \mathbf{v}} \equiv \hat{\mathbf{v}}` and by construction
:math:`\mathbf{f}_\text{K} = 0`.
Let's unpack the relation with an example. Suppose a body with :math:`d_x = d_y` and :math:`d_z \ll d_x`. Note that the vector
Let's unpack the relation with an example. Suppose a body with :math:`r_x = r_y` and :math:`r_z \ll r_x`. Note that the vector
:math:`\hat{\mathbf{n}}_{s, \mathbf{v}} \times \hat{\mathbf{v}}` gives the direction of the circulation induced by the
deflection of the flow by the solid body. Along :math:`z`, the circulation will be proportional to :math:`\frac{d_y d_z}{d_x}v_x v_y
- \frac{d_z d_x}{d_y}v_x v_y = 0` (due to :math:`d_x = d_y`). Therefore, on the plane where the solid is blunt, the motion
deflection of the flow by the solid body. Along :math:`z`, the circulation will be proportional to :math:`\frac{r_y r_z}{r_x}v_x v_y
- \frac{r_z r_x}{r_y}v_x v_y = 0` (due to :math:`r_x = r_y`). Therefore, on the plane where the solid is blunt, the motion
produces no circulation.
Now, for simplicity, let :math:`v_x = 0`. In this case also the circulation along :math:`y`, proportional
to :math:`\frac{d_y d_z}{d_x}v_x v_z - \frac{d_y d_x}{d_y}v_x v_z`, is zero. The only non-zero component of the circulation
will be along :math:`x` and be proportional to :math:`\left(\frac{d_x d_z}{d_y} - \frac{d_x d_y}{d_z}\right) v_y v_z \approx
\frac{d_x^2}{d_z} v_y v_z`.
to :math:`\frac{r_y r_z}{r_x}v_x v_z - \frac{r_y r_x}{r_y}v_x v_z`, is zero. The only non-zero component of the circulation
will be along :math:`x` and be proportional to :math:`\left(\frac{r_x r_z}{r_y} - \frac{r_x r_y}{r_z}\right) v_y v_z \approx
\frac{r_x^2}{r_z} v_y v_z`.
We would have :math:`\mathbf{v}_\parallel = \{v_x, 0, v_z\}` and
:math:`\Gamma \propto \{d_z v_y v_z, ~ 0,~ - d_x v_x v_y \} / \|\mathbf{v}\|`.
:math:`\Gamma \propto \{r_z v_y v_z, ~ 0,~ - r_x v_x v_y \} / \|\mathbf{v}\|`.
The motion produces no circulation on the plane where the solid is blunt, and on the other two planes
the circulation is
:math:`\Gamma \propto r_\Gamma ~ \|\mathbf{v}\|~ \sin(2 \alpha) ~ = ~2 r_\Gamma ~\|\mathbf{v}\| ~\sin(\alpha)~\cos(\alpha)`
with :math:`\alpha` the angle between the velocity and its projection on the body on the plane (e.g. on the plane
orthogonal to :math:`x` we have :math:`\sin(\alpha) = v_y/\|\mathbf{v}\|` and
:math:`\cos(\alpha) = v_z/\|\mathbf{v}\|`), and :math:`r_\Gamma`, the lift surface on the plane (e.g. :math:`d_z` for
:math:`\cos(\alpha) = v_z/\|\mathbf{v}\|`), and :math:`r_\Gamma`, the lift surface on the plane (e.g. :math:`r_z` for
the plane orthogonal to :math:`x`). Furthermore, the direction of the circulation is given by the cross product (because
the solid boundary "rotates" the incoming flow velocity towards its projection on the body).
+6
View File
@@ -49,9 +49,15 @@ extensions = [
'sphinx_favicon',
'sphinx_reredirects',
'sphinx_toolbox.collapse',
'sphinx_toolbox.github',
'sphinx_toolbox.sidebar_links',
'mujoco_include',
]
# GitHub-related options
github_username = 'google-deepmind'
github_repository = 'mujoco'
# Bibtex references for sphinxcontrib.bibtex
bibtex_bibfiles = ['references.bib']
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

+5
View File
@@ -14,6 +14,11 @@
programming/index.rst
APIreference/index.rst
python
MJX <mjx>
unity
models
changelog
.. sidebar-links::
:github:
+312
View File
@@ -0,0 +1,312 @@
==========
MuJoCo XLA
==========
Starting with version 3.0.0, MuJoCo includes MuJoCo XLA (MJX) under the
`mjx <https://github.com/google-deepmind/mujoco/tree/main/mjx>`__ directory. MJX allows MuJoCo to run on compute
hardware supported by the `XLA <https://www.tensorflow.org/xla>`__ compiler via the
`JAX <https://github.com/google/jax#readme>`__ framework. MJX runs on a
`all platforms supported by JAX <https://jax.readthedocs.io/en/latest/installation.html#supported-platforms>`__: Nvidia
and AMD GPUs, Apple Silicon, and `Google Cloud TPUs <https://cloud.google.com/tpu>`__.
The MJX API is consistent with the main simulation functions in the MuJoCo API, although it is currently missing some
features. While the :ref:`API documentation <Mainsimulation>` is applicable to both libraries, we indicate features
unsupported by MJX in the :ref:`notes <MjxFeatureParity>` below.
MJX is distributed as a separate package called ``mujoco-mjx`` on `PyPI <https://pypi.org/project/mujoco-mjx>`__.
Although it depends on the main ``mujoco`` package for model compilation and visualization, it is a re-implementation of
MuJoCo that uses the same algorithms as the MuJoCo implementation. However, in order to properly leverage JAX, MJX
deliberately diverges from the MuJoCo API in a few places, see below.
MJX is a successor to the `generalized physics pipeline <https://github.com/google/brax/tree/main/brax/generalized>`__
in Google's `Brax <https://github.com/google/brax>`__ physics and reinforcement learning library. MJX was built
by core contributors to both MuJoCo and Brax, who will together continue to support both Brax (for its reinforcement
learning algorithms and included environments) and MJX (for its physics algorithms). A future version of Brax will
depend on the ``mujoco-mjx`` package, and Brax's existing
`generalized pipeline <https://github.com/google/brax/tree/main/brax/generalized>`__ will be deprecated. This change
will be largely transparent to users of Brax.
.. _MjxNotebook:
Tutorial notebook
=================
The following IPython notebook demonstrates the use of MJX along with reinforcement learning to train humanoid and
quadruped robots to locomote: |colab|.
.. |colab| image:: https://colab.research.google.com/assets/colab-badge.svg
:target: https://colab.research.google.com/github/google-deepmind/mujoco/blob/main/mjx/tutorial.ipynb
.. _MjxInstallation:
Installation
============
The recommended way to install this package is via `PyPI <https://pypi.org/project/mujoco-mjx/>`__:
.. code-block:: shell
pip install mujoco-mjx
A copy of the MuJoCo library is provided as part of this package's depdendencies and does **not** need to be downloaded
or installed separately.
.. _MjxUsage:
Basic usage
===========
Once installed, the package can be imported via ``from mujoco import mjx``. Structs, functions, and enums are available
directly from the top-level ``mjx`` module.
.. _MjxStructs:
Structs
-------
Before running MJX functions on an accelerator device, structs must be copied onto the device via the ``mjx.device_put``
function. Placing an :ref:`mjModel` on device yields an ``mjx.Model``. Placing an :ref:`mjData` on device yields
an ``mjx.Data``:
.. code-block:: python
model = mujoco.MjModel.from_xml_string("...")
data = mujoco.MjData(model)
mjx_model = mjx.device_put(model)
mjx_data = mjx.device_put(data)
These MJX variants mirror their MuJoCo counterparts but have three key differences:
#. Fields in ``mjx.Model`` and ``mjx.Data`` are JAX arrays copied onto device, instead of numpy arrays.
#. Some fields are missing from ``mjx.Model`` and ``mjx.Data`` for features that are
:ref:`unsupported <mjxFeatureParity>` in MJX.
#. Arrays in ``mjx.Model`` and ``mjx.Data`` support adding batch dimensions. Batch dimensions are a natural way to
express domain randomization (in the case of ``mjx.Model``) or high-throughput simulation for reinforcement learning
(in the case of ``mjx.Data``).
Neither ``mjx.Model`` nor ``mjx.Data`` are meant to be constructed manually. An ``mjx.Data`` may be created by calling
``mjx.make_data``, which mirrors the :ref:`mj_makeData` function in MuJoCo:
.. code-block:: python
model = mujoco.MjModel.from_xml_string("...")
mjx_model = mjx.device_put(model)
mjx_data = mjx.make_data(model)
Using ``mx.make_data`` may be preferable when constructing batched ``mjx.Data`` structures inside of a ``vmap``.
.. _MjxFunctions:
Functions
---------
MuJoCo functions are exposed as MJX functions of the same name, but following
`PEP 8 <https://peps.python.org/pep-0008/>`__-compliant names. Most of the :ref:`main simulation <Mainsimulation>` and
some of the :ref:`sub-components <Subcomponents>` for forward simulation are available from the top-level ``mjx`` module.
MJX functions are not `JIT compiled <https://jax.readthedocs.io/en/latest/jax-101/02-jitting.html>`__ by default -- we
leave it to the user to JIT MJX functions, or JIT their own functions that reference MJX functions. See the
:ref:`minimal example <MjxExample>` below.
.. _MjxEnums:
Enums and constants
-------------------
MJX enums are available as ``mjx.EnumType.ENUM_VALUE``, for example ``mjx.JointType.FREE``. Enums for unsupported MJX
features are omitted from the MJX enum declaration. MJX declares no constants but references MuJoCo constants directly.
.. _MjxExample:
Minimal example
---------------
.. code-block:: python
# Throw a ball at 100 different velocities.
import jax
import mujoco
from mujoco import mjx
XML=r"""
<mujoco>
<worldbody>
<body>
<freejoint/>
<geom size=".15" mass="1" type="sphere"/>
</body>
</worldbody>
</mujoco>
"""
model = mujoco.MjModel.from_xml_string(XML)
mjx_model = mjx.device_put(model)
@jax.vmap
def batched_step(vel):
mjx_data = mjx.make_data(mjx_model)
qvel = mjx_data.qvel.at[0].set(vel)
mjx_data = mjx_data.replace(qvel=qvel)
pos = mjx.step(mjx_model, mjx_data).qpos[0]
return pos
vel = jax.numpy.arange(0.0, 1.0, 0.01)
pos = jax.jit(batched_step)(vel)
print(pos)
.. _MjxFeatureParity:
Feature Parity
==============
MJX supports most of the main simulation features of MuJoCo, with a few exceptions. MJX will raise an exception if
asked to copy to device an :ref:`mjModel` with field values referencing unsupported features.
The following features are **fully supported** in MJX:
.. list-table::
:width: 90%
:align: left
:widths: 1 5
:header-rows: 1
* - Category
- Feature
* - Dynamics
- :ref:`Forward <mj_forward>`
* - :ref:`Joint <mjtJoint>`
- ``FREE``, ``BALL``, ``SLIDE``, ``HINGE``
* - :ref:`Transmission <mjtTrn>`
- ``TRN_JOINT``
* - :ref:`Actuation <geactuation>`
- ``DYN_NONE``, ``DYN_INTEGRATOR``, ``DYN_FILTER``, ``GAIN_FIXED``, ``GAIN_AFFINE``, ``BIAS_NONE``,
``BIAS_AFFINE``
* - :ref:`Geom <mjtGeom>`
- ``PLANE``, ``SPHERE``, ``CAPSULE``, ``BOX``, ``MESH``
* - :ref:`Constraint <mjtConstraint>`
- ``EQUALITY``, ``FRICTION_DOF``, ``LIMIT_JOINT``, ``CONTACT_PYRAMIDAL``
* - :ref:`Integrator <mjtIntegrator>`
- ``EULER``, ``RK4``
* - :ref:`Cone <mjtCone>`
- ``PYRAMIDAL``
* - :ref:`Condim <coContact>`
- 3
* - :ref:`Solver <mjtSolver>`
- ``CG``
* - Fluid Model
- :ref:`flInertia`
The following features are **in development** and coming soon:
.. list-table::
:width: 90%
:align: left
:widths: 1 5
:header-rows: 1
* - Category
- Feature
* - Dynamics
- :ref:`Inverse <mj_inverse>`
* - :ref:`Transmission <mjtTrn>`
- ``TRN_TENDON``
* - :ref:`Geom <mjtGeom>`
- ``HFIELD``, ``ELLIPSOID``, ``CYLINDER``, ``SDF``
* - :ref:`Integrator <mjtIntegrator>`
- ``IMPLICIT``, ``IMPLICITFAST``
* - :ref:`Cone <mjtCone>`
- ``ELLIPTIC``
* - :ref:`Condim <coContact>`
- 1, 4, 6
* - :ref:`Solver <mjtSolver>`
- ``NEWTON``
* - Fluid Model
- :ref:`flEllipsoid`
* - :ref:`Tendons <tendon>`
- :ref:`Spatial <tendon-spatial>`, :ref:`Fixed <tendon-fixed>`
The following features are **unsupported**:
.. list-table::
:width: 90%
:align: left
:widths: 1 5
:header-rows: 1
* - Category
- Feature
* - :ref:`Transmission <mjtTrn>`
- ``TRN_JOINTINPARENT``, ``TRN_SLIDERCRANK``, ``TRN_SITE``, ``TRN_BODY``, ``MUSCLE``
* - :ref:`Solver <mjtSolver>`
- ``PGS``
* - :ref:`Callbacks <glphysics>`
- ``mjDYN_USER``, ``mjGAIN_USER``, ``mjBIAS_USER``, ``mjSENS_USER``
.. _MjxSharpBits:
🔪 MJX - The Sharp Bits 🔪
==========================
GPUs and TPUs have unique performance tradeoffs that MJX is subject to. MJX specializes in simulating big batches of
parallel identical physics scenes using algorithms that can be efficiently vectorized on
`SIMD hardware <https://en.wikipedia.org/wiki/Single_instruction,_multiple_data>`__. This specialization is useful
for machine learning workloads such as `reinforcement learning <https://en.wikipedia.org/wiki/Reinforcement_learning>`__
that require massive data throughput.
There are certain workflows that MJX is ill-suited for:
Single scene simulation
Simulating a single scene (1 instance of :ref:`mjData`), MJX can be **10x** slower than MuJoCo, which has been
carefully optimized for CPU. MJX works best when simulating thousands or tens of thousands of scenes in parallel.
Large, complex scenes with many contacts
Accelerators exhibit poor performance for
`branching code <https://aschrein.github.io/jekyll/update/2019/06/13/whatsup-with-my-branches-on-gpu.html#tldr>`__.
Branching is used in broad-phase collision detection, when identifying potential collisions between large numbers of
bodies in a scene. MJX ships with a simple branchless broad-phase algorithm (see performance tuning) but it is not as
powerful as the one in MuJoCo.
To see how this affects simulation, let us consider a physics scene with increasing numbers of physics bodies. We
simulate a scene with a variable number of humanoids (from 1 to 10) and then compare MJX's performance on an Nvidia
A100 GPU to MuJoCo on a 12-core workstation:
.. figure:: images/mjx/mujoco_vs_mjx_large_scene.png
:width: 658px
:align: center
Notice that as we increase the number of humanoids (which increases the number of potential contacts in a scene), MJX
performance degrades more rapidly than MuJoCo. At the limit, for such a large scene, MuJoCo performance nearly
matches MJX.
Scenes with collisions between meshes with many vertices
MJX supports mesh geometries and can determine if two meshes are colliding using branchless versions of
`mesh collision algorithms <https://ubm-twvideo01.s3.amazonaws.com/o1/vault/gdc2013/slides/822403Gregorius_Dirk_TheSeparatingAxisTest.pdf>`__.
These algorithms work well for smaller meshes (with hundreds of vertices) but suffer with large meshes. With careful
tuning, MJX can simulate scenes with mesh collisions well -- see the MJX
`shadow hand <https://github.com/google-deepmind/mujoco/tree/main/mjx/mujoco/mjx/benchmark/model/shadow_hand>`__
config for an example.
.. _MjxPerformance:
Performance tuning
==================
For MJX to perform well, some configuration parameters should be adjusted from their default MuJoCo values:
:ref:`option` element
For now, solver must be set to ``CG`` (but Newton is on its way!). The ``iterations`` and ``ls_iterations``
attributes---which control solver and linesearch iterations, respectively---should be brought down to just low enough
that the simulation remains stable. Accurate solver forces are not so important in reinforcement learning in which
domain randomization is often used to add noise to physics for sim2real.
:ref:`contact-pair` element
Consider explicitly marking geoms for collision detection to reduce the number of contacts that MJX must consider
during each step. Enabling only an explicit list of valid contacts can have a dramatic effect on simulation
performance in MJX. Doing this well often requires an understanding of the task -- for example, the
`OpenAI Gym Humanoid <https://github.com/openai/gym/blob/master/gym/envs/mujoco/humanoid_v4.py>`__ task resets when
the humanoid starts to fall, so full contact with the floor is not needed.
:ref:`option-flag` element
Disabling ``eulerdamp`` can help performance and is often not needed for stability.
+141 -45
View File
@@ -1069,6 +1069,14 @@ has 1000 bodies (each with a geom), 3000 degrees of freedom and around 1000 acti
takes around 1 ms on a single core of a modern processor. As with most other MuJoCo models, the soft constraints allow
simulation at much larger timesteps (this model is stable at 30 ms timestep and even higher).
Particles are also compatible with the passive forces 2D and 3D plugins, discussed in the :ref:`deformable
<CDeformable>` section. However, collisions are limited to the particle themselves and not to the whole boundary of the
skin that encloses them. This makes contacts very fast but does not guarantee that all penetrations can be avoided. For
a more complete treatment, see again the :ref:`deformable <CDeformable>` section, which outlines how to use
:ref:`flexcomp<body-flexcomp>` to create such an object. It is easy to port models create with composite particles to
flex, see the folder `elasticity/ <https://github.com/google-deepmind/mujoco/tree/main/model/plugin/elasticity>`__ for
several examples.
**1D grid**.
|image6| |image7|
@@ -1111,59 +1119,51 @@ coordinates. The plot on the right shows a cloth pinned to the world body at the
capsule probe. The skin on the right is subdivided using bi-cubic interpolation, which increases visual quality in the
absence of textures. When textures are present (left) the benefits of subdivision are less visible.
**Rope and loop**.
**Cable**.
|image10| |image11|
|coil|
.. code-block:: xml
<body name="B10" pos="0 0 1">
<freejoint/>
<composite type="rope" count="21 1 1" spacing="0.04" offset="0 0 2">
<joint kind="main" damping="0.005"/>
<geom type="capsule" size=".01 .015" rgba=".8 .2 .1 1"/>
</composite>
</body>
<extension>
<plugin plugin="mujoco.elasticity.cable"/>
</extension>
The remaining composite object types create kinematic trees of element bodies, and the parent body becomes the root of
the tree. This is why :el:`composite` appears inside a moving body, and not inside the world body as in particle and
grid objects. If it appeared inside the world body, the root of the composite object would not move. Unlike grids and
particles, the orientation of the element bodies here can change. The kinematic tree is constructed using (mostly)
hinge joints. In the case of rope and loop objects illustrated here, the tree is a chain. Note the naming of the
parent body. This name must correspond to one of the automatically-generated names of the element bodies. This
mechanism is used to specify where the composite object should attach to the parent. Compared to 1D grids, the rope
and loop are less jittery and can use capsule and ellipsoid geoms in addition to spheres (thus filling the gaps for
collision detection). However this comes at a price. Because we have long kinematic chains, the resulting differential
equations become stiff and can no longer be integrated at large timesteps. The examples we provide illustrate
comfortable timesteps where the models are stable. The rope can be easily tied into a knot using mouse perturbations,
as shown in the left plot. Using a larger number of smaller elements makes knots and other manipulations even easier.
The loop is similar to a rope but the first and last element bodies are connected with an equality constraint.
<worldbody>
<composite prefix="actuated" type="cable" curve="cos(s) sin(s) s" count="41 1 1"
size="0.25 .1 4" offset="0.25 0 .05" initial="none">
<plugin plugin="mujoco.elasticity.cable">
<!--Units are in Pa (SI)-->
<config key="twist" value="5e8"/>
<config key="bend" value="15e8"/>
<config key="vmax" value="0"/>
</plugin>
<joint kind="main" damping="0.15" armature="0.01"/>
<geom type="capsule" size=".005" rgba=".8 .2 .1 1"/>
</composite>
</worldbody>
The cable simulates an inextensible elastic 1D object having twist and bending stiffness. It is discretized using a
sequence of capsules or boxes. Its stiffness and inertia properties are computed directly from the given parameters and
the shape of the cross section, which allows for anisotropic behaviors, which can be found in e.g. belts or computer
cables. It is a single kinematic tree, so it is exactly inextensible without the use of additional constraints, enabling
the use of large time steps. The elastic model is geometrically exact and based on computing the Bishop or twist-free
frame of the centerline, i.e., the line passing through the center of the cross section. The orientations of the geoms
are expressed with respect to this frame and then decomposed into twist and bending components, hence different
stiffnesses can be set independently. Moreover, it is possible to specify if the stress-free configuration is flat or
curve, such as in the case of coil springs. The cable requires using a first-party :ref:`engine plugin<exPlugin>`, which
may be integrated directly into the engine in the future.
**Rope and loop**.
The rope and loop are deprecated. It is recommended to use the cable for simulating inextensible elastic rods that are
bent and twisted and 1D flex :ref:`deformable objects <CDeformable>` for extensible strings in a tensile loading
scenario (e.g. a stretched rubber band).
**Cloth**.
|image12| |image13|
.. code-block:: xml
<body name="B3_5" pos="0 0 1">
<freejoint/>
<composite type="cloth" count="9 9 1" spacing="0.05" flatinertia="0.01">
<joint kind="main" damping="0.001"/>
<skin material="matcarpet" texcoord="true" inflate="0.005" subgrid="2"/>
<geom type="capsule" size="0.015 0.01" rgba=".8 .2 .1 1"/>
</composite>
</body>
The cloth type is an alternative to a 2D grid, and has somewhat different properties. Similar to rope vs. 1D grid, the
cloth is less jittery than a 2D grid and can also fill collision holes better. This is done by using capsules or
ellipsoids, and arranging them in the pattern shown on the right. The geom capsules are shown in red, the kinematic
tree in thick blue, the equality-constrained tendons holding the cloth together in thin gray, and the joints in cyan.
The element body corresponding to the parent body has a floating joint rendered as a cube, while the rest of the tree
is constructed using pairs of hinge joints that form universal joints. Note the naming of the parent body: similar to
rope, it must coincide with one of the automatically-generated element body names in the composite object. Explicit
pinning is not possible. However if the parent is a static body, the cloth is essentially pinned but only at one
point. Similar to rope, the cloth object involves long kinematic chains that require relatively small timesteps and
some damping for stable integration. The parameters can be found in the XML model files in the software distribution.
The cloth is deprecated. It is recommended to use 2D flex :ref:`deformable objects <CDeformable>` for simulating thin
elastic structures.
**Box**.
@@ -1222,6 +1222,94 @@ of the system making it softer or harder, damped or springy, etc. Note that box,
involve long kinematic chains, and can be simulated at large timesteps - similar to particle and grid, and unlike rope
and cloth.
.. _CDeformable:
Deformable objects
~~~~~~~~~~~~~~~~~~
The :ref:`composite objects <CComposite>` described earlier were intended to emulate soft bodies in what is effectively
a rigid-body simulator. This was possible because MuJoCo constraints are soft, but nevertheless it was limited in
functionality and modeling power. In MuJoCo 3.0 we have introduced true deformable objects involving new model elements.
The :ref:`skin<deformable-skin>` described earlier was actually one such element, but it is merely used for
visualization. We now have a related element :ref:`flex<deformable-flex>` which generates contact forces, constraint
forces and passive forces as needed to model a wide range of deformable entities. Both skins and flexes are now defined
within a new grouping element in the XML called :ref:`deformable<deformable>`. A flex is a low-level element that
specifies everything needed at runtime, but is difficult to design at modeling time. To aid with modeling, we have
further introduced the element :ref:`flexcomp<body-flexcomp>` which automates the creation of the low-level flex,
similar to how :ref:`composite<body-composite>` automates the creation of (collections of) MuJoCo objects needed to
emulate a soft body. Flexes may eventually supersede composites, but for now both are useful for somewhat different
purposes.
A flex is a collection of MuJoCo bodies that are connected with massless stretchable elements. These elements can be
capsules (1D flex), triangles (2D flex), or tetrahedra (3D flex). In all cases we allow a radius, which makes the
elements smooth and also volumetric in 1D and 2D. The primitive elements are illustrated below:
|flexelem|
Thus far these look like geoms. But the key difference is that they deform: as the bodies (vertices) move independently
of each other, the shape of the elements changes in real time. Collisions and contact forces are now generalized to
handle these deformable geometric elements. Note that when two such elements collide, the contact no longer involves
just two bodies, but can involve up to 8 bodies (if both elements are tetrahedra). Contact forces are computed as
before, given the contact frame and relevant quantities expressed in that frame. But then the contact force is
distributed among all interacting bodies. The notion of contact Jacobian is complicated because the contact point cannot
be considered fixed in any body frame. Instead we use a weighting scheme to "assign" each contact point to multiple
bodies. It is also possible to create a rigid flex, by assigning all vertices to the same body. This is a way to
re-purpose the new flex collision machinery to implement rigid non-convex mesh collisions (unlike mesh geoms which are
convexified for collision purposes).
**Deformation model**.
In order to preserve the shape of the flex (in a soft sense), we need to generate passive or constraint forces. Prior to
MuJoCo 3.0 this would involve a large number of tendons plus constraints on tendons and joints. This is still possible
here, but inefficient both in terms of modeling and in terms of simulation when the flex is large. Instead, the design
philosophy is to use a single set of parameters and provide two modeling choices: a new (soft) equality constraint type
that applies to all edges of a given flex, which permits large time steps, or a discretized continuum representation,
where each element is in a constant stress state, which is equivalent to piecewise linear finite elements and achieves
improved realism and accuracy. The edge-based model could be seen as a "lumped" stiffness model, where the correct
coupling of deformation modes (e.g. shear and volumetric) is averaged in a single quantity. The continuum model enables
instead to specify shear and volumetic stiffnesses separately using the `Poisson's ratio
<https://en.wikipedia.org/wiki/Poisson%27s_ratio>`__ of the material. For more details, see the `Saint Venant-Kirchhoff
<https://en.wikipedia.org/wiki/Hyperelastic_material#Saint_Venant%E2%80%93Kirchhoff_model>`__ hyperelastic model. This
functionality is currently based on first-party :ref:`engine plugins<exPlugin>` as of MuJoCo 3.0 but may be integrated
into the engine in future releases.
**Creation and visualization**.
.. code-block:: xml
<extension>
<plugin plugin="mujoco.elasticity.solid"/>
</extension>
<worldbody>
<flexcomp type="grid" count="24 4 4" spacing=".1 .1 .1" pos=".1 0 1.5"
radius=".0" rgba="0 .7 .7 1" name="softbody" dim="3" mass="7">
<contact condim="3" solref="0.01 1" solimp=".95 .99 .0001" selfcollide="none"/>
<edge damping="1"/>
<plugin plugin="mujoco.elasticity.solid">
<config key="poisson" value="0.2"/>
<!--Units are in Pa (SI)-->
<config key="young" value="5e4"/>
</plugin>
</flexcomp>
</worldbody>
Using the :ref:`flexcomp<body-flexcomp>` element, we can create flexes from meshes, including tetrahedral meshes, and
automatically generate all the bodies/vertices and connect them with suitable elements. We can also create grids and
other topologies automatically. This machinery makes it easy to create very large flexes, involving thousands or even
tens of thousands of bodies, elements and edges. Obviously such simulations will not be fast. Even for medium-sized
flexes, pruning of collision pairs and essential. This is why we have developed elaborate methods for pruning
self-collisions; see XML reference.
In case of 3D flexes made of tetrahedra, it may be useful to examine how the flex is "triangulated" internally. We have
a special visualization mode that peels off the outer layers. Below is an example with the Stanford Bunny. Note how it
has smaller tetrahedra on the outside and larger ones on the inside. This mesh design makes sense, because we want the
collision surface to be accurate, but on the inside we just need soft material properties - which require less spatial
resolution.
|bunny1| |bunny2|
.. _CInclude:
Including files
@@ -1562,3 +1650,11 @@ in a visible way, and the energy fluctuates around the initial value instead of
:height: 250px
.. |particle| image:: images/models/particle.gif
:width: 270px
.. |flexelem| image:: images/modeling/flexelem.png
:width: 400px
.. |bunny1| image:: images/modeling/bunny1.png
:width: 300px
.. |bunny2| image:: images/modeling/bunny2.png
:width: 300px
.. |coil| image:: images/modeling/coil.png
:width: 300px
+18 -3
View File
@@ -585,8 +585,21 @@ Equality constraints can impose additional constraints beyond those already impo
and the joints/DOFs defined in it. They can be used to create loop joints, or in general model mechanical coupling.
The internal forces that enforce these constraints are computed together with all other constraint forces. The
available equality constraint types are: connect two bodies at a point (creating a ball joint outside the kinematic
tree); weld two bodies together; make two surfaces slide on each other; fix the position of a joint or tendon; couple
the positions of two joints or two tendons via a cubic polynomial.
tree); weld two bodies together; fix the position of a joint or tendon; couple the positions of two joints or two
tendons via a cubic polynomial; constrain the edges of a flex (i.e. deformable mesh) to their initial lengths.
Flex
^^^^
Flexes were added in MuJoCo 3.0. They represent deformable meshes that can be 1, 2 or 3 dimensional (thus their elements
are capsules, triangles or tetrahedra). Unlike geoms which are static shapes attached rigidly to a single body, the
elements of a flex are deformable: they are constructed by connecting multiple bodies, thus the body positions and
orientations determine the shape of the flex elements at runtime. These deformable elements suport collisions and
contact forces, as well as generate passive and constraint forces which softly preserve the shape of the deformable
entity. Automation is provided to load a mesh from a file, construct bodies corresponding to the mesh vertices,
construct flex elements corresponding to the mesh faces (or lines or tetrahedra, depending on dimensionality), and
obtain a corresponding deformable mesh.
Contact pair
^^^^^^^^^^^^
@@ -596,7 +609,9 @@ sources: automated proximity tests and other filters collectively called "dynami
geom pairs provided in the model. The latter is a separate type of model element. Because a contact involves a
combination of two geoms, the explicit specification allows the user to define contact parameters in ways that cannot
be done with the dynamic mechanism. It is also useful for fine-tuning the contact model, in particular adding contact
pairs that were removed by an aggressive filtering scheme.
pairs that were removed by an aggressive filtering scheme. The contact machinery is now extended to flex elements,
which can create contact interactions between more than two bodies. However such collisions are automated and cannot
be finetuned using contact pairs.
Contact exclude
^^^^^^^^^^^^^^^
+1
View File
@@ -256,6 +256,7 @@ Currently, there are three directories of first-party plugins:
bending strains. The 3D solid is a
`Saint Venant-Kirchhoff <https://en.wikipedia.org/wiki/Hyperelastic_material#Saint_Venant%E2%80%93Kirchhoff_model>`__
model discretized with piecewise linear finite elements, which is suitable for large deformations with small strains.
See also :ref:`composite <CComposite>` and :ref:`deformable <CDeformable>` objects.
* **sensor:** The plugins in the `sensor/ <https://github.com/google-deepmind/mujoco/tree/main/plugin/sensor>`__
directory implement custom sensors. Currently the sole sensor plugin is the touch grid sensor, see the
`README <https://github.com/google-deepmind/mujoco/blob/main/plugin/sensor/README.md>`__ for details.
+2 -2
View File
@@ -30,14 +30,14 @@ _____
The MuJoCo app needs to be run at least once before the native library can be used, in order to register the library as
a trusted binary. Then, copy the dynamic library file from
``/Applications/MuJoCo.app/Contents/Frameworks/mujoco.framework/Versions/Current/libmujoco.2.3.8.dylib`` (it can be
``/Applications/MuJoCo.app/Contents/Frameworks/mujoco.framework/Versions/Current/libmujoco.3.0.1.dylib`` (it can be
found by browsing the contents of ``MuJoCo.app``) and rename it as ``mujoco.dylib``.
Linux
_____
Expand the ``tar.gz`` archive to ``~/.mujoco``. Then copy the dynamic library from
``~/.mujoco/mujoco-2.3.8/lib/libmujoco.so.2.3.8`` and rename it as ``libmujoco.so``.
``~/.mujoco/mujoco-3.0.1/lib/libmujoco.so.3.0.1`` and rename it as ``libmujoco.so``.
Windows
_______
+1 -1
View File
@@ -24,7 +24,7 @@ extern "C" {
#endif
// header version; should match the library version as returned by mj_version()
#define mjVERSION_HEADER 238
#define mjVERSION_HEADER 301
// needed to define size_t, fabs and log10
#include <stdlib.h>
+2
View File
@@ -0,0 +1,2 @@
recursive-include mujoco/mjx/test_data *
recursive-include mujoco/mjx/benchmark *.obj *.stl *.xml
+54
View File
@@ -0,0 +1,54 @@
# MuJoCo XLA (MJX)
[![PyPI Python Version][pypi-versions-badge]][pypi]
[![PyPI version][pypi-badge]][pypi]
[pypi-versions-badge]: https://img.shields.io/pypi/pyversions/mujoco-mjx
[pypi-badge]: https://badge.fury.io/py/mujoco-mjx.svg
[pypi]: https://pypi.org/project/mujoco-mjx/
This package is a re-implementation of the
[MuJoCo physics engine](https://github.com/google-deepmind/mujoco) in
[JAX](https://github.com/google/jax). This library is developed and maintained
by Google DeepMind, and is kept up-to-date with the latest developments in
MuJoCo itself.
The `mujoco-mjx` package is API-compatible with MuJoCo, but is missing some
features found in MuJoCo. See our
[documentation](https://mujoco.readthedocs.io/en/stable/mjx.html) for more
details concerning feature parity.
## Installation
The recommended way to install this package is via [PyPI](https://pypi.org/project/mujoco-mjx/):
```sh
pip install mujoco-mjx
```
## Usage
Once installed, the package can be imported via `from mujoco import mjx`. Please
consult our [documentation](https://mujoco.readthedocs.io/en/stable/mjx.html)
for further detail on the package's API.
We recommend going through the tutorial notebook which introduces the MJX API
and trains a reinforcement learning policy in a few minutes: [![Open In
Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/google-deepmind/mujoco/blob/main/mjx/tutorial.ipynb)
## Versioning
The `major.minor.micro` portion of the version number matches the version of
MuJoCo that this library provides. Optionally, if we release updates to MJX that
target the same version of MuJoCo, a `.postN` suffix is added, for example
`3.0.1.post2` represents the second update to MJX for MuJoCo 3.0.1.
## License and Disclaimer
Copyright 2023 DeepMind Technologies Limited
MuJoCo and its libraries are licensed under the Apache License,
Version 2.0. You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0.
This is not an officially supported Google product.
+17
View File
@@ -0,0 +1,17 @@
-f https://storage.googleapis.com/jax-releases/jax_cuda_releases.html
jax[cuda12_local]==0.4.13; python_version == '3.8' \
--hash=sha256:03bfe6749dfe647f16f15f6616638adae6c4a7ca7167c75c21961ecfd3a3baaa
jax[cuda12_local]==0.4.18; python_version >= '3.9' \
--hash=sha256:c3ab72ea2f1c5d8ccf2561e79f6562fb2964629f3e55b3ac1c11c48b64c20336
jaxlib==0.4.13+cuda12.cudnn89; python_version == '3.8' \
--hash=sha256:31372f41dc28ecb11a5cc5573ae632e8ee9cfb63788edc8ad8a77e8d3279f569
jaxlib==0.4.18+cuda12.cudnn89; python_version >= '3.9' \
--hash=sha256:14f74ff081882ea091c121e355051b35932e39cb7ff7242b88a87f3690f3ca90 \
--hash=sha256:7c87dc2d68257b02e83c04be88a3c447373ee7077d65f43545bcbda5bfe2231d \
--hash=sha256:4d16e9c7592e1aaca0b3d28d2c8beba415a2721bb7001f2947728247951a250d \
--hash=sha256:759c08c69f4a5b1e6b39c3e4eff908a04ce3b2b483bb594ed624407c7d12d110 \
--hash=sha256:a7a04dbe1851cd50d07691282116aee49a2f0be7838e55b76d7ada86db06be62 \
--hash=sha256:2bf842db3d58c8c6c52fbc8ed3fabefd7b91a21746cd59d3eaf3522eea229b53 \
--hash=sha256:35d265ef9bb3835a14580cbaa9402060f117e46056f80e0996405fff3964667a \
--hash=sha256:0e4352f24d629e912965e6435e140c1b06086243a098651f2d01b75f3738b51c
+34
View File
@@ -0,0 +1,34 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Public API for MJX."""
# pylint:disable=g-importing-member
from mujoco.mjx._src.collision_driver import collision
from mujoco.mjx._src.constraint import make_constraint
from mujoco.mjx._src.device import device_get_into
from mujoco.mjx._src.device import device_put
from mujoco.mjx._src.forward import forward
from mujoco.mjx._src.forward import step
from mujoco.mjx._src.io import make_data
from mujoco.mjx._src.passive import passive
from mujoco.mjx._src.smooth import com_pos
from mujoco.mjx._src.smooth import com_vel
from mujoco.mjx._src.smooth import crb
from mujoco.mjx._src.smooth import factor_m
from mujoco.mjx._src.smooth import kinematics
from mujoco.mjx._src.smooth import mul_m
from mujoco.mjx._src.smooth import rne
from mujoco.mjx._src.smooth import transmission
from mujoco.mjx._src.types import *
+14
View File
@@ -0,0 +1,14 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
+64
View File
@@ -0,0 +1,64 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Collision base."""
import dataclasses
from typing import Dict, List, Optional, Tuple
import jax
# pylint: disable=g-importing-member
from mujoco.mjx._src.dataclasses import PyTreeNode
from mujoco.mjx._src.types import GeomType
# pylint: enable=g-importing-member
Contact = Tuple[jax.Array, jax.Array, jax.Array]
@dataclasses.dataclass
class Candidate:
geom1: int
geom2: int
ipair: int
geomp: int # priority geom
dim: int
CandidateSet = Dict[
Tuple[GeomType, GeomType, Tuple[int, ...], Tuple[int, ...]],
List[Candidate],
]
class GeomInfo(PyTreeNode):
"""Collision info for a geom."""
pos: jax.Array
mat: jax.Array
size: jax.Array
face: Optional[jax.Array] = None
vert: Optional[jax.Array] = None
edge: Optional[jax.Array] = None
facenorm: Optional[jax.Array] = None
class SolverParams(PyTreeNode):
"""Contact solver params."""
friction: jax.Array
solref: jax.Array
solreffriction: jax.Array
solimp: jax.Array
margin: jax.Array
gap: jax.Array
+724
View File
@@ -0,0 +1,724 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Convex collisions."""
from typing import Tuple
import jax
from jax import numpy as jp
from mujoco.mjx._src import math
# pylint: disable=g-importing-member
from mujoco.mjx._src.collision_base import Contact
from mujoco.mjx._src.collision_base import GeomInfo
# pylint: enable=g-importing-member
def _closest_segment_point_plane(
a: jax.Array, b: jax.Array, p0: jax.Array, plane_normal: jax.Array
) -> jax.Array:
"""Gets the closest point between a line segment and a plane.
Args:
a: first line segment point
b: second line segment point
p0: point on plane
plane_normal: plane normal
Returns:
closest point between the line segment and the plane
"""
# Parametrize a line segment as S(t) = a + t * (b - a), plug it into the plane
# equation dot(n, S(t)) - d = 0, then solve for t to get the line-plane
# intersection. We then clip t to be in [0, 1] to be on the line segment.
n = plane_normal
d = jp.sum(p0 * n) # shortest distance from origin to plane
denom = jp.sum(n * (b - a))
t = (d - jp.sum(n * a)) / (denom + 1e-6 * (denom == 0.0))
t = jp.clip(t, 0, 1)
segment_point = a + t * (b - a)
return segment_point
def _closest_triangle_point(
p0: jax.Array, p1: jax.Array, p2: jax.Array, pt: jax.Array
) -> jax.Array:
"""Gets the closest point between a triangle and a point in space.
Args:
p0: triangle point
p1: triangle point
p2: triangle point
pt: point to test
Returns:
closest point on the triangle w.r.t point pt
"""
# Parametrize the triangle s.t. a point inside the triangle is
# Q = p0 + u * e0 + v * e1, when 0 <= u <= 1, 0 <= v <= 1, and
# 0 <= u + v <= 1. Let e0 = (p1 - p0) and e1 = (p2 - p0).
# We analytically minimize the distance between the point pt and Q.
e0 = p1 - p0
e1 = p2 - p0
a = e0.dot(e0)
b = e0.dot(e1)
c = e1.dot(e1)
d = pt - p0
# The determinant is 0 only if the angle between e1 and e0 is 0
# (i.e. the triangle has overlapping lines).
det = a * c - b * b
u = (c * e0.dot(d) - b * e1.dot(d)) / det
v = (-b * e0.dot(d) + a * e1.dot(d)) / det
inside = (0 <= u) & (u <= 1) & (0 <= v) & (v <= 1) & (u + v <= 1)
closest_p = p0 + u * e0 + v * e1
d0 = (closest_p - pt).dot(closest_p - pt)
# If the closest point is outside the triangle, it must be on an edge, so we
# check each triangle edge for a closest point to the point pt.
closest_p1, d1 = math.closest_segment_point_and_dist(p0, p1, pt)
closest_p = jp.where((d0 < d1) & inside, closest_p, closest_p1)
min_d = jp.where((d0 < d1) & inside, d0, d1)
closest_p2, d2 = math.closest_segment_point_and_dist(p1, p2, pt)
closest_p = jp.where(d2 < min_d, closest_p2, closest_p)
min_d = jp.minimum(min_d, d2)
closest_p3, d3 = math.closest_segment_point_and_dist(p2, p0, pt)
closest_p = jp.where(d3 < min_d, closest_p3, closest_p)
return closest_p
def _closest_segment_triangle_points(
a: jax.Array,
b: jax.Array,
p0: jax.Array,
p1: jax.Array,
p2: jax.Array,
triangle_normal: jax.Array,
) -> Tuple[jax.Array, jax.Array]:
"""Gets the closest points between a line segment and triangle.
Args:
a: first line segment point
b: second line segment point
p0: triangle point
p1: triangle point
p2: triangle point
triangle_normal: normal of triangle
Returns:
closest point on the triangle w.r.t the line segment
"""
# The closest triangle point is either on the edge or within the triangle.
# First check triangle edges for the closest point.
# TODO(robotics-simulation): consider vmapping over closest point functions
seg_pt1, tri_pt1 = math.closest_segment_to_segment_points(a, b, p0, p1)
d1 = (seg_pt1 - tri_pt1).dot(seg_pt1 - tri_pt1)
seg_pt2, tri_pt2 = math.closest_segment_to_segment_points(a, b, p1, p2)
d2 = (seg_pt2 - tri_pt2).dot(seg_pt2 - tri_pt2)
seg_pt3, tri_pt3 = math.closest_segment_to_segment_points(a, b, p0, p2)
d3 = (seg_pt3 - tri_pt3).dot(seg_pt3 - tri_pt3)
# Next, handle the case where the closest triangle point is inside the
# triangle. Either the line segment intersects the triangle or a segment
# endpoint is closest to a point inside the triangle.
seg_pt4 = _closest_segment_point_plane(a, b, p0, triangle_normal)
tri_pt4 = _closest_triangle_point(p0, p1, p2, seg_pt4)
d4 = (seg_pt4 - tri_pt4).dot(seg_pt4 - tri_pt4)
# Get the point with minimum distance from the line segment point to the
# triangle point.
distance = jp.array([[d1, d2, d3, d4]])
min_dist = jp.amin(distance)
mask = (distance == min_dist).T
seg_pt = jp.array([seg_pt1, seg_pt2, seg_pt3, seg_pt4]) * mask
tri_pt = jp.array([tri_pt1, tri_pt2, tri_pt3, tri_pt4]) * mask
seg_pt = jp.sum(seg_pt, axis=0) / jp.sum(mask)
tri_pt = jp.sum(tri_pt, axis=0) / jp.sum(mask)
return seg_pt, tri_pt
def _manifold_points(
poly: jax.Array, poly_mask: jax.Array, poly_norm: jax.Array
) -> jax.Array:
"""Chooses four points on the polygon with approximately maximal area."""
dist_mask = jp.where(poly_mask, 0.0, -1e6)
a_idx = jp.argmax(dist_mask)
a = poly[a_idx]
# choose point b furthest from a
b_idx = (((a - poly) ** 2).sum(axis=1) + dist_mask).argmax()
b = poly[b_idx]
# choose point c furthest along the axis orthogonal to (a-b)
ab = jp.cross(poly_norm, a - b)
ap = a - poly
c_idx = (jp.abs(ap.dot(ab)) + dist_mask).argmax()
c = poly[c_idx]
# choose point d furthest from the other two triangle edges
ac = jp.cross(poly_norm, a - c)
bc = jp.cross(poly_norm, b - c)
bp = b - poly
dist_bp = jp.abs(bp.dot(bc)) + dist_mask
dist_ap = jp.abs(ap.dot(ac)) + dist_mask
d_idx = jp.concatenate([dist_bp, dist_ap]).argmax() % poly.shape[0]
return jp.array([a_idx, b_idx, c_idx, d_idx])
def _project_pt_onto_plane(
pt: jax.Array, plane_pt: jax.Array, plane_normal: jax.Array
) -> jax.Array:
"""Projects a point onto a plane along the plane normal."""
dist = (pt - plane_pt).dot(plane_normal)
return pt - dist * plane_normal
def _project_poly_onto_plane(
poly: jax.Array, plane_pt: jax.Array, plane_normal: jax.Array
) -> jax.Array:
"""Projects a polygon onto a plane using the plane normal."""
return jax.vmap(_project_pt_onto_plane, in_axes=[0, None, None])(
poly, plane_pt, math.normalize(plane_normal)
)
def _project_poly_onto_poly_plane(
poly1: jax.Array, norm1: jax.Array, poly2: jax.Array, norm2: jax.Array
) -> jax.Array:
"""Projects poly1 onto the poly2 plane along poly1's normal."""
d = poly2[0].dot(norm2)
denom = norm1.dot(norm2)
t = (d - poly1.dot(norm2)) / (denom + 1e-6 * (denom == 0.0))
new_poly = poly1 + t.reshape(-1, 1) * norm1
return new_poly
def _point_in_front_of_plane(
plane_pt: jax.Array, plane_normal: jax.Array, pt: jax.Array
) -> jax.Array:
"""Checks if a point is strictly in front of a plane."""
return (pt - plane_pt).dot(plane_normal) > 1e-6
def _clip_edge_to_planes(
edge_p0: jax.Array,
edge_p1: jax.Array,
plane_pts: jax.Array,
plane_normals: jax.Array,
) -> Tuple[jax.Array, jax.Array]:
"""Clips an edge against side planes.
We return two clipped points, and a mask to include the new edge or not.
Args:
edge_p0: the first point on the edge
edge_p1: the second point on the edge
plane_pts: side plane points
plane_normals: side plane normals
Returns:
new_ps: new edge points that are clipped against side planes
mask: a boolean mask, True if an edge point is a valid clipped point and
False otherwise
"""
p0, p1 = edge_p0, edge_p1
p0_in_front = jax.vmap(jp.dot)(p0 - plane_pts, plane_normals) > 1e-6
p1_in_front = jax.vmap(jp.dot)(p1 - plane_pts, plane_normals) > 1e-6
# Get candidate clipped points along line segment (p0, p1) by clipping against
# all clipping planes.
candidate_clipped_ps = jax.vmap(
_closest_segment_point_plane, in_axes=[None, None, 0, 0]
)(p0, p1, plane_pts, plane_normals)
def clip_edge_point(p0, p1, p0_in_front, clipped_ps):
@jax.vmap
def choose_edge_point(in_front, clipped_p):
return jp.where(in_front, clipped_p, p0)
# Pick the clipped point if p0 is in front of the clipping plane. Otherwise
# keep p0 as the edge point.
new_edge_ps = choose_edge_point(p0_in_front, clipped_ps)
# Pick the clipped point that is most along the edge direction.
# This degenerates to picking the original point p0 if p0 is *not* in front
# of any clipping planes.
dists = jp.dot(new_edge_ps - p0, p1 - p0)
new_edge_p = new_edge_ps[jp.argmax(dists)]
return new_edge_p
# Clip each edge point.
new_p0 = clip_edge_point(p0, p1, p0_in_front, candidate_clipped_ps)
new_p1 = clip_edge_point(p1, p0, p1_in_front, candidate_clipped_ps)
clipped_pts = jp.array([new_p0, new_p1])
# Keep the original points if both points are in front of any of the clipping
# planes, rather than creating a new clipped edge. If the entire subject edge
# is in front of any clipping plane, we need to grab an edge from the clipping
# polygon instead.
both_in_front = p0_in_front & p1_in_front
mask = ~jp.any(both_in_front)
new_ps = jp.where(mask, clipped_pts, jp.array([p0, p1]))
# Mask out crossing clipped edge points.
mask = jp.where((p0 - p1).dot(new_ps[0] - new_ps[1]) < 0, False, mask)
return new_ps, jp.array([mask, mask])
def _clip(
clipping_poly: jax.Array,
subject_poly: jax.Array,
clipping_normal: jax.Array,
subject_normal: jax.Array,
) -> Tuple[jax.Array, jax.Array]:
"""Clips a subject polygon against a clipping polygon.
A parallelized clipping algorithm for convex polygons. The result is a set of
vertices on the clipped subject polygon in the subject polygon plane.
Args:
clipping_poly: the polygon that we use to clip the subject polygon against
subject_poly: the polygon that gets clipped
clipping_normal: normal of the clipping polygon
subject_normal: normal of the subject polygon
Returns:
clipped_pts: points on the clipped polygon
mask: True if a point is in the clipping polygon, False otherwise
"""
# Get clipping edge points, edge planes, and edge normals.
clipping_p0 = jp.roll(clipping_poly, 1, axis=0)
clipping_plane_pts = clipping_p0
clipping_p1 = clipping_poly
clipping_plane_normals = jax.vmap(jp.cross, in_axes=[0, None])(
clipping_p1 - clipping_p0,
clipping_normal,
)
# Get subject edge points, edge planes, and edge normals.
subject_edge_p0 = jp.roll(subject_poly, 1, axis=0)
subject_plane_pts = subject_edge_p0
subject_edge_p1 = subject_poly
subject_plane_normals = jax.vmap(jp.cross, in_axes=[0, None])(
subject_edge_p1 - subject_edge_p0,
subject_normal,
)
# Clip all edges of the subject poly against clipping side planes.
clipped_edges0, masks0 = jax.vmap(
_clip_edge_to_planes, in_axes=[0, 0, None, None]
)(
subject_edge_p0,
subject_edge_p1,
clipping_plane_pts,
clipping_plane_normals,
)
# Project the clipping poly onto the subject plane.
clipping_p0_s = _project_poly_onto_poly_plane(
clipping_p0, clipping_normal, subject_poly, subject_normal
)
clipping_p1_s = _project_poly_onto_poly_plane(
clipping_p1, clipping_normal, subject_poly, subject_normal
)
# Clip all edges of the clipping poly against subject planes.
clipped_edges1, masks1 = jax.vmap(
_clip_edge_to_planes, in_axes=[0, 0, None, None]
)(clipping_p0_s, clipping_p1_s, subject_plane_pts, subject_plane_normals)
# Merge the points and reshape.
clipped_edges = jp.concatenate([clipped_edges0, clipped_edges1])
masks = jp.concatenate([masks0, masks1])
clipped_points = clipped_edges.reshape((-1, 3))
mask = masks.reshape(-1)
return clipped_points, mask
def _create_contact_manifold(
clipping_poly: jax.Array,
subject_poly: jax.Array,
clipping_norm: jax.Array,
subject_norm: jax.Array,
sep_axis: jax.Array,
) -> Tuple[jax.Array, jax.Array, jax.Array]:
"""Creates a contact manifold between two convex polygons.
The polygon faces are expected to have a counter clockwise winding order so
that clipping plane normals point away from the polygon center.
Args:
clipping_poly: the reference polygon to clip the contact against.
subject_poly: the subject polygon to clip contacts onto.
clipping_norm: the clipping polygon normal.
subject_norm: the subject polygon normal.
sep_axis: the separating axis
Returns:
tuple of dist, pos, and normal
"""
# Clip the subject (incident) face onto the clipping (reference) face.
# The incident points are clipped points on the subject polygon.
poly_incident, mask = _clip(
clipping_poly, subject_poly, clipping_norm, subject_norm
)
# The reference points are clipped points on the clipping polygon.
poly_ref = _project_poly_onto_plane(
poly_incident, clipping_poly[0], clipping_norm
)
behind_clipping_plane = _point_in_front_of_plane(
clipping_poly[0], -clipping_norm, poly_incident
)
mask = mask & behind_clipping_plane
# Choose four contact points.
best = _manifold_points(poly_ref, mask, clipping_norm)
contact_pts = jp.take(poly_ref, best, axis=0)
mask_pts = jp.take(mask, best, axis=0)
penetration_dir = jp.take(poly_incident, best, axis=0) - contact_pts
penetration = penetration_dir.dot(-clipping_norm)
dist = jp.where(mask_pts, -penetration, jp.ones_like(penetration))
pos = contact_pts
normal = -jp.stack([sep_axis] * 4, 0)
return dist, pos, normal
def _sat_hull_hull(
faces_a: jax.Array,
faces_b: jax.Array,
vertices_a: jax.Array,
vertices_b: jax.Array,
normals_a: jax.Array,
normals_b: jax.Array,
unique_edges_a: jax.Array,
unique_edges_b: jax.Array,
) -> Tuple[jax.Array, jax.Array, jax.Array]:
"""Runs the Separating Axis Test for a pair of hulls.
Given two convex hulls, the Separating Axis Test finds a separating axis
between all edge pairs and face pairs. Edge pairs create a single contact
point and face pairs create a contact manifold (up to four contact points).
We return both the edge and face contacts. Valid contacts can be checked with
dist < 0. Resulting edge contacts should be preferred over face contacts.
Args:
faces_a: An ndarray of hull A's polygon faces.
faces_b: An ndarray of hull B's polygon faces.
vertices_a: Vertices for hull A.
vertices_b: Vertices for hull B.
normals_a: Normal vectors for hull A's polygon faces.
normals_b: Normal vectors for hull B's polygon faces.
unique_edges_a: Unique edges for hull A.
unique_edges_b: Unique edges for hull B.
Returns:
tuple of dist, pos, and normal
"""
# get the separating axes
edge_dir_a = unique_edges_a[:, 0] - unique_edges_a[:, 1]
edge_dir_b = unique_edges_b[:, 0] - unique_edges_b[:, 1]
edge_dir_a_r = jp.tile(edge_dir_a, reps=(unique_edges_b.shape[0], 1))
edge_dir_b_r = jp.repeat(edge_dir_b, repeats=unique_edges_a.shape[0], axis=0)
edge_edge_axes = jax.vmap(jp.cross)(edge_dir_a_r, edge_dir_b_r)
edge_edge_axes = jax.vmap(lambda x: math.normalize(x, axis=0))(
edge_edge_axes
)
axes = jp.concatenate([normals_a, normals_b, edge_edge_axes])
# for each separating axis, get the support
@jax.vmap
def get_support(axis):
support_a = jax.vmap(jp.dot, in_axes=[None, 0])(axis, vertices_a)
support_b = jax.vmap(jp.dot, in_axes=[None, 0])(axis, vertices_b)
dist1 = support_a.max() - support_b.min()
dist2 = support_b.max() - support_a.min()
sign = jp.where(dist1 > dist2, -1, 1)
dist = jp.minimum(dist1, dist2)
dist = jp.where(~jp.all(axis == 0.0), dist, 1e6) # degenerate axis
return dist, sign
support, sign = get_support(axes)
# choose the best separating axis
best_idx = jp.argmin(support)
best_sign = sign[best_idx]
best_axis = axes[best_idx]
is_edge_contact = best_idx >= (normals_a.shape[0] + normals_b.shape[0])
# get the (reference) face most aligned with the separating axis
dist_a = jax.vmap(jp.dot, in_axes=[None, 0])(best_axis, normals_a)
dist_b = jax.vmap(jp.dot, in_axes=[None, 0])(best_axis, normals_b)
a_max = dist_a.argmax()
b_max = dist_b.argmax()
a_min = dist_a.argmin()
b_min = dist_b.argmin()
ref_face = jp.where(best_sign > 0, faces_a[a_max], faces_b[b_max])
ref_face_norm = jp.where(best_sign > 0, normals_a[a_max], normals_b[b_max])
incident_face = jp.where(best_sign > 0, faces_b[b_min], faces_a[a_min])
incident_face_norm = jp.where(
best_sign > 0, normals_b[b_min], normals_a[a_min]
)
dist, pos, normal = _create_contact_manifold(
ref_face,
incident_face,
ref_face_norm,
incident_face_norm,
-best_sign * best_axis,
)
# For edge contacts, we use the clipped face point, mainly for performance
# reasons. For small penetration, the clipped face point is roughly the edge
# contact point.
idx = dist.argmin()
dist = jp.where(
is_edge_contact,
jp.array([dist[idx], 1, 1, 1]),
dist,
)
pos = jp.where(is_edge_contact, jp.tile(pos[idx], (4, 1)), pos)
return dist, pos, normal
def plane_convex(plane: GeomInfo, convex: GeomInfo) -> Contact:
"""Calculates contacts between a plane and a convex object."""
vert = convex.vert
# get points in the convex frame
plane_pos = convex.mat.T @ (plane.pos - convex.pos)
n = convex.mat.T @ plane.mat[:, 2]
support = (plane_pos - vert) @ n
idx = _manifold_points(vert, support > 0, n)
pos = vert[idx]
# convert to world frame
pos = convex.pos + pos @ convex.mat.T
n = plane.mat[:, 2]
frame = jp.stack([math.make_frame(n)] * 4, axis=0)
unique = jp.tril(idx == idx[:, None]).sum(axis=1) == 1
dist = jp.where(unique, -support[idx], 1)
return dist, pos, frame
def sphere_convex(sphere: GeomInfo, convex: GeomInfo) -> Contact:
"""Calculates contact between a sphere and a convex object."""
faces = jp.take(convex.vert, convex.face, axis=0)
normals = convex.facenorm
# Put sphere in convex frame.
sphere_pos = convex.mat.T @ (sphere.pos - convex.pos)
# Get support from face normals.
@jax.vmap
def get_support(faces, normal):
pos = sphere_pos - normal * sphere.size[0]
return jp.dot(pos - faces[0], normal)
support = get_support(faces, normals)
# Pick the face with minimal penetration as long as it has support.
support = jp.where(support >= 0, -1e12, support)
best_idx = support.argmax()
face = faces[best_idx]
normal = normals[best_idx]
# Get closest point between the polygon face and the sphere center point.
# Project the sphere center point onto poly plane. If it's inside polygon
# edge normals, then we're done.
pt = _project_pt_onto_plane(sphere_pos, face[0], normal)
edge_p0 = jp.roll(face, 1, axis=0)
edge_p1 = face
edge_normals = jax.vmap(jp.cross, in_axes=[0, None])(
edge_p1 - edge_p0,
normal,
)
edge_dist = jax.vmap(
lambda plane_pt, plane_norm: (pt - plane_pt).dot(plane_norm)
)(edge_p0, edge_normals)
inside = jp.all(edge_dist <= 0) # lte to handle degenerate edges
# If the point is outside edge normals, project onto the closest edge plane
# that the point is in front of.
degenerate_edge = jp.all(edge_normals == 0, axis=1)
behind = edge_dist < 0.0
edge_dist = jp.where(degenerate_edge | behind, 1e12, edge_dist)
idx = edge_dist.argmin()
edge_pt = math.closest_segment_point(edge_p0[idx], edge_p1[idx], pt)
pt = jp.where(inside, pt, edge_pt)
# Get the normal, dist, and contact position.
n, d = math.normalize_with_norm(pt - sphere_pos)
spt = sphere_pos + n * sphere.size[0]
dist = d - sphere.size[0]
pos = (pt + spt) * 0.5
# Go back to world frame.
n = convex.mat @ n
pos = convex.mat @ pos + convex.pos
return jax.tree_map(
lambda x: jp.expand_dims(x, axis=0), (dist, pos, math.make_frame(n))
)
def capsule_convex(cap: GeomInfo, convex: GeomInfo) -> Contact:
"""Calculates contacts between a capsule and a convex object."""
# Get convex transformed normals, faces, and vertices.
faces = jp.take(convex.vert, convex.face, axis=0)
normals = convex.facenorm
# Put capsule in convex frame.
cap_pos = convex.mat.T @ (cap.pos - convex.pos)
axis, length = cap.mat[:, 2], cap.size[1]
axis = convex.mat.T @ axis
seg = axis * length
cap_pts = jp.array([
cap_pos - seg,
cap_pos + seg,
])
# Get support from face normals.
@jax.vmap
def get_support(face, normal):
pts = cap_pts - normal * cap.size[0]
sup = jax.vmap(lambda x: jp.dot(x - face[0], normal))(pts)
return sup.min()
support = get_support(faces, normals)
has_support = jp.all(support < 0)
# Pick the face with minimal penetration as long as it has support.
support = jp.where(support >= 0, -1e12, support)
best_idx = support.argmax()
face = faces[best_idx]
normal = normals[best_idx]
# Clip the edge against side planes and create two contact points against the
# face.
edge_p0 = jp.roll(face, 1, axis=0)
edge_p1 = face
edge_normals = jax.vmap(jp.cross, in_axes=[0, None])(
edge_p1 - edge_p0,
normal,
)
cap_pts_clipped, mask = _clip_edge_to_planes(
cap_pts[0], cap_pts[1], edge_p0, edge_normals
)
cap_pts_clipped = cap_pts_clipped - normal * cap.size[0]
face_pts = jax.vmap(_project_pt_onto_plane, in_axes=[0, None, None])(
cap_pts_clipped, face[0], normal
)
# Create variables for the face contact.
pos = (cap_pts_clipped + face_pts) * 0.5
norm = jp.stack([normal] * 2, 0)
penetration = jp.where(
mask & has_support, jp.dot(face_pts - cap_pts_clipped, normal), -1
)
# Get a potential edge contact.
edge_closest, cap_closest = jax.vmap(
math.closest_segment_to_segment_points, in_axes=[0, 0, None, None]
)(edge_p0, edge_p1, cap_pts[0], cap_pts[1])
e_idx = ((edge_closest - cap_closest) ** 2).sum(axis=1).argmin()
cap_closest_pt, edge_closest_pt = cap_closest[e_idx], edge_closest[e_idx]
edge_axis = cap_closest_pt - edge_closest_pt
edge_axis, edge_dist = math.normalize_with_norm(edge_axis)
edge_pos = (
edge_closest_pt + (cap_closest_pt - edge_axis * cap.size[0])
) * 0.5
edge_norm = edge_axis
edge_penetration = cap.size[0] - edge_dist
has_edge_contact = edge_penetration > 0
# Get the contact info.
pos = jp.where(has_edge_contact, pos.at[0].set(edge_pos), pos)
n = -jp.where(has_edge_contact, norm.at[0].set(edge_norm), norm)
# Go back to world frame.
pos = convex.pos + pos @ convex.mat.T
n = n @ convex.mat.T
dist = -jp.where(
has_edge_contact, penetration.at[0].set(edge_penetration), penetration
)
frame = jax.vmap(math.make_frame)(n)
return dist, pos, frame
def convex_convex(c1: GeomInfo, c2: GeomInfo) -> Contact:
"""Calculates contacts between two convex objects."""
if c1.face is None or c2.face is None or c1.vert is None or c2.vert is None:
raise AssertionError('Mesh info missing.')
# pad face vertices so that we can broadcast between geom1 and geom2
s1, s2 = c1.face.shape[-1], c2.face.shape[-1]
if s1 < s2:
face = jp.pad(c1.face, ((0, 0), (0, s2 - s1)), 'edge')
c1 = c1.replace(face=face)
elif s2 < s1:
face = jp.pad(c2.face, ((0, 0), (0, s1 - s2)), 'edge')
c2 = c2.replace(face=face)
# ensure that the first object has fewer verts
swapped = c1.vert.shape[0] > c2.vert.shape[0]
if swapped:
c1, c2 = c2, c1
faces1 = jp.take(c1.vert, c1.face, axis=0)
faces2 = jp.take(c2.vert, c2.face, axis=0)
to_local_pos = c2.mat.T @ (c1.pos - c2.pos)
to_local_mat = c2.mat.T @ c1.mat
faces1 = to_local_pos + faces1 @ to_local_mat.T
normals1 = c1.facenorm @ to_local_mat.T
normals2 = c2.facenorm
vertices1 = to_local_pos + c1.vert @ to_local_mat.T
vertices2 = c2.vert
unique_edges1 = jp.take(vertices1, c1.edge, axis=0)
unique_edges2 = jp.take(vertices2, c2.edge, axis=0)
dist, pos, normal = _sat_hull_hull(
faces1,
faces2,
vertices1,
vertices2,
normals1,
normals2,
unique_edges1,
unique_edges2,
)
# Go back to world frame.
pos = c2.pos + pos @ c2.mat.T
normal = normal @ c2.mat.T
normal = -normal if swapped else normal
frame = jax.vmap(math.make_frame)(normal)
return dist, pos, frame
# store ncon as function attributes
plane_convex.ncon = 4
sphere_convex.ncon = 1
capsule_convex.ncon = 2
convex_convex.ncon = 4
+373
View File
@@ -0,0 +1,373 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Collide geometries."""
from typing import Callable, Dict, Optional, Sequence, Tuple, Union
import jax
from jax import numpy as jp
import mujoco
from mujoco.mjx._src import collision_base
# pylint: disable=g-importing-member
from mujoco.mjx._src.collision_base import Candidate
from mujoco.mjx._src.collision_base import CandidateSet
from mujoco.mjx._src.collision_base import GeomInfo
from mujoco.mjx._src.collision_base import SolverParams
from mujoco.mjx._src.collision_convex import capsule_convex
from mujoco.mjx._src.collision_convex import convex_convex
from mujoco.mjx._src.collision_convex import plane_convex
from mujoco.mjx._src.collision_convex import sphere_convex
from mujoco.mjx._src.collision_primitive import capsule_capsule
from mujoco.mjx._src.collision_primitive import plane_capsule
from mujoco.mjx._src.collision_primitive import plane_sphere
from mujoco.mjx._src.collision_primitive import sphere_capsule
from mujoco.mjx._src.collision_primitive import sphere_sphere
from mujoco.mjx._src.types import Contact
from mujoco.mjx._src.types import Data
from mujoco.mjx._src.types import DisableBit
from mujoco.mjx._src.types import GeomType
from mujoco.mjx._src.types import Model
# pylint: enable=g-importing-member
import numpy as np
# pair-wise collision functions
_COLLISION_FUNC = {
(GeomType.PLANE, GeomType.SPHERE): plane_sphere,
(GeomType.PLANE, GeomType.CAPSULE): plane_capsule,
(GeomType.PLANE, GeomType.BOX): plane_convex,
(GeomType.PLANE, GeomType.MESH): plane_convex,
(GeomType.SPHERE, GeomType.SPHERE): sphere_sphere,
(GeomType.SPHERE, GeomType.CAPSULE): sphere_capsule,
(GeomType.SPHERE, GeomType.BOX): sphere_convex,
(GeomType.SPHERE, GeomType.MESH): sphere_convex,
(GeomType.CAPSULE, GeomType.CAPSULE): capsule_capsule,
(GeomType.CAPSULE, GeomType.BOX): capsule_convex,
(GeomType.CAPSULE, GeomType.MESH): capsule_convex,
(GeomType.BOX, GeomType.BOX): convex_convex,
(GeomType.BOX, GeomType.MESH): convex_convex,
(GeomType.MESH, GeomType.MESH): convex_convex,
}
def get_collision_fn(
key: Tuple[Union[GeomType, mujoco.mjtGeom], Union[GeomType, mujoco.mjtGeom]]
) -> Optional[Callable[[GeomInfo, GeomInfo], collision_base.Contact]]:
"""Returns a collision function given a pair of geom types."""
return _COLLISION_FUNC.get(key, None)
def _add_candidate(
result: CandidateSet,
m: Union[Model, mujoco.MjModel],
g1: int,
g2: int,
ipair: int = -1,
):
"""Adds a candidate to test for collision."""
t1, t2 = m.geom_type[g1], m.geom_type[g2]
if t1 > t2:
t1, t2, g1, g2 = t2, t1, g2, g1
def mesh_key(i):
convex_data = [[None] * m.ngeom] * 3
if isinstance(m, Model):
convex_data = [m.geom_convex_face, m.geom_convex_vert, m.geom_convex_edge]
key = tuple((-1,) if v[i] is None else v[i].shape for v in convex_data)
return key
k1, k2 = mesh_key(g1), mesh_key(g2)
candidates = {(c.geom1, c.geom2) for c in result.get((t1, t2, k1, k2), [])}
if (g1, g2) in candidates:
return
if ipair > -1:
candidate = Candidate(g1, g2, ipair, -1, m.pair_dim[ipair])
elif m.geom_priority[g1] != m.geom_priority[g2]:
gp = g1 if m.geom_priority[g1] > m.geom_priority[g2] else g2
candidate = Candidate(g1, g2, -1, gp, m.geom_condim[gp])
else:
dim = max(m.geom_condim[g1], m.geom_condim[g2])
candidate = Candidate(g1, g2, -1, -1, dim)
result.setdefault((t1, t2, k1, k2), []).append(candidate)
def _pair_params(
m: Model,
candidates: Sequence[Candidate],
) -> SolverParams:
"""Gets solver params for pair geoms."""
ipair = jp.array([c.ipair for c in candidates])
friction = jp.clip(m.pair_friction[ipair], a_min=mujoco.mjMINMU)
solref = m.pair_solref[ipair]
solreffriction = m.pair_solreffriction[ipair]
solimp = m.pair_solimp[ipair]
margin = m.pair_margin[ipair]
gap = m.pair_gap[ipair]
return SolverParams(friction, solref, solreffriction, solimp, margin, gap)
def _priority_params(
m: Model,
candidates: Sequence[Candidate],
) -> SolverParams:
"""Gets solver params from priority geoms."""
geomp = jp.array([c.geomp for c in candidates])
friction = m.geom_friction[geomp][:, jp.array([0, 0, 1, 2, 2])]
solref = m.geom_solref[geomp]
solreffriction = jp.zeros(geomp.shape + (mujoco.mjNREF,))
solimp = m.geom_solimp[geomp]
g = jp.array([(c.geom1, c.geom2) for c in candidates])
margin = jp.amax(m.geom_margin[g.T], axis=0)
gap = jp.amax(m.geom_gap[g.T], axis=0)
return SolverParams(friction, solref, solreffriction, solimp, margin, gap)
def _dynamic_params(
m: Model,
candidates: Sequence[Candidate],
) -> SolverParams:
"""Gets solver params for dynamic geoms."""
g1 = jp.array([c.geom1 for c in candidates])
g2 = jp.array([c.geom2 for c in candidates])
friction = jp.maximum(m.geom_friction[g1], m.geom_friction[g2])
# copy friction terms for the full geom pair
friction = friction[:, jp.array([0, 0, 1, 2, 2])]
minval = jp.array(mujoco.mjMINVAL)
solmix1, solmix2 = m.geom_solmix[g1], m.geom_solmix[g2]
mix = solmix1 / (solmix1 + solmix2)
mix = jp.where((solmix1 < minval) & (solmix2 < minval), 0.5, mix)
mix = jp.where((solmix1 < minval) & (solmix2 >= minval), 0.0, mix)
mix_fn = jax.vmap(lambda a, b, m: m * a + (1 - m) * b)
solref1, solref2 = m.geom_solref[g1], m.geom_solref[g2]
solref = jp.minimum(solref1, solref2)
s_mix = mix_fn(solref1, solref2, mix)
solref = jp.where((solref1[0] > 0) & (solref2[0] > 0), s_mix, solref)
solreffriction = jp.zeros(g1.shape + (mujoco.mjNREF,))
solimp = mix_fn(m.geom_solimp[g1], m.geom_solimp[g2], mix)
margin = jp.maximum(m.geom_margin[g1], m.geom_margin[g2])
gap = jp.maximum(m.geom_gap[g1], m.geom_gap[g2])
return SolverParams(friction, solref, solreffriction, solimp, margin, gap)
def _pair_info(
m: Model, d: Data, geom1: Sequence[int], geom2: Sequence[int]
) -> Tuple[GeomInfo, GeomInfo, Sequence[Dict[str, Optional[int]]]]:
"""Returns geom pair info for calculating collision."""
g1, g2 = jp.array(geom1), jp.array(geom2)
info1 = GeomInfo(
d.geom_xpos[g1],
d.geom_xmat[g1],
m.geom_size[g1],
)
info2 = GeomInfo(
d.geom_xpos[g2],
d.geom_xmat[g2],
m.geom_size[g2],
)
in_axes1 = in_axes2 = jax.tree_map(lambda x: 0, info1)
if m.geom_convex_face[geom1[0]] is not None:
info1 = info1.replace(
face=jp.stack([m.geom_convex_face[i] for i in geom1]),
vert=jp.stack([m.geom_convex_vert[i] for i in geom1]),
edge=jp.stack([m.geom_convex_edge[i] for i in geom1]),
facenorm=jp.stack([m.geom_convex_facenormal[i] for i in geom1]),
)
in_axes1 = in_axes1.replace(face=0, vert=0, edge=0, facenorm=0)
if m.geom_convex_face[geom2[0]] is not None:
info2 = info2.replace(
face=jp.stack([m.geom_convex_face[i] for i in geom2]),
vert=jp.stack([m.geom_convex_vert[i] for i in geom2]),
edge=jp.stack([m.geom_convex_edge[i] for i in geom2]),
facenorm=jp.stack([m.geom_convex_facenormal[i] for i in geom2]),
)
in_axes2 = in_axes2.replace(face=0, vert=0, edge=0, facenorm=0)
return info1, info2, [in_axes1, in_axes2]
def _body_pair_filter(
m: Union[Model, mujoco.MjModel], b1: int, b2: int
) -> bool:
"""Filters body pairs for collision."""
dsbl_filterparent = m.opt.disableflags & DisableBit.FILTERPARENT
weld1 = m.body_weldid[b1]
weld2 = m.body_weldid[b2]
parent_weld1 = m.body_weldid[m.body_parentid[weld1]]
parent_weld2 = m.body_weldid[m.body_parentid[weld2]]
if weld1 == weld2:
# filter out self-collisions
return True
if (
not dsbl_filterparent
and weld1 != 0
and weld2 != 0
and (weld1 == parent_weld2 or weld2 == parent_weld1)
):
# filter out parent-child collisions
return True
return False
def _collide_geoms(
m: Model,
d: Data,
geom_types: Tuple[GeomType, GeomType],
candidates: Sequence[Candidate],
) -> Contact:
"""Collides a geom pair."""
fn = get_collision_fn(geom_types)
if not fn:
return Contact.zero()
# group sol params by different candidate types
typ_cands = {}
for c in candidates:
typ = (c.ipair > -1, c.geomp > -1)
typ_cands.setdefault(typ, []).append(c)
geom1, geom2, params = [], [], []
for (pair, priority), candidates in typ_cands.items():
geom1.extend([c.geom1 for c in candidates])
geom2.extend([c.geom2 for c in candidates])
if pair:
params.append(_pair_params(m, candidates))
elif priority:
params.append(_priority_params(m, candidates))
else:
params.append(_dynamic_params(m, candidates))
# call contact function
g1, g2, in_axes = _pair_info(m, d, geom1, geom2)
res = jax.vmap(fn, in_axes=in_axes)(g1, g2)
dist, pos, frame = jax.tree_map(jp.concatenate, res)
params = jax.tree_map(lambda *x: jp.concatenate(x), *params)
geom1, geom2 = jp.array(geom1), jp.array(geom2)
# repeat params by the number of contacts per geom pair
n_repeat = dist.shape[-1] // geom1.shape[0]
geom1, geom2, params = jax.tree_map(
lambda x: jp.repeat(x, n_repeat, axis=0),
(geom1, geom2, params),
)
con = Contact(
dist=dist,
pos=pos,
frame=frame,
includemargin=params.margin - params.gap,
friction=params.friction,
solref=params.solref,
solreffriction=params.solreffriction,
solimp=params.solimp,
geom1=geom1,
geom2=geom2,
dim=np.array([]),
efc_address=np.array([]),
)
return con
def _max_contact_points(m: Model) -> int:
"""Returns the maximum number of contact points when set as a numeric."""
for i in range(m.nnumeric):
name = m.names[m.name_numericadr[i] :].decode('utf-8').split('\x00', 1)[0]
if name == 'max_contact_points':
return int(m.numeric_data[m.numeric_adr[i]])
return -1
def collision_candidates(m: Union[Model, mujoco.MjModel]) -> CandidateSet:
"""Returns candidates for collision checking."""
candidate_set = {}
for ipair in range(m.npair):
g1, g2 = m.pair_geom1[ipair], m.pair_geom2[ipair]
_add_candidate(candidate_set, m, g1, g2, ipair)
body_pairs = []
exclude_signature = set(m.exclude_signature)
for b1 in range(m.nbody):
for b2 in range(b1, m.nbody):
signature = (b1 << 16) + (b2)
if signature in exclude_signature:
continue
if _body_pair_filter(m, b1, b2):
continue
body_pairs.append((b1, b2))
for b1, b2 in body_pairs:
start1 = m.body_geomadr[b1]
end1 = m.body_geomadr[b1] + m.body_geomnum[b1]
for g1 in range(start1, end1):
start2 = m.body_geomadr[b2]
end2 = m.body_geomadr[b2] + m.body_geomnum[b2]
for g2 in range(start2, end2):
mask = m.geom_contype[g1] & m.geom_conaffinity[g2]
mask |= m.geom_contype[g2] & m.geom_conaffinity[g1]
if mask != 0:
_add_candidate(candidate_set, m, g1, g2)
return candidate_set
def ncon(m: Model) -> int:
"""Returns the number of contacts computed in MJX given a model."""
candidates = collision_candidates(m)
max_count = _max_contact_points(m)
count = sum([
len(v) * get_collision_fn(k[0:2]).ncon for k, v in candidates.items() # pytype: disable=attribute-error
])
return min(max_count, count) if max_count > -1 else count
def collision(m: Model, d: Data) -> Data:
"""Collides geometries."""
candidate_set = collision_candidates(m)
contacts = []
for key, candidates in candidate_set.items():
geom_types = key[0:2]
contacts.append(_collide_geoms(m, d, geom_types, candidates))
if not contacts:
return d.replace(contact=Contact.zero(), ncon=0)
contact = jax.tree_map(lambda *x: jp.concatenate(x), *contacts)
max_contact_points = _max_contact_points(m)
if max_contact_points > -1 and contact.dist.shape[0] > max_contact_points:
# get top-k contacts
_, idx = jax.lax.top_k(-contact.dist, k=max_contact_points)
contact = jax.tree_map(lambda x, idx=idx: jp.take(x, idx, axis=0), contact)
ncon_ = contact.dist.shape[0]
ns = d.ne + d.nf + d.nl
# TODO(robotics-simulation): add support for other friction dimensions
contact = contact.replace(efc_address=np.arange(ns, ns + d.ncon * 4, 4))
contact = contact.replace(dim=3 * np.ones(ncon_, dtype=np.int32))
return d.replace(contact=contact, ncon=ncon_)
@@ -0,0 +1,499 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Tests the collision driver."""
import dataclasses
from typing import Dict, Optional, Tuple
from absl.testing import absltest
from absl.testing import parameterized
from etils import epath
import jax
import jax.numpy as jp
import mujoco
from mujoco import mjx
# pylint: disable=g-importing-member
from mujoco.mjx._src.types import Contact
from mujoco.mjx._src.types import Data
from mujoco.mjx._src.types import Model
# pylint: emable=g-importing-member
import numpy as np
def _assert_attr_eq(mjx_d, mj_d, attr, name, atol):
if attr == 'efc_address':
# we do not test efc_address since it gets set in constraint logic
return
err_msg = f'mismatch: {attr} in run: {name}'
mjx_d, mj_d = getattr(mjx_d, attr), getattr(mj_d, attr)
if attr == 'frame':
mj_d = mj_d.reshape((-1, 3, 3))
if mjx_d.shape != mj_d.shape:
raise AssertionError(f'{attr} shape mismatch: {mjx_d.shape}, {mj_d.shape}')
np.testing.assert_allclose(mjx_d, mj_d, err_msg=err_msg, atol=atol)
def _collide(
mjcf: str, assets: Optional[Dict[str, str]] = None
) -> Tuple[mujoco.MjModel, mujoco.MjData, Model, Data]:
m = mujoco.MjModel.from_xml_string(mjcf, assets or {})
mx = mjx.device_put(m)
d = mujoco.MjData(m)
dx = mjx.device_put(d)
mujoco.mj_step(m, d)
collision_jit_fn = jax.jit(mjx.collision)
kinematics_jit_fn = jax.jit(mjx.kinematics)
dx = kinematics_jit_fn(mx, dx)
dx = collision_jit_fn(mx, dx)
return d, dx
class SphereCollisionTest(parameterized.TestCase):
_SPHERE_PLANE = """
<mujoco>
<worldbody>
<geom size="40 40 40" type="plane"/>
<body pos="0 0 0.4">
<joint type="free"/>
<geom size="0.5" type="sphere"/>
</body>
</worldbody>
</mujoco>
"""
_SPHERE_SPHERE = """
<mujoco>
<worldbody>
<body>
<joint type="free"/>
<geom pos="0 0 0" size="0.2" type="sphere"/>
</body>
<body >
<joint type="free"/>
<geom pos="0 0.3 0" size="0.11" type="sphere"/>
</body>
</worldbody>
</mujoco>
"""
_SPHERE_CAP = """
<mujoco>
<worldbody>
<body>
<joint type="free"/>
<geom pos="0 0.3 0" size="0.05" type="sphere"/>
</body>
<body>
<joint axis="1 0 0" type="free"/>
<geom fromto="0.0 -0.5 0.14 0.0 0.5 0.14" size="0.1" type="capsule"/>
</body>
</worldbody>
</mujoco>
"""
@parameterized.parameters(
('sphere_plane', _SPHERE_PLANE),
('sphere_sphere', _SPHERE_SPHERE),
('sphere_cap', _SPHERE_CAP),
)
def test_sphere(self, name, mjcf):
d, dx = _collide(mjcf)
for field in dataclasses.fields(Contact):
_assert_attr_eq(dx.contact, d.contact, field.name, name, 1e-5)
_SPHERE_CONVEX = """
<mujoco>
<worldbody>
<body pos="0.52 0 0.52">
<joint axis="1 0 0" type="free"/>
<geom size="0.05" type="sphere"/>
</body>
<body>
<joint axis="1 0 0" type="free"/>
<geom size="0.5 0.5 0.5" type="box"/>
</body>
</worldbody>
</mujoco>
"""
def test_sphere_convex(self):
d, dx = _collide(self._SPHERE_CONVEX)
for field in dataclasses.fields(Contact):
_assert_attr_eq(dx.contact, d.contact, field.name, 'sphere_convex', 1e-4)
class CapsuleCollisionTest(parameterized.TestCase):
_CAP_PLANE = """
<mujoco>
<worldbody>
<geom size="40 40 40" type="plane"/>
<body pos="0 0 0.4">
<joint type="free"/>
<geom fromto="-1 0 0 1 0 0" size="0.5" type="capsule"/>
</body>
</worldbody>
</mujoco>
"""
_CAP_CAP = """
<mujoco model="two_capsules">
<worldbody>
<body>
<joint type="free"/>
<geom fromto="0.62235904 0.58846647 0.651046 1.5330081 0.33564585 0.977849"
size="0.05" type="capsule"/>
</body>
<body>
<joint type="free"/>
<geom fromto="0.5505271 0.60345304 0.476661 1.3900293 0.30709633 0.932082"
size="0.05" type="capsule"/>
</body>
</worldbody>
</mujoco>
"""
@parameterized.parameters(
('capsule_plane', _CAP_PLANE),
('capsule_capsule', _CAP_CAP),
)
def test_capsule(self, name, mjcf):
d, dx = _collide(mjcf)
for field in dataclasses.fields(Contact):
_assert_attr_eq(dx.contact, d.contact, field.name, name, 1e-4)
_PARALLEL_CAP = """
<mujoco>
<worldbody>
<body>
<joint type="free"/>
<geom fromto="-0.5 0.1 0.25 0.5 0.1 0.25" size="0.1" type="capsule"/>
</body>
<body>
<joint type="free"/>
<geom fromto="-0.5 0.1 0.1 0.5 0.1 0.1" size="0.1" type="capsule"/>
</body>
</worldbody>
</mujoco>
"""
def test_parallel_capsules(self):
"""Tests that two parallel capsules are colliding at the midpoint."""
_, dx = _collide(self._PARALLEL_CAP)
np.testing.assert_allclose(dx.contact.dist, -0.05)
np.testing.assert_allclose(
dx.contact.pos[0],
np.array([0.0, 0.1, (0.15 + 0.2) / 2.0]),
atol=1e-5,
)
np.testing.assert_allclose(
dx.contact.frame[0, 0, :], np.array([0, 0.0, -1.0]), atol=1e-5
)
_CAP_BOX = """
<mujoco>
<worldbody>
<body pos="0 0 0.54">
<joint axis="1 0 0" type="free"/>
<geom fromto="-0.4 0 0 0.4 0 0" size="0.05" type="capsule"/>
</body>
<body>
<joint axis="1 0 0" type="free"/>
<geom size="0.5 0.5 0.5" type="box"/>
</body>
</worldbody>
</mujoco>
"""
def test_capsule_convex(self):
"""Tests a capsule-convex collision for a face contact."""
d, dx = _collide(self._CAP_BOX)
for field in dataclasses.fields(Contact):
_assert_attr_eq(dx.contact, d.contact, field.name, 'capsule_convex', 1e-4)
_CAP_EDGE_BOX = """
<mujoco>
<worldbody>
<body pos="0.5 0 0.55" euler="0 30 0">
<joint axis="1 0 0" type="free"/>
<geom fromto="-0.6 0 0 0.6 0 0" size="0.05" type="capsule"/>
</body>
<body>
<joint axis="1 0 0" type="free"/>
<geom size="0.5 0.5 0.5" type="box"/>
</body>
</worldbody>
</mujoco>
"""
def test_capsule_convex_edge(self):
"""Tests a capsule-convex collision for an edge contact."""
d, dx = _collide(self._CAP_EDGE_BOX)
c = dx.contact
self.assertEqual(c.pos.shape[0], 2)
self.assertGreater(c.dist[1], 0)
# extract the contact point with penetration
c = jax.tree_map(lambda x: jp.take(x, 0, axis=0)[None], dx.contact)
c = c.replace(dim=c.dim[np.array([0])])
for field in dataclasses.fields(Contact):
_assert_attr_eq(c, d.contact, field.name, 'capsule_convex_edge', 1e-4)
class ConvexTest(absltest.TestCase):
"""Tests the convex contact functions."""
_BOX_PLANE = """
<mujoco>
<worldbody>
<geom size="40 40 40" type="plane"/>
<body pos="0 0 0.7" euler="45 0 0">
<joint axis="1 0 0" type="free"/>
<geom size="0.5 0.5 0.5" type="box"/>
</body>
</worldbody>
</mujoco>
"""
def test_box_plane(self):
"""Tests box collision with a plane."""
d, dx = _collide(self._BOX_PLANE)
np.testing.assert_array_less(dx.contact.dist[:2], 0)
np.testing.assert_array_less(-dx.contact.dist[2:], 0)
# extract the contact points with penetration
c = jax.tree_map(lambda x: jp.take(x, jp.array([0, 1]), axis=0), dx.contact)
c = c.replace(dim=c.dim[np.array([0, 1])])
for field in dataclasses.fields(Contact):
_assert_attr_eq(c, d.contact, field.name, 'box_plane', 1e-2)
_BOX_BOX = """
<mujoco>
<worldbody>
<body pos="0.0 1.0 0.2">
<joint axis="1 0 0" type="free"/>
<geom size="0.2 0.2 0.2" type="box"/>
</body>
<body pos="0.1 1.0 0.495" euler="0.1 -0.1 45">
<joint axis="1 0 0" type="free"/>
<geom size="0.1 0.1 0.1" type="box"/>
</body>
</worldbody>
</mujoco>
"""
def test_box_box(self):
"""Tests a face contact for a box-box collision."""
d, dx = _collide(self._BOX_BOX)
c = dx.contact
self.assertEqual(c.pos.shape[0], 4)
np.testing.assert_array_less(c.dist, 0)
np.testing.assert_array_almost_equal(c.pos[:, 2], np.array([0.39] * 4), 2)
np.testing.assert_array_almost_equal(
c.frame[:, 0, :], np.array([[0.0, 0.0, 1.0]] * 4)
)
np.testing.assert_array_almost_equal(
c.frame.reshape((-1, 9)), d.contact.frame[:4, :]
)
_BOX_BOX_EDGE = """
<mujoco>
<worldbody>
<body pos="-1.0 -1.0 0.2">
<joint axis="1 0 0" type="free"/>
<geom size="0.2 0.2 0.2" type="box"/>
</body>
<body pos="-1.0 -1.2 0.55" euler="0 45 30">
<joint axis="1 0 0" type="free"/>
<geom size="0.1 0.1 0.1" type="box"/>
</body>
</worldbody>
</mujoco>
"""
def test_box_box_edge(self):
"""Tests an edge contact for a box-box collision."""
d, dx = _collide(self._BOX_BOX_EDGE)
# Only one contact point.
np.testing.assert_array_less(dx.contact.dist[:1], 0)
np.testing.assert_array_less(-dx.contact.dist[1:], 0)
# extract the contact point with penetration
c = jax.tree_map(lambda x: jp.take(x, 0, axis=0)[None], dx.contact)
c = c.replace(dim=c.dim[np.array([0])])
for field in dataclasses.fields(Contact):
_assert_attr_eq(c, d.contact, field.name, 'box_box_edge', 1e-2)
_CONVEX_CONVEX = """
<mujoco>
<asset>
<mesh name="tetrahedron" file="meshes/tetrahedron.stl" scale="0.1 0.1 0.1" />
<mesh name="dodecahedron" file="meshes/dodecahedron.stl" scale="0.01 0.01 0.01" />
</asset>
<worldbody>
<body pos="0.0 2.0 0.096">
<joint axis="1 0 0" type="free"/>
<geom size="0.2 0.2 0.2" type="mesh" mesh="tetrahedron"/>
</body>
<body pos="0.0 2.0 0.289" euler="0.1 -0.1 45">
<joint axis="1 0 0" type="free"/>
<geom size="0.1 0.1 0.1" type="mesh" mesh="dodecahedron"/>
</body>
</worldbody>
</mujoco>
"""
def test_convex_convex(self):
"""Tests generic convex-convex collision."""
directory = epath.resource_path('mujoco.mjx')
assets = {
'meshes/tetrahedron.stl': (
directory / 'test_data' / 'meshes/tetrahedron.stl'
).read_bytes(),
'meshes/dodecahedron.stl': (
directory / 'test_data' / 'meshes/dodecahedron.stl'
).read_bytes(),
}
_, dx = _collide(self._CONVEX_CONVEX, assets=assets)
c = dx.contact
# Only one contact point for an edge contact.
self.assertLess(c.dist[0], 0)
np.testing.assert_array_less(0, c.dist[1:])
np.testing.assert_array_almost_equal(c.frame[0, 0], np.array([0, 0, 1]))
class BodyPairFilterTest(absltest.TestCase):
"""Tests that certain body pairs get filtered."""
_SELF_COLLISION = """
<mujoco>
<worldbody>
<body>
<joint type="free"/>
<geom size="0.2"/>
<geom size="0.2"/>
</body>
</worldbody>
</mujoco>
"""
def test_filter_self_collision(self):
"""Tests that self collisions get filtered."""
d, dx = _collide(self._SELF_COLLISION)
self.assertEqual(dx.contact.pos.shape[0], d.contact.pos.shape[0])
self.assertEqual(dx.contact.pos.shape[0], 0)
_PARENT_CHILD = """
<mujoco>
<worldbody>
<body>
<joint type="free"/>
<geom size="0.2"/>
<body pos="0.0 0.0 0.1">
<joint type="hinge"/>
<geom size="0.2"/>
</body>
</body>
</worldbody>
</mujoco>
"""
def test_filter_parent_child(self):
"""Tests that parent-child collisions get filtered."""
m = mujoco.MjModel.from_xml_string(self._PARENT_CHILD)
mx = mjx.device_put(m)
d = mujoco.MjData(m)
dx = mjx.device_put(d)
mujoco.mj_step(m, d)
collision_jit_fn = jax.jit(mjx.collision)
kinematics_jit_fn = jax.jit(mjx.kinematics)
dx = kinematics_jit_fn(mx, dx)
dx = collision_jit_fn(mx, dx)
self.assertEqual(dx.contact.pos.shape[0], d.contact.pos.shape[0])
self.assertEqual(dx.contact.pos.shape[0], 0)
def test_disable_filter_parent_child(self):
"""Tests that filterparent flag disables parent-child filtering."""
m = mujoco.MjModel.from_xml_string(self._PARENT_CHILD)
m.opt.disableflags |= mujoco.mjtDisableBit.mjDSBL_FILTERPARENT
mx = mjx.device_put(m)
d = mujoco.MjData(m)
dx = mjx.device_put(d)
mujoco.mj_step(m, d)
collision_jit_fn = jax.jit(mjx.collision)
kinematics_jit_fn = jax.jit(mjx.kinematics)
dx = kinematics_jit_fn(mx, dx)
dx = collision_jit_fn(mx, dx)
# one collision between parent-child spheres
self.assertEqual(dx.contact.pos.shape[0], d.contact.pos.shape[0])
self.assertEqual(dx.contact.pos.shape[0], 1)
class TopKContactTest(absltest.TestCase):
"""Tests top-k contacts."""
_CAPSULES = """
<mujoco>
<custom>
<numeric data="2" name="max_contact_points"/>
</custom>
<worldbody>
<body pos="0 0 0.54">
<joint axis="1 0 0" type="free"/>
<geom fromto="-0.4 0 0 0.4 0 0" size="0.05" type="capsule"/>
</body>
<body pos="0 0 0.54">
<joint axis="1 0 0" type="free"/>
<geom fromto="-0.4 0 0 0.4 0 0" size="0.05" type="capsule"/>
</body>
<body pos="0 0 0.54">
<joint axis="1 0 0" type="free"/>
<geom fromto="-0.4 0 0 0.4 0 0" size="0.05" type="capsule"/>
</body>
</worldbody>
</mujoco>
"""
def test_top_k_contacts(self):
m = mujoco.MjModel.from_xml_string(self._CAPSULES)
mx_top_k = mjx.device_put(m)
mx_all = mx_top_k.replace(
nnumeric=0, name_numericadr=np.array([]), numeric_data=np.array([])
)
d = mujoco.MjData(m)
dx = mjx.device_put(d)
collision_jit_fn = jax.jit(mjx.collision)
kinematics_jit_fn = jax.jit(mjx.kinematics)
dx = kinematics_jit_fn(mx_all, dx)
dx_all = collision_jit_fn(mx_all, dx)
dx_top_k = collision_jit_fn(mx_top_k, dx)
self.assertEqual(dx_all.ncon, 3)
self.assertEqual(dx_top_k.ncon, 2)
if __name__ == '__main__':
absltest.main()
+126
View File
@@ -0,0 +1,126 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Collision primitives."""
from typing import Tuple
import jax
from jax import numpy as jp
from mujoco.mjx._src import math
# pylint: disable=g-importing-member
from mujoco.mjx._src.collision_base import Contact
from mujoco.mjx._src.collision_base import GeomInfo
# pylint: enable=g-importing-member
def _plane_sphere(
plane_normal: jax.Array,
plane_pos: jax.Array,
sphere_pos: jax.Array,
radius: jax.Array,
) -> Tuple[jax.Array, jax.Array]:
"""Returns the penetration and contact point between a plane and sphere."""
cdist = jp.dot(sphere_pos - plane_pos, plane_normal)
dist = cdist - radius
pos = sphere_pos - plane_normal * (radius + 0.5 * dist)
return dist, pos
def plane_sphere(plane: GeomInfo, sphere: GeomInfo) -> Contact:
"""Calculates contact between a plane and a sphere."""
n = plane.mat[:, 2]
dist, pos = _plane_sphere(n, plane.pos, sphere.pos, sphere.size[0])
return jax.tree_map(
lambda x: jp.expand_dims(x, axis=0), (dist, pos, math.make_frame(n))
)
def plane_capsule(plane: GeomInfo, cap: GeomInfo) -> Contact:
"""Calculates two contacts between a capsule and a plane."""
n, axis = plane.mat[:, 2], cap.mat[:, 2]
# align contact frames with capsule axis
b, b_norm = math.normalize_with_norm(axis - n * jp.dot(n, axis))
y, z = jp.array([0.0, 1.0, 0.0]), jp.array([0.0, 0.0, 1.0])
b = jp.where(b_norm < 0.5, jp.where((-0.5 < n[1]) & (n[1] < 0.5), y, z), b)
frame = jp.array([[n, b, jp.cross(n, b)]])
segment = axis * cap.size[1]
contacts = []
for offset in [segment, -segment]:
dist, pos = _plane_sphere(n, plane.pos, cap.pos + offset, cap.size[0])
dist = jp.expand_dims(dist, axis=0)
pos = jp.expand_dims(pos, axis=0)
contacts.append((dist, pos, frame))
return jax.tree_map(lambda *x: jp.concatenate(x), *contacts)
def _sphere_sphere(
pos1: jax.Array, radius1: jax.Array, pos2: jax.Array, radius2: jax.Array
) -> Contact:
"""Returns the penetration, contact point, and normal between two spheres."""
n, dist = math.normalize_with_norm(pos2 - pos1)
n = jp.where(dist == 0.0, jp.array([1.0, 0.0, 0.0]), n)
dist = dist - (radius1 + radius2)
pos = pos1 + n * (radius1 + dist * 0.5)
return dist, pos, n
def sphere_sphere(s1: GeomInfo, s2: GeomInfo) -> Contact:
"""Calculates contact between two spheres."""
dist, pos, n = _sphere_sphere(s1.pos, s1.size[0], s2.pos, s2.size[0])
return jax.tree_map(
lambda x: jp.expand_dims(x, axis=0), (dist, pos, math.make_frame(n))
)
def sphere_capsule(sphere: GeomInfo, cap: GeomInfo) -> Contact:
"""Calculates one contact between a sphere and a capsule."""
axis, length = cap.mat[:, 2], cap.size[1]
segment = axis * length
pt = math.closest_segment_point(
cap.pos - segment, cap.pos + segment, sphere.pos
)
dist, pos, n = _sphere_sphere(sphere.pos, sphere.size[0], pt, cap.size[0])
return jax.tree_map(
lambda x: jp.expand_dims(x, axis=0), (dist, pos, math.make_frame(n))
)
def capsule_capsule(cap1: GeomInfo, cap2: GeomInfo) -> Contact:
"""Calculates one contact between two capsules."""
axis1, length1, axis2, length2 = (
cap1.mat[:, 2],
cap1.size[1],
cap2.mat[:, 2],
cap2.size[1],
)
seg1, seg2 = axis1 * length1, axis2 * length2
pt1, pt2 = math.closest_segment_to_segment_points(
cap1.pos - seg1,
cap1.pos + seg1,
cap2.pos - seg2,
cap2.pos + seg2,
)
radius1, radius2 = cap1.size[0], cap2.size[0]
dist, pos, n = _sphere_sphere(pt1, radius1, pt2, radius2)
return jax.tree_map(
lambda x: jp.expand_dims(x, axis=0), (dist, pos, math.make_frame(n))
)
# store ncon as function attributes
plane_sphere.ncon = 1
plane_capsule.ncon = 2
sphere_sphere.ncon = 1
sphere_capsule.ncon = 1
capsule_capsule.ncon = 1
+340
View File
@@ -0,0 +1,340 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Core non-smooth constraint functions."""
from typing import Tuple
import jax
from jax import numpy as jp
import mujoco
from mujoco.mjx._src import math
from mujoco.mjx._src import scan
from mujoco.mjx._src import support
# pylint: disable=g-importing-member
from mujoco.mjx._src.dataclasses import PyTreeNode
from mujoco.mjx._src.types import Contact
from mujoco.mjx._src.types import Data
from mujoco.mjx._src.types import DisableBit
from mujoco.mjx._src.types import EqType
from mujoco.mjx._src.types import JointType
from mujoco.mjx._src.types import Model
# pylint: enable=g-importing-member
import numpy as np
class _Efc(PyTreeNode):
J: jax.Array
R: jax.Array
aref: jax.Array
frictionloss: jax.Array
@classmethod
def zero(cls, m: Model) -> '_Efc':
z = jp.empty((0,))
return _Efc(J=jp.empty((0, m.nv)), R=z, aref=z, frictionloss=z)
def _kbi(
m: Model,
solref: jax.Array,
solimp: jax.Array,
pos: jax.Array,
) -> Tuple[jax.Array, jax.Array, jax.Array]:
"""Calculates stiffness, damping, and impedance of a constraint."""
timeconst, dampratio = solref
if not m.opt.disableflags & DisableBit.REFSAFE:
timeconst = jp.maximum(timeconst, 2 * m.opt.timestep) * (timeconst > 0)
dmin, dmax, width, mid, power = solimp
dmin = jp.clip(dmin, mujoco.mjMINIMP, mujoco.mjMAXIMP)
dmax = jp.clip(dmax, mujoco.mjMINIMP, mujoco.mjMAXIMP)
width = jp.maximum(0, width)
mid = jp.clip(mid, mujoco.mjMINIMP, mujoco.mjMAXIMP)
power = jp.maximum(1, power)
# See https://mujoco.readthedocs.io/en/latest/modeling.html#solver-parameters
k = 1 / (dmax * dmax * timeconst * timeconst * dampratio * dampratio)
b = 2 / (dmax * timeconst)
# TODO(robotics-simulation): check various solparam settings in model gen test
k = jp.where(dampratio <= 0, -dampratio / (dmax * dmax), k)
b = jp.where(timeconst <= 0, -timeconst / dmax, b)
imp_x = jp.abs(pos) / width
imp_a = (1.0 / jp.power(mid, power - 1)) * jp.power(imp_x, power)
imp_b = 1 - (1.0 / jp.power(1 - mid, power - 1)) * jp.power(1 - imp_x, power)
imp_y = jp.where(imp_x < mid, imp_a, imp_b)
imp = dmin + imp_y * (dmax - dmin)
imp = jp.clip(imp, dmin, dmax)
imp = jp.where(imp_x > 1.0, dmax, imp)
return k, b, imp # corresponds to K, B, I of efc_KBIP
def _instantiate_connect(m: Model, d: Data) -> _Efc:
"""Returns jacobians and supporting data for connect equality constraints."""
if (m.opt.disableflags & DisableBit.EQUALITY) or m.neq == 0:
return _Efc.zero(m)
connect_id = np.nonzero(m.eq_type == EqType.CONNECT)[0]
if connect_id.size == 0:
return _Efc.zero(m)
body1id, body2id = m.eq_obj1id[connect_id], m.eq_obj2id[connect_id]
data = m.eq_data[connect_id]
solref, solimp = m.eq_solref[connect_id], m.eq_solimp[connect_id]
def fn(data, id1, id2, solref, solimp):
anchor1, anchor2 = data[0:3], data[3:6]
# find global points
pos1 = d.xmat[id1] @ anchor1 + d.xpos[id1]
pos2 = d.xmat[id2] @ anchor2 + d.xpos[id2]
# compute position error
cpos = pos1 - pos2
# compute Jacobian difference (opposite of contact: 0 - 1)
jacp1, _ = support.jac(m, d, pos1, id1)
jacp2, _ = support.jac(m, d, pos2, id2)
j = (jacp1 - jacp2).T
# impedance, inverse constraint mass, reference acceleration
k, b, imp = _kbi(m, solref, solimp, math.norm(cpos))
invweight = m.body_invweight0[id1, 0] + m.body_invweight0[id2, 0]
r = jp.maximum(invweight * (1 - imp) / imp, mujoco.mjMINVAL).repeat(3)
aref = -b * (j @ d.qvel) - k * imp * cpos
return _Efc(J=j, R=r, aref=aref, frictionloss=jp.zeros_like(r))
efcs = jax.vmap(fn)(data, body1id, body2id, solref, solimp)
return jax.tree_map(jp.concatenate, efcs)
def _instantiate_weld(m: Model, d: Data) -> _Efc:
"""Returns jacobians and supporting data for connect weld constraints."""
if (m.opt.disableflags & DisableBit.EQUALITY) or m.neq == 0:
return _Efc.zero(m)
weld_id = np.nonzero(m.eq_type == EqType.WELD)[0]
if weld_id.size == 0:
return _Efc.zero(m)
body1id, body2id = m.eq_obj1id[weld_id], m.eq_obj2id[weld_id]
data = m.eq_data[weld_id]
solref, solimp = m.eq_solref[weld_id], m.eq_solimp[weld_id]
def fn(data, id1, id2, solref, solimp):
anchor1, anchor2 = data[0:3], data[3:6]
relpose, torquescale = data[6:10], data[10]
# find global points
pos1 = d.xmat[id1] @ anchor2 + d.xpos[id1]
pos2 = d.xmat[id2] @ anchor1 + d.xpos[id2]
# compute position error
cpos = pos1 - pos2
# compute Jacobian difference (opposite of contact: 0 - 1)
jacp1, jacr1 = support.jac(m, d, pos1, id1)
jacp2, jacr2 = support.jac(m, d, pos2, id2)
jacdifp = jacp1 - jacp2
jacdifr = (jacr1 - jacr2) * torquescale
# compute orientation error: neg(q1) * q0 * relpose (axis components only)
quat = math.quat_mul(d.xquat[id1], relpose)
quat1 = math.quat_inv(d.xquat[id2])
crot = math.quat_mul(quat1, quat)[1:] # copy axis components
# correct rotation Jacobian: 0.5 * neg(q1) * (jac0-jac1) * q0 * relpose
jac_fn = lambda j: math.quat_mul(math.quat_mul_axis(quat1, j), quat)[1:]
jacdifr = 0.5 * jax.vmap(jac_fn)(jacdifr)
j = jp.concatenate((jacdifp.T, jacdifr.T))
pos = jp.concatenate((cpos, crot))
# impedance, inverse constraint mass, reference acceleration
k, b, imp = _kbi(m, solref, solimp, math.norm(pos.at[3:].mul(torquescale)))
invweight = m.body_invweight0[id1] + m.body_invweight0[id2]
r = jp.maximum(invweight * (1 - imp) / imp, mujoco.mjMINVAL).repeat(3)
aref = -b * (j @ d.qvel) - k * imp * pos
return _Efc(J=j, R=r, aref=aref, frictionloss=jp.zeros_like(r))
efcs = jax.vmap(fn)(data, body1id, body2id, solref, solimp)
return jax.tree_map(jp.concatenate, efcs)
def _instantiate_friction(m: Model, d: Data) -> _Efc:
# TODO(robotics-team): implement _instantiate_friction
del d
return _Efc.zero(m)
def _instantiate_limit(m: Model, d: Data) -> _Efc:
"""Returns jacobians and supporting data for joint limits."""
if (m.opt.disableflags & DisableBit.LIMIT) or not m.jnt_limited.any():
return _Efc.zero(m)
def fn(jnt_typs, jnt_range, solref, solimp, margin, qpos, dofs, invweight0):
js, rs, arefs = [], [], []
qpos_i, dof_i = 0, 0
for i in range(len(jnt_typs)):
jnt_typ = JointType(jnt_typs[i])
if jnt_typ == JointType.FREE:
return None # omit constraint rows for free joints
elif jnt_typ == JointType.BALL:
axis, angle = math.quat_to_axis_angle(qpos[qpos_i : qpos_i + 4])
dist = jp.amax(jnt_range[i]) - angle
j = jp.sum(
jax.vmap(jp.multiply)(dofs[dof_i : dof_i + 3], -axis), axis=0
)
elif jnt_typ in (JointType.HINGE, JointType.SLIDE):
dist_min = qpos[qpos_i] - jnt_range[i, 0]
dist_max = jnt_range[i, 1] - qpos[qpos_i]
dist = jp.minimum(dist_min, dist_max)
j = dofs[dof_i] * ((dist_min < dist_max) * 2 - 1)
else:
raise RuntimeError(f'unrecognized joint type: {jnt_typ}')
dist = dist - margin[i]
k, b, imp = _kbi(m, solref[i], solimp[i], dist)
r = jp.maximum(invweight0[dof_i] * (1 - imp) / imp, mujoco.mjMINVAL)
aref = -b * (j @ d.qvel) - k * imp * dist
j, aref = j * (dist < 0), aref * (dist < 0)
js, rs, arefs = js + [j], rs + [r], arefs + [aref]
dof_i, qpos_i = dof_i + jnt_typ.dof_width(), qpos_i + jnt_typ.qpos_width()
return jp.stack(js), jp.stack(rs), jp.stack(arefs)
j, r, aref = scan.flat(
m,
fn,
'jjjjjqvv',
'jjj',
m.jnt_type,
m.jnt_range,
m.jnt_solref,
m.jnt_solimp,
m.jnt_margin,
d.qpos,
jp.eye(m.nv),
m.dof_invweight0,
)
return _Efc(J=j, R=r, aref=aref, frictionloss=jp.zeros_like(r))
def _instantiate_contact(m: Model, d: Data) -> _Efc:
"""Returns jacobians and supporitng data for contacts."""
if (m.opt.disableflags & DisableBit.CONTACT) or d.ncon == 0:
return _Efc.zero(m)
def fn(contact: Contact):
dist = contact.dist - contact.includemargin
k, b, imp = _kbi(m, contact.solref, contact.solimp, dist)
geom_bodyid = jp.array(m.geom_bodyid)
body1, body2 = geom_bodyid[contact.geom1], geom_bodyid[contact.geom2]
diff = support.jac_dif_pair(m, d, contact.pos, body1, body2)
t = m.body_invweight0[body1, 0] + m.body_invweight0[body2, 0]
# rotate Jacobian differences to contact frame
diff_con = contact.frame @ diff.T
# TODO(robotics-simulation): add support for other friction dimensions
# 4 pyramidal friction directions
js, rs = [], []
for diff_tan, friction in zip(diff_con[1:], contact.friction[:2]):
for f in (friction, -friction):
js.append(diff_con[0] + diff_tan * f)
rs.append((t + f * f * t) * 2 * f * f * (1 - imp) / imp)
j, r = jp.stack(js), jp.stack(rs)
r = jp.maximum(r, mujoco.mjMINVAL)
aref = -b * (j @ d.qvel) - k * imp * dist
mask_fn = jax.vmap(lambda x, mask=(dist < 0): x * mask)
j, aref = jax.tree_map(mask_fn, (j, aref))
return _Efc(J=j, R=r, aref=aref, frictionloss=jp.zeros_like(r))
return jax.tree_map(jp.concatenate, jax.vmap(fn)(d.contact))
def count_constraints(m: Model, d: Data) -> Tuple[int, int, int, int]:
"""Returns equality, friction, limit, and contact constraint counts."""
if m.opt.disableflags & DisableBit.CONSTRAINT:
return 0, 0, 0, 0
if m.opt.disableflags & DisableBit.EQUALITY:
ne = 0
else:
ne_weld = (m.eq_type == EqType.WELD).sum()
ne_connect = (m.eq_type == EqType.CONNECT).sum()
ne = ne_weld * 6 + ne_connect * 3
nf = 0
if (m.opt.disableflags & DisableBit.LIMIT) or not m.jnt_limited.any():
nl = 0
else:
nl = (m.jnt_type != JointType.FREE).sum()
if (m.opt.disableflags & DisableBit.CONTACT):
nc = 0
else:
nc = d.ncon * 4
return ne, nf, nl, nc
def make_constraint(m: Model, d: Data) -> Data:
"""Creates constraint jacobians and other supporting data."""
ns = sum(count_constraints(m, d)[:-1])
# TODO(robotics-simulation): make device_put set nefc/efc_address instead
d = d.tree_replace({'contact.efc_address': np.arange(ns, ns + d.ncon * 4, 4)})
if m.opt.disableflags & DisableBit.CONSTRAINT:
efc = _Efc.zero(m)
else:
efcs = (
_instantiate_connect(m, d),
_instantiate_weld(m, d),
_instantiate_friction(m, d),
_instantiate_limit(m, d),
_instantiate_contact(m, d),
)
efc = jax.tree_map(lambda *x: jp.concatenate(x), *efcs)
d = d.replace(
efc_J=efc.J,
efc_D=1 / efc.R,
efc_aref=efc.aref,
efc_frictionloss=efc.frictionloss,
nefc=efc.aref.shape[0],
)
return d
+144
View File
@@ -0,0 +1,144 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Tests for constraint functions."""
from absl.testing import absltest
from absl.testing import parameterized
import jax
from jax import numpy as jp
import mujoco
from mujoco import mjx
from mujoco.mjx._src import constraint
from mujoco.mjx._src import test_util
# pylint: disable=g-importing-member
from mujoco.mjx._src.types import DisableBit
# pylint: enable=g-importing-member
import numpy as np
def _assert_eq(a, b, name, step, fname, atol=1e-3, rtol=1e-3):
err_msg = f'mismatch: {name} at step {step} in {fname}'
np.testing.assert_allclose(a, b, err_msg=err_msg, atol=atol, rtol=rtol)
class ConstraintTest(parameterized.TestCase):
@parameterized.parameters(enumerate(test_util.TEST_FILES))
def testconstraints(self, seed, fname):
"""Test constraints."""
np.random.seed(seed)
# exclude convex.xml since convex contacts are not exactly equivalent
if fname == 'convex.xml':
return
m = test_util.load_test_file(fname)
d = mujoco.MjData(m)
mx = mjx.device_put(m)
dx = mjx.make_data(mx)
forward_jit_fn = jax.jit(mjx.forward)
# give the system a little kick to ensure we have non-identity rotations
d.qvel = np.random.random(m.nv)
for i in range(100):
dx = dx.replace(qpos=jax.device_put(d.qpos), qvel=jax.device_put(d.qvel))
mujoco.mj_step(m, d)
dx = forward_jit_fn(mx, dx)
nnz_filter = dx.efc_J.any(axis=1)
mj_efc_j = d.efc_J.reshape((-1, m.nv))
mjx_efc_j = dx.efc_J[nnz_filter]
_assert_eq(mj_efc_j, mjx_efc_j, 'efc_J', i, fname)
mjx_efc_d = dx.efc_D[nnz_filter]
_assert_eq(d.efc_D, mjx_efc_d, 'efc_D', i, fname)
mjx_efc_aref = dx.efc_aref[nnz_filter]
_assert_eq(d.efc_aref, mjx_efc_aref, 'efc_aref', i, fname)
mjx_efc_frictionloss = dx.efc_frictionloss[nnz_filter]
_assert_eq(
d.efc_frictionloss,
mjx_efc_frictionloss,
'efc_frictionloss',
i,
fname,
)
def test_disable_refsafe(self):
m = test_util.load_test_file('ant.xml')
timeconst = m.opt.timestep / 4.0 # timeconst < 2 * timestep
solimp = jp.array([timeconst, 1.0])
solref = jp.array([0.8, 0.99, 0.001, 0.2, 2])
pos = jp.ones(3)
m.opt.disableflags = m.opt.disableflags | DisableBit.REFSAFE
mx = mjx.device_put(m)
k, *_ = constraint._kbi(mx, solimp, solref, pos)
self.assertEqual(k, 1 / (0.99**2 * timeconst**2))
m.opt.disableflags = m.opt.disableflags & ~DisableBit.REFSAFE
mx = mjx.device_put(m)
k, *_ = constraint._kbi(mx, solimp, solref, pos)
self.assertEqual(k, 1 / (0.99**2 * (2 * m.opt.timestep) ** 2))
def test_disableconstraint(self):
m = test_util.load_test_file('ant.xml')
d = mujoco.MjData(m)
m.opt.disableflags = m.opt.disableflags & ~DisableBit.CONSTRAINT
mx, dx = mjx.device_put(m), mjx.device_put(d)
dx = constraint.make_constraint(mx, dx)
self.assertGreater(dx.efc_J.shape[0], 1)
m.opt.disableflags = m.opt.disableflags | DisableBit.CONSTRAINT
mx = mjx.device_put(m)
dx = constraint.make_constraint(mx, dx)
self.assertEqual(dx.efc_J.shape[0], 0)
def test_disable_equality(self):
m = test_util.load_test_file('weld.xml')
d = mujoco.MjData(m)
m.opt.disableflags = m.opt.disableflags | DisableBit.EQUALITY
mx, dx = mjx.device_put(m), mjx.device_put(d)
dx = constraint.make_constraint(mx, dx)
self.assertEqual(dx.efc_J.shape[0], 0)
def test_disable_contact(self):
m = test_util.load_test_file('ant.xml')
d = mujoco.MjData(m)
d.qpos[2] = 0.0
mujoco.mj_forward(m, d)
m.opt.disableflags = m.opt.disableflags & ~DisableBit.CONTACT
mx, dx = mjx.device_put(m), mjx.device_put(d)
dx = dx.tree_replace(
{'contact.frame': dx.contact.frame.reshape((-1, 3, 3))}
)
efc = constraint._instantiate_contact(mx, dx)
self.assertIsNotNone(efc)
m.opt.disableflags = m.opt.disableflags | DisableBit.CONTACT
mx, dx = mjx.device_put(m), mjx.device_put(d)
efc = constraint._instantiate_contact(mx, dx)
self.assertEqual(efc.J.shape[0], 0)
if __name__ == '__main__':
absltest.main()
+151
View File
@@ -0,0 +1,151 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Wrapper that automatically registers dataclass as a Jax PyTree."""
import copy
import dataclasses
import typing
from typing import Dict, Optional, Sequence, TypeVar
import jax
import numpy as np
_T = TypeVar('_T')
def dataclass(clz: _T) -> _T:
"""Wraps a dataclass with metadata for which fields are pytrees.
This is based off flax.struct.dataclass, but instead of using field
descriptors to specify which fields are pytrees, we follow a simple rule:
a leaf field is a pytree node if and only if it's a jax.Array
Args:
clz: the class to register as a dataclass
Returns:
the resulting dataclass, registered with Jax
"""
data_clz = dataclasses.dataclass(frozen=True)(clz)
meta_fields, data_fields = [], []
for field in dataclasses.fields(data_clz):
if any((
field.type is jax.Array,
dataclasses.is_dataclass(field.type),
jax.Array in typing.get_args(field.type),
)):
data_fields.append(field)
else:
meta_fields.append(field)
def replace(self, **updates):
""""Returns a new object replacing the specified fields with new values."""
return dataclasses.replace(self, **updates)
data_clz.replace = replace
def iterate_clz_with_keys(x):
# numpy arrays are not hashable, so convert them to tuples for jit cache
to_tup = lambda x: tuple(x) if len(x.shape) == 1 else tuple(map(to_tup, x))
def to_meta(field, obj):
val = getattr(obj, field.name)
return to_tup(val) if isinstance(val, np.ndarray) else val
def to_data(field, obj):
return (jax.tree_util.GetAttrKey(field.name), getattr(obj, field.name))
data = tuple(to_data(f, x) for f in data_fields)
meta = tuple(to_meta(f, x) for f in meta_fields)
return data, meta
def clz_from_iterable(meta, data):
def from_meta(field, meta):
if field.type is np.ndarray:
return (field.name, np.array(meta))
else:
return (field.name, meta)
from_data = lambda field, meta: (field.name, meta)
meta_args = tuple(from_meta(f, m) for f, m in zip(meta_fields, meta))
data_args = tuple(from_data(f, m) for f, m in zip(data_fields, data))
return data_clz(**dict(meta_args + data_args))
jax.tree_util.register_pytree_with_keys(
data_clz, iterate_clz_with_keys, clz_from_iterable
)
return data_clz
TNode = TypeVar('TNode', bound='PyTreeNode')
class PyTreeNode:
"""Base class for dataclasses that should act like a JAX pytree node.
This base class additionally avoids type checking errors when using PyType.
"""
def __init_subclass__(cls):
dataclass(cls)
def __init__(self, *args, **kwargs):
# stub for pytype
raise NotImplementedError
def replace(self: TNode, **overrides) -> TNode:
# stub for pytype
raise NotImplementedError
def tree_replace(
self, params: Dict[str, Optional[jax.typing.ArrayLike]]
) -> 'PyTreeNode':
new = self
for k, v in params.items():
new = _tree_replace(new, k.split('.'), v)
return new
def _tree_replace(
base: PyTreeNode,
attr: Sequence[str],
val: Optional[jax.typing.ArrayLike],
) -> PyTreeNode:
"""Sets attributes in a struct.dataclass with values."""
if not attr:
return base
# special case for List attribute
if len(attr) > 1 and isinstance(getattr(base, attr[0]), list):
lst = copy.deepcopy(getattr(base, attr[0]))
for i, g in enumerate(lst):
if not hasattr(g, attr[1]):
continue
v = val if not hasattr(val, '__iter__') else val[i]
lst[i] = _tree_replace(g, attr[1:], v)
return base.replace(**{attr[0]: lst})
if len(attr) == 1:
return base.replace(**{attr[0]: val})
return base.replace(
**{attr[0]: _tree_replace(getattr(base, attr[0]), attr[1:], val)}
)
+282
View File
@@ -0,0 +1,282 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Get and put mujoco data on/off device."""
import copy
import dataclasses
from typing import Any, Dict, Iterable, List, Union, overload
import warnings
import jax
from jax import numpy as jp
import mujoco
from mujoco.mjx._src import collision_driver
from mujoco.mjx._src import mesh
from mujoco.mjx._src import types
_MJ_TYPE_ATTR = {
mujoco.mjtBias: (mujoco.MjModel.actuator_biastype,),
mujoco.mjtDyn: (mujoco.MjModel.actuator_dyntype,),
mujoco.mjtEq: (mujoco.MjModel.eq_type,),
mujoco.mjtGain: (mujoco.MjModel.actuator_gaintype,),
mujoco.mjtTrn: (mujoco.MjModel.actuator_trntype,),
mujoco.mjtCone: (
mujoco.MjModel.opt,
mujoco.MjOption.cone,
),
mujoco.mjtIntegrator: (
mujoco.MjModel.opt,
mujoco.MjOption.integrator,
),
}
_TYPE_MAP = {
mujoco._structs._MjContactList: types.Contact, # pylint: disable=protected-access
mujoco.MjData: types.Data,
mujoco.MjModel: types.Model,
mujoco.MjOption: types.Option,
mujoco.MjStatistic: types.Statistic,
mujoco.mjtBias: types.BiasType,
mujoco.mjtCone: types.ConeType,
mujoco.mjtDisableBit: types.DisableBit,
mujoco.mjtDyn: types.DynType,
mujoco.mjtEq: types.EqType,
mujoco.mjtGain: types.GainType,
mujoco.mjtIntegrator: types.IntegratorType,
mujoco.mjtSolver: types.SolverType,
mujoco.mjtTrn: types.TrnType,
}
_TRANSFORMS = {
(types.Data, 'ximat'): lambda x: x.reshape(x.shape[:-1] + (3, 3)),
(types.Data, 'xmat'): lambda x: x.reshape(x.shape[:-1] + (3, 3)),
(types.Data, 'geom_xmat'): lambda x: x.reshape(x.shape[:-1] + (3, 3)),
(types.Model, 'actuator_trnid'): lambda x: x[:, 0],
(types.Contact, 'frame'): (
lambda x: x.reshape(x.shape[:-1] + (3, 3)) # pylint: disable=g-long-lambda
if x is not None and x.shape[0] else jp.zeros((0, 3, 3))
),
}
_INVERSE_TRANSFORMS = {
(types.Data, 'ximat'): lambda x: x.reshape(x.shape[:-2] + (9,)),
(types.Data, 'xmat'): lambda x: x.reshape(x.shape[:-2] + (9,)),
(types.Data, 'geom_xmat'): lambda x: x.reshape(x.shape[:-2] + (9,)),
(types.Contact, 'frame'): (
lambda x: x.reshape(x.shape[:-2] + (9,)) # pylint: disable=g-long-lambda
if x is not None and x.shape[0] else jp.zeros((0, 9))
),
}
_DERIVED = mesh.DERIVED.union(
# efc_J is dense in MJX, sparse in MJ. ignore for now.
{(types.Data, 'efc_J'), (types.Option, 'has_fluid_params')}
)
def _model_derived(value: mujoco.MjModel) -> Dict[str, Any]:
return {k: jax.device_put(v) for k, v in mesh.get(value).items()}
def _data_derived(value: mujoco.MjData) -> Dict[str, Any]:
return {'efc_J': jax.device_put(value.efc_J)}
def _option_derived(value: types.Option) -> Dict[str, Any]:
has_fluid = (
value.density > 0 or value.viscosity > 0 or (value.wind != 0.0).any()
)
return {'has_fluid_params': has_fluid}
def _validate(m: mujoco.MjModel):
"""Validates that an mjModel is compatible with MJX."""
if m.opt.solver not in set(types.SolverType):
name = mujoco.mjtSolver(m.opt.solver).name
warnings.warn(f'Solver {name} is not supported, reverting to CG.')
m.opt.solver = mujoco.mjtSolver.mjSOL_CG.value
# check enum types
for mj_type, attrs in _MJ_TYPE_ATTR.items():
val = m
for attr in attrs:
val = attr.fget(val) # pytype: disable=attribute-error
typs = set(val) if isinstance(val, Iterable) else {val}
unsupported_typs = typs - set(_TYPE_MAP[mj_type])
if unsupported_typs:
raise NotImplementedError(f'{unsupported_typs} not implemented.')
# check condim
if any(dim != 3 for dim in m.geom_condim) or any(
dim != 3 for dim in m.pair_dim
):
raise NotImplementedError('Only condim=3 is supported.')
# check collision geom types
candidate_set = collision_driver.collision_candidates(m)
for g1, g2, *_ in candidate_set:
g1, g2 = mujoco.mjtGeom(g1), mujoco.mjtGeom(g2)
if g1 == mujoco.mjtGeom.mjGEOM_PLANE and g2 in (
mujoco.mjtGeom.mjGEOM_PLANE,
mujoco.mjtGeom.mjGEOM_HFIELD,
):
# MuJoCo does not collide planes with other planes or hfields
continue
if collision_driver.get_collision_fn((g1, g2)) is None:
raise NotImplementedError(f'({g1}, {g2}) collisions not implemented.')
# TODO(erikfrey): warn for high solver iterations, nefc, etc.
# mjNDISABLE is not a DisableBit flag, so must be explicitly ignored
disablebit_members = set(mujoco.mjtDisableBit.__members__.values()) - {
mujoco.mjtDisableBit.mjNDISABLE}
unsupported_disable = disablebit_members - {
mujoco.mjtDisableBit(t.value) for t in types.DisableBit
}
for f in unsupported_disable:
if f & m.opt.disableflags:
warnings.warn(f'Ignoring disable flag {f.name}.')
# mjNENABLE is not an EnableBit flag, so must be explicitly ignored
unsupported_enable = set(mujoco.mjtEnableBit.__members__.values()) - {
mujoco.mjtEnableBit.mjNENABLE
}
for f in unsupported_enable:
if f & m.opt.enableflags:
warnings.warn(f'Ignoring enable flag {f.name}.')
@overload
def device_put(value: mujoco.MjData) -> types.Data:
...
@overload
def device_put(value: mujoco.MjModel) -> types.Model:
...
def device_put(value):
"""Places mujoco data onto a device.
Args:
value: a mujoco struct to transfer
Returns:
on-device MJX struct reflecting the input value
"""
clz = _TYPE_MAP.get(type(value))
if clz is None:
raise NotImplementedError(f'{type(value)} is not supported for device_put.')
if isinstance(value, mujoco.MjModel):
_validate(value) # type: ignore
init_kwargs = {}
for f in dataclasses.fields(clz): # type: ignore
if (clz, f.name) in _DERIVED:
continue
field_value = getattr(value, f.name)
if (clz, f.name) in _TRANSFORMS:
field_value = _TRANSFORMS[(clz, f.name)](field_value)
if f.type is jax.Array:
field_value = jax.device_put(field_value)
elif type(field_value) in _TYPE_MAP.keys():
field_value = device_put(field_value)
init_kwargs[f.name] = copy.copy(field_value)
derived_kwargs = {}
if isinstance(value, mujoco.MjModel):
derived_kwargs = _model_derived(value)
elif isinstance(value, mujoco.MjData):
derived_kwargs = _data_derived(value)
elif isinstance(value, mujoco.MjOption):
derived_kwargs = _option_derived(value)
return clz(**init_kwargs, **derived_kwargs) # type: ignore
@overload
def device_get_into(
result: Union[mujoco.MjData, List[mujoco.MjData]], value: types.Data
):
...
def device_get_into(result, value):
"""Transfers data off device into a mujoco MjData.
Data on device often has a batch dimension which adds (N,) to the beginning
of each array shape where N = batch size.
If result is a single MjData, arrays are copied over with the batch dimension
intact. If result is a list, the list must be length N and will be populated
with distinct MjData structs where the batch dimension is stripped.
Args:
result: struct (or list of structs) to transfer into
value: device value to transfer
Raises:
RuntimeError: if result length doesn't match data batch size
"""
value = jax.device_get(value)
if isinstance(result, list):
array_shapes = [s.shape for s in jax.tree_util.tree_flatten(value)[0]]
if any(len(s) < 1 or s[0] != array_shapes[0][0] for s in array_shapes):
raise ValueError('unrecognizable batch dimension in value')
batch_size = array_shapes[0][0]
if len(result) != batch_size:
raise ValueError(
f"result length ({len(result)}) doesn't match value batch size"
f' ({batch_size})'
)
for i in range(batch_size):
value_i = jax.tree_map(lambda x, i=i: x[i], value)
device_get_into(result[i], value_i)
else:
if isinstance(result, mujoco.MjData):
mujoco._functions._realloc_con_efc( # pylint: disable=protected-access
result, ncon=value.ncon, nefc=value.nefc
)
for f in dataclasses.fields(value): # type: ignore
if (type(value), f.name) in _DERIVED:
continue
field_value = getattr(value, f.name)
if (type(value), f.name) in _INVERSE_TRANSFORMS:
field_value = _INVERSE_TRANSFORMS[(type(value), f.name)](field_value)
if type(field_value) in _TYPE_MAP.values():
device_get_into(getattr(result, f.name), field_value)
continue
try:
setattr(result, f.name, field_value)
except AttributeError:
getattr(result, f.name)[:] = field_value
+184
View File
@@ -0,0 +1,184 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Tests for moving mujoco structs on and off device."""
import dataclasses
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 device
from mujoco.mjx._src import test_util
from mujoco.mjx._src import types
# pylint: disable=g-importing-member
from mujoco.mjx._src.dataclasses import PyTreeNode
# pylint: enable=g-importing-member
import numpy as np
def _assert_eq(testcase, a, b, attr=None, name=None):
if (type(a), attr) in device._DERIVED:
return
if attr:
a, b = getattr(a, attr), getattr(b, attr)
if isinstance(a, PyTreeNode):
for field in dataclasses.fields(a):
_assert_eq(testcase, a, b, field.name, type(a).__name__)
return
typ = {'Model': types.Model, 'Data': types.Data,
'Contact': types.Contact}.get(name)
if (typ, attr) in device._TRANSFORMS:
b = device._TRANSFORMS[(typ, attr)](b)
err_msg = f'mismatch: {attr} in {name}'
if not hasattr(b, 'shape') or not b.shape:
testcase.assertEqual(a, b, err_msg)
return
a, b = np.array(a), np.array(b)
np.testing.assert_allclose(a, b, err_msg=err_msg, atol=1e-8)
class DeviceTest(parameterized.TestCase):
@parameterized.parameters(test_util.TEST_FILES)
def testdevice_put(self, fname):
"""Test putting MjData and MjModel on device."""
m = test_util.load_test_file(fname)
# advance state to ensure non-zero fields
d = mujoco.MjData(m)
for _ in range(10):
mujoco.mj_step(m, d)
_assert_eq(self, mjx.device_put(d), d)
_assert_eq(self, mjx.device_put(m), m)
@parameterized.parameters(test_util.TEST_FILES)
def testdevice_get(self, fname):
"""Test getting MjData from a device."""
m = test_util.load_test_file(fname)
mx = device.device_put(m)
dx = mjx.make_data(mx)
d = mujoco.MjData(m)
device.device_get_into(d, dx)
_assert_eq(self, dx, d)
@parameterized.parameters(set(test_util.TEST_FILES) - {'convex.xml'})
def testdevice_get_batched(self, fname):
"""Test getting MjData from a device."""
m = test_util.load_test_file(fname)
mx = device.device_put(m)
batch_size = 32
# create mjx_data and batch it
dx = mjx.make_data(mx)
dx = jax.tree_map(
lambda x: jp.repeat(x, batch_size).reshape((batch_size,) + x.shape),
dx,
)
ds = [mujoco.MjData(m) for _ in range(batch_size - 1)]
with self.assertRaises(ValueError):
device.device_get_into(ds, dx)
ds = [mujoco.MjData(m) for _ in range(batch_size)]
device.device_get_into(ds, dx)
dx = jax.device_get(dx) # faster indexing for testing
for i in range(batch_size):
_assert_eq(self, jax.tree_map(lambda x, i=i: x[i], dx), ds[i])
class ValidateInputTest(absltest.TestCase):
def test_solver(self):
m = mujoco.MjModel.from_xml_string(
'<mujoco><option solver="Newton"/><worldbody/></mujoco>'
)
with self.assertWarns(UserWarning):
mx = mjx.device_put(m)
self.assertEqual(mx.opt.solver, mujoco.mjtSolver.mjSOL_CG)
def test_integrator(self):
m = mujoco.MjModel.from_xml_string(
'<mujoco><option integrator="implicit"/><worldbody/></mujoco>'
)
with self.assertRaises(NotImplementedError):
_ = mjx.device_put(m)
def test_cone(self):
m = mujoco.MjModel.from_xml_string(
'<mujoco><option cone="elliptic"/><worldbody/></mujoco>'
)
with self.assertRaises(NotImplementedError):
_ = mjx.device_put(m)
def test_trn(self):
m = test_util.load_test_file('ant.xml')
m.actuator_trntype[0] = mujoco.mjtTrn.mjTRN_SITE
with self.assertRaises(NotImplementedError):
_ = mjx.device_put(m)
def test_dyn(self):
m = test_util.load_test_file('ant.xml')
m.actuator_dyntype[0] = mujoco.mjtDyn.mjDYN_MUSCLE
with self.assertRaises(NotImplementedError):
_ = mjx.device_put(m)
def test_gain(self):
m = test_util.load_test_file('ant.xml')
m.actuator_gaintype[0] = mujoco.mjtGain.mjGAIN_MUSCLE
with self.assertRaises(NotImplementedError):
_ = mjx.device_put(m)
def test_bias(self):
m = test_util.load_test_file('ant.xml')
m.actuator_gaintype[0] = mujoco.mjtGain.mjGAIN_MUSCLE
with self.assertRaises(NotImplementedError):
_ = mjx.device_put(m)
def test_condim(self):
m = test_util.load_test_file('ant.xml')
for i in [1, 4, 6]:
m.geom_condim[0] = i
with self.assertRaises(NotImplementedError):
_ = mjx.device_put(m)
def test_geoms(self):
m = mujoco.MjModel.from_xml_string("""
<mujoco>
<worldbody>
<body>
<joint axis="1 0 0" type="free"/>
<geom size="0.2 0.2 0.2" type="box"/>
</body>
<body>
<joint axis="1 0 0" type="free"/>
<geom size="0.1 0.1" type="cylinder"/>
</body>
</worldbody>
</mujoco>
""")
with self.assertRaises(NotImplementedError):
_ = mjx.device_put(m)
if __name__ == '__main__':
absltest.main()
+356
View File
@@ -0,0 +1,356 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Forward step functions."""
import functools
from typing import Optional, Sequence
import jax
from jax import numpy as jp
import mujoco
from mujoco.mjx._src import collision_driver
from mujoco.mjx._src import constraint
from mujoco.mjx._src import math
from mujoco.mjx._src import passive
from mujoco.mjx._src import scan
from mujoco.mjx._src import smooth
from mujoco.mjx._src import solver
from mujoco.mjx._src import support
# pylint: disable=g-importing-member
from mujoco.mjx._src.types import BiasType
from mujoco.mjx._src.types import Data
from mujoco.mjx._src.types import DisableBit
from mujoco.mjx._src.types import DynType
from mujoco.mjx._src.types import GainType
from mujoco.mjx._src.types import IntegratorType
from mujoco.mjx._src.types import JointType
from mujoco.mjx._src.types import Model
from mujoco.mjx._src.types import SolverType
# pylint: enable=g-importing-member
import numpy as np
# RK4 tableau
_RK4_A = np.array([
[0.5, 0.0, 0.0],
[0.0, 0.5, 0.0],
[0.0, 0.0, 1.0],
])
_RK4_B = np.array([1.0 / 6.0, 1.0 / 3.0, 1.0 / 3.0, 1.0 / 6.0])
def named_scope(fn, name: str = ''):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
with jax.named_scope(name or getattr(fn, '__name__')):
res = fn(*args, **kwargs)
return res
return wrapper
@named_scope
def _position(m: Model, d: Data) -> Data:
"""Position-dependent computations."""
# TODO(robotics-simulation): tendon
d = smooth.kinematics(m, d)
d = smooth.com_pos(m, d)
d = smooth.crb(m, d)
d = smooth.factor_m(m, d, d.qM)
d = collision_driver.collision(m, d)
d = constraint.make_constraint(m, d)
d = smooth.transmission(m, d)
return d
@named_scope
def _velocity(m: Model, d: Data) -> Data:
"""Velocity-dependent computations."""
d = d.replace(actuator_velocity=d.actuator_moment @ d.qvel)
d = smooth.com_vel(m, d)
d = passive.passive(m, d)
d = smooth.rne(m, d)
return d
@named_scope
def _actuation(m: Model, d: Data) -> Data:
"""Actuation-dependent computations."""
if not m.nu or m.opt.disableflags & DisableBit.ACTUATION:
return d.replace(
act_dot=jp.zeros((m.na,)),
qfrc_actuator=jp.zeros((m.nv,)),
)
ctrl = d.ctrl
if not m.opt.disableflags & DisableBit.CLAMPCTRL:
ctrlrange = jp.where(
m.actuator_ctrllimited[:, None],
m.actuator_ctrlrange,
jp.array([-jp.inf, jp.inf]),
)
ctrl = jp.clip(ctrl, ctrlrange[:, 0], ctrlrange[:, 1])
# act_dot for stateful actuators
def get_act_dot(dyn_typ, dyn_prm, ctrl, act):
if dyn_typ == DynType.NONE:
act_dot = jp.array(0.0)
elif dyn_typ == DynType.INTEGRATOR:
act_dot = ctrl
elif dyn_typ == DynType.FILTER:
act_dot = (ctrl - act) / jp.clip(dyn_prm[0], mujoco.mjMINVAL)
else:
raise NotImplementedError(f'dyntype {dyn_typ.name} not implemented.')
return act_dot
act_dot = jp.zeros((m.na,))
if m.na:
act_dot = scan.flat(
m,
get_act_dot,
'uuua',
'a',
m.actuator_dyntype,
m.actuator_dynprm,
ctrl,
d.act,
group_by='u',
)
ctrl_act = ctrl
if m.na:
act_last_dim = d.act[m.actuator_actadr + m.actuator_actnum - 1]
ctrl_act = jp.where(m.actuator_actadr == -1, ctrl, act_last_dim)
def get_force(*args):
gain_t, gain_p, bias_t, bias_p, len_, vel, ctrl_act = args
typ, prm = GainType(gain_t), gain_p
if typ == GainType.FIXED:
gain = prm[0]
elif typ == GainType.AFFINE:
gain = prm[0] + prm[1] * len_ + prm[2] * vel
else:
raise RuntimeError(f'unrecognized gaintype {typ.name}.')
typ, prm = BiasType(bias_t), bias_p
bias = jp.array(0.0)
if typ == BiasType.AFFINE:
bias = prm[0] + prm[1] * len_ + prm[2] * vel
return gain * ctrl_act + bias
force = scan.flat(
m,
get_force,
'uuuuuuu',
'u',
m.actuator_gaintype,
m.actuator_gainprm,
m.actuator_biastype,
m.actuator_biasprm,
d.actuator_length,
d.actuator_velocity,
ctrl_act,
group_by='u',
)
forcerange = jp.where(
m.actuator_forcelimited[:, None],
m.actuator_forcerange,
jp.array([-jp.inf, jp.inf]),
)
force = jp.clip(force, forcerange[:, 0], forcerange[:, 1])
qfrc_actuator = d.actuator_moment.T @ force
# clamp qfrc_actuator
actfrcrange = jp.where(
m.jnt_actfrclimited[:, None],
m.jnt_actfrcrange,
jp.array([-jp.inf, jp.inf]),
)
ids = sum(
([i] * JointType(j).dof_width() for i, j in enumerate(m.jnt_type)), []
)
actfrcrange = jp.take(actfrcrange, jp.array(ids), axis=0)
qfrc_actuator = jp.clip(qfrc_actuator, actfrcrange[:, 0], actfrcrange[:, 1])
d = d.replace(act_dot=act_dot, qfrc_actuator=qfrc_actuator)
return d
@named_scope
def _acceleration(m: Model, d: Data) -> Data:
"""Add up all non-constraint forces, compute qacc_smooth."""
qfrc_applied = d.qfrc_applied + support.xfrc_accumulate(m, d)
qfrc_smooth = d.qfrc_passive - d.qfrc_bias + d.qfrc_actuator + qfrc_applied
qacc_smooth = smooth.solve_m(m, d, qfrc_smooth)
d = d.replace(qfrc_smooth=qfrc_smooth, qacc_smooth=qacc_smooth)
return d
@named_scope
def _integrate_pos(
jnt_typs: Sequence[str], qpos: jax.Array, qvel: jax.Array, dt: jax.Array
) -> jax.Array:
"""Integrate position given velocity."""
qs, qi, vi = [], 0, 0
for jnt_typ in jnt_typs:
if jnt_typ == JointType.FREE:
pos = qpos[qi : qi + 3] + dt * qvel[vi : vi + 3]
quat = math.quat_integrate(
qpos[qi + 3 : qi + 7], qvel[vi + 3 : vi + 6], dt
)
qs.append(jp.concatenate([pos, quat]))
qi, vi = qi + 7, vi + 6
elif jnt_typ == JointType.BALL:
quat = math.quat_integrate(qpos[qi : qi + 4], qvel[vi : vi + 3], dt)
qs.append(quat)
qi, vi = qi + 4, vi + 3
elif jnt_typ in (JointType.HINGE, JointType.SLIDE):
pos = qpos[qi] + dt * qvel[vi]
qs.append(pos[None])
qi, vi = qi + 1, vi + 1
else:
raise RuntimeError(f'unrecognized joint type: {jnt_typ}')
return jp.concatenate(qs) if qs else jp.empty((0,))
@named_scope
def _advance(
m: Model,
d: Data,
act_dot: jax.Array,
qacc: jax.Array,
qvel: Optional[jax.Array] = None,
) -> Data:
"""Advance state and time given activation derivatives and acceleration."""
act = d.act
if m.na:
act = d.act + act_dot * m.opt.timestep
actrange = jp.where(
m.actuator_actlimited[:, None],
m.actuator_actrange,
jp.array([-jp.inf, jp.inf]),
)
fn = lambda act, actrange: jp.clip(act, actrange[0], actrange[1])
act = scan.flat(m, fn, 'au', 'a', act, actrange, group_by='u')
# advance velocities
d = d.replace(qvel=d.qvel + qacc * m.opt.timestep)
# advance positions with qvel if given, d.qvel otherwise (semi-implicit)
qvel = d.qvel if qvel is None else qvel
integrate_fn = lambda *args: _integrate_pos(*args, dt=m.opt.timestep)
qpos = scan.flat(m, integrate_fn, 'jqv', 'q', m.jnt_type, d.qpos, qvel)
# advance time
time = d.time + m.opt.timestep
return d.replace(act=act, qpos=qpos, time=time)
@named_scope
def _euler(m: Model, d: Data) -> Data:
"""Euler integrator, semi-implicit in velocity."""
# integrate damping implicitly
qacc = d.qacc
if not m.opt.disableflags & DisableBit.EULERDAMP:
# TODO(robotics-simulation): can this be done with a smaller perf hit
mh = d.qM.at[m.dof_Madr].add(m.opt.timestep * m.dof_damping)
dh = smooth.factor_m(m, d, mh)
qfrc = d.qfrc_smooth + d.qfrc_constraint
qacc = smooth.solve_m(m, dh, qfrc)
return _advance(m, d, d.act_dot, qacc)
@named_scope
def _rungekutta4(m: Model, d: Data) -> Data:
"""Runge-Kutta explicit order 4 integrator."""
d_t0 = d
# pylint: disable=invalid-name
A, B = _RK4_A, _RK4_B
C = jp.tril(A).sum(axis=0) # C(i) = sum_j A(i,j)
T = d.time + C * m.opt.timestep
# pylint: enable=invalid-name
kqvel = d.qvel # intermediate RK solution
# RK solutions sum
qvel, qacc, act_dot = jax.tree_map(
lambda k: B[0] * k, (kqvel, d.qacc, d.act_dot)
)
integrate_fn = lambda *args: _integrate_pos(*args, dt=m.opt.timestep)
def f(carry, x):
qvel, qacc, act_dot, kqvel, d = carry
a, b, t = x # tableau numbers
dqvel, dqacc, dact_dot = jax.tree_map(
lambda k: a * k, (kqvel, d.qacc, d.act_dot)
)
# get intermediate RK solutions
kqpos = scan.flat(m, integrate_fn, 'jqv', 'q', m.jnt_type, d_t0.qpos, dqvel)
kact = d_t0.act + dact_dot * m.opt.timestep
kqvel = d_t0.qvel + dqacc * m.opt.timestep
d = d.replace(qpos=kqpos, qvel=kqvel, act=kact, time=t)
d = forward(m, d)
qvel += b * kqvel
qacc += b * d.qacc
act_dot += b * d.act_dot
return (qvel, qacc, act_dot, kqvel, d), None
abt = jp.vstack([jp.diag(A), B[1:4], T]).T
out, _ = jax.lax.scan(f, (qvel, qacc, act_dot, kqvel, d), abt, unroll=3)
qvel, qacc, act_dot, *_ = out
d = _advance(m, d_t0, act_dot, qacc, qvel)
return d
@named_scope
def forward(m: Model, d: Data) -> Data:
"""Forward dynamics."""
d = _position(m, d)
d = _velocity(m, d)
d = _actuation(m, d)
d = _acceleration(m, d)
if d.efc_J.size == 0:
d = d.replace(qacc=d.qacc_smooth)
return d
if m.opt.solver == SolverType.CG:
d = named_scope(solver.cg_solve)(m, d)
else:
raise NotImplementedError(f'solver {m.opt.solver} not implemented.')
return d
@named_scope
def step(m: Model, d: Data) -> Data:
"""Advance simulation."""
d = forward(m, d)
if m.opt.integrator == IntegratorType.EULER:
d = _euler(m, d)
elif m.opt.integrator == IntegratorType.RK4:
d = _rungekutta4(m, d)
else:
raise NotImplementedError(f'integrator {m.opt.integrator} not implemented.')
return d
+125
View File
@@ -0,0 +1,125 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Tests for forward functions."""
import itertools
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 forward
from mujoco.mjx._src import test_util
# pylint: disable=g-importing-member
from mujoco.mjx._src.types import DisableBit
# pylint: enable=g-importing-member
import numpy as np
def _assert_attr_eq(a, b, attr, step, fname, atol=1e-3, rtol=1e-3):
err_msg = f'mismatch: {attr} at step {step} in {fname}'
a, b = getattr(a, attr), getattr(b, attr)
np.testing.assert_allclose(a, b, err_msg=err_msg, atol=atol, rtol=rtol)
class ForwardTest(parameterized.TestCase):
@parameterized.parameters(enumerate(test_util.TEST_FILES))
def test_forward(self, seed, fname):
"""Test mujoco mj forward function matches mujoco_mjx forward function."""
if fname in ('weld.xml',):
return
np.random.seed(seed)
m = test_util.load_test_file(fname)
d = mujoco.MjData(m)
mx = mjx.device_put(m)
dx = mjx.make_data(mx)
forward_jit_fn = jax.jit(mjx.forward)
# give the system a little kick to ensure we have non-identity rotations
d.qvel = np.random.random(m.nv) * 0.05
for i in range(100):
qpos, qvel = d.qpos.copy(), d.qvel.copy()
mujoco.mj_step(m, d)
dx = forward_jit_fn(mx, dx.replace(qpos=qpos, qvel=qvel))
_assert_attr_eq(d, dx, 'qfrc_smooth', i, fname)
_assert_attr_eq(d, dx, 'qacc_smooth', i, fname)
@parameterized.parameters(itertools.product(test_util.TEST_FILES, (0, 1)))
def test_step(self, fname, integrator_type):
"""Test mujoco mj step matches mujoco_mjx step."""
if fname in (
'mixed_joint_pendulum.xml',
'ball_pendulum.xml',
'convex.xml',
'humanoid.xml',
'triple_pendulum.xml', # TODO(b/301485081)
'weld.xml',
):
# skip models with big constraint violations at step 0 or too slow to run
return
np.random.seed(integrator_type)
m = test_util.load_test_file(fname)
step_jit_fn = jax.jit(forward.step)
m.opt.integrator = integrator_type
int_typ = 'euler' if integrator_type == 0 else 'rk4'
test_name = f'{fname} - {int_typ}'
steps = 100 if int_typ == 'euler' else 30
dt = m.opt.timestep
m.opt.timestep = dt if int_typ == 'euler' else dt * 3
mx = mjx.device_put(m)
d = mujoco.MjData(m)
# give the system a little kick to ensure we have non-identity rotations
d.qvel = np.random.normal(m.nv) * 0.05
for i in range(steps):
# in order to avoid re-jitting, reuse the same mj_data shape
qpos, qvel = d.qpos, d.qvel
d = mujoco.MjData(m)
d.qpos, d.qvel = qpos, qvel
dx = mjx.device_put(d)
mujoco.mj_step(m, d)
dx = step_jit_fn(mx, dx)
_assert_attr_eq(d, dx, 'qpos', i, test_name, atol=1e-2)
_assert_attr_eq(d, dx, 'qvel', i, test_name, atol=1e-2)
_assert_attr_eq(d, dx, 'act', i, test_name)
_assert_attr_eq(d, dx, 'time', i, test_name)
def test_disable_eulerdamp(self):
m = test_util.load_test_file('ant.xml')
m.opt.disableflags = m.opt.disableflags | DisableBit.EULERDAMP
d = mujoco.MjData(m)
mx = mjx.device_put(m)
self.assertTrue((mx.dof_damping > 0).any())
dx = mjx.device_put(d)
dx = jax.jit(forward.forward)(mx, dx)
dx = dx.replace(qvel=jp.ones_like(dx.qvel), qacc=jp.ones_like(dx.qacc))
dx = jax.jit(forward._euler)(mx, dx)
np.testing.assert_allclose(dx.qvel, 1 + m.opt.timestep)
if __name__ == '__main__':
absltest.main()
+105
View File
@@ -0,0 +1,105 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Functions to initialize, load, or save data."""
from jax import numpy as jp
from mujoco.mjx._src import collision_driver
from mujoco.mjx._src import constraint
# pylint: disable=g-importing-member
from mujoco.mjx._src.types import Contact
from mujoco.mjx._src.types import Data
from mujoco.mjx._src.types import Model
# pylint: enable=g-importing-member
import numpy as np
def make_data(m: Model) -> Data:
"""Allocate and initialize Data."""
# create first d to get num contacts and nc
d = Data(
solver_niter=jp.array(0, dtype=jp.int32),
ne=0,
nf=0,
nl=0,
nefc=0,
ncon=0,
time=jp.zeros((), dtype=jp.float32),
qpos=m.qpos0,
qvel=jp.zeros(m.nv, dtype=jp.float32),
act=jp.zeros(m.na, dtype=jp.float32),
qacc_warmstart=jp.zeros(m.nv, dtype=jp.float32),
ctrl=jp.zeros(m.nu, dtype=jp.float32),
qfrc_applied=jp.zeros(m.nv, dtype=jp.float32),
xfrc_applied=jp.zeros((m.nbody, 6), dtype=jp.float32),
eq_active=jp.zeros(m.neq, dtype=jp.int32),
qacc=jp.zeros(m.nv, dtype=jp.float32),
act_dot=jp.zeros(m.na, dtype=jp.float32),
xpos=jp.zeros((m.nbody, 3), dtype=jp.float32),
xquat=jp.zeros((m.nbody, 4), dtype=jp.float32),
xmat=jp.zeros((m.nbody, 3, 3), dtype=jp.float32),
xipos=jp.zeros((m.nbody, 3), dtype=jp.float32),
ximat=jp.zeros((m.nbody, 3, 3), dtype=jp.float32),
xanchor=jp.zeros((m.njnt, 3), dtype=jp.float32),
xaxis=jp.zeros((m.njnt, 3), dtype=jp.float32),
geom_xpos=jp.zeros((m.ngeom, 3), dtype=jp.float32),
geom_xmat=jp.zeros((m.ngeom, 3, 3), dtype=jp.float32),
subtree_com=jp.zeros((m.nbody, 3), dtype=jp.float32),
cdof=jp.zeros((m.nv, 6), dtype=jp.float32),
cinert=jp.zeros((m.nbody, 10), dtype=jp.float32),
actuator_length=jp.zeros(m.nu, dtype=jp.float32),
actuator_moment=jp.zeros((m.nu, m.nv), dtype=jp.float32),
crb=jp.zeros((m.nbody, 10), dtype=jp.float32),
qM=jp.zeros(m.nM, dtype=jp.float32),
qLD=jp.zeros(m.nM, dtype=jp.float32),
qLDiagInv=jp.zeros(m.nv, dtype=jp.float32),
qLDiagSqrtInv=jp.zeros(m.nv, dtype=jp.float32),
contact=Contact.zero(),
efc_J=jp.zeros((), dtype=jp.float32),
efc_frictionloss=jp.zeros((), dtype=jp.float32),
efc_D=jp.zeros((), dtype=jp.float32),
actuator_velocity=jp.zeros(m.nu, dtype=jp.float32),
cvel=jp.zeros((m.nbody, 6), dtype=jp.float32),
cdof_dot=jp.zeros((m.nv, 6), dtype=jp.float32),
qfrc_bias=jp.zeros(m.nv, dtype=jp.float32),
qfrc_passive=jp.zeros(m.nv, dtype=jp.float32),
efc_aref=jp.zeros((), dtype=jp.float32),
actuator_force=jp.zeros(m.nu, dtype=jp.float32),
qfrc_actuator=jp.zeros(m.nv, dtype=jp.float32),
qfrc_smooth=jp.zeros(m.nv, dtype=jp.float32),
qacc_smooth=jp.zeros(m.nv, dtype=jp.float32),
qfrc_constraint=jp.zeros(m.nv, dtype=jp.float32),
qfrc_inverse=jp.zeros(m.nv, dtype=jp.float32),
efc_force=jp.zeros((), dtype=jp.float32),
)
# get contact data with correct shapes
ncon = collision_driver.ncon(m)
d = d.replace(contact=Contact.zero((ncon,)), ncon=ncon)
d = d.tree_replace({'contact.dim': 3 * np.ones(ncon)})
ne, nf, nl, nc = constraint.count_constraints(m, d)
d = d.replace(ne=ne, nf=nf, nl=nl, nefc=ne + nf + nl + nc)
ns = ne + nf + nl
d = d.tree_replace({'contact.efc_address': np.arange(ns, ns + ncon * 4, 4)})
d = d.replace(
efc_J=jp.zeros((d.nefc, m.nv), dtype=jp.float32),
efc_frictionloss=jp.zeros(d.nefc, dtype=jp.float32),
efc_D=jp.zeros(d.nefc, dtype=jp.float32),
efc_aref=jp.zeros(d.nefc, dtype=jp.float32),
efc_force=jp.zeros(d.nefc, dtype=jp.float32),
)
return d
+42
View File
@@ -0,0 +1,42 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Tests for io functions."""
from absl.testing import absltest
from absl.testing import parameterized
import jax
from mujoco import mjx
from mujoco.mjx._src import test_util
class IoTest(parameterized.TestCase):
@parameterized.parameters(test_util.TEST_FILES)
def test_make_data(self, fname):
"""Test that data created by make_data matches data returned by step."""
m = test_util.load_test_file(fname)
mx = mjx.device_put(m)
dx = mjx.make_data(mx)
dx_step = mjx.step(mx, dx)
_, dx_treedef = jax.tree_util.tree_flatten(dx)
_, dx_step_treedef = jax.tree_util.tree_flatten(dx_step)
self.assertEqual(dx_treedef, dx_step_treedef)
if __name__ == '__main__':
absltest.main()
+356
View File
@@ -0,0 +1,356 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Some useful math functions."""
from typing import Optional, Tuple, Union
import jax
from jax import numpy as jp
def norm(
x: jax.Array, axis: Optional[Union[Tuple[int, ...], int]] = None
) -> jax.Array:
"""Calculates a linalg.norm(x) that's safe for gradients at x=0.
Avoids a poorly defined gradient for jnp.linal.norm(0) see
https://github.com/google/jax/issues/3058 for details
Args:
x: A jnp.array
axis: The axis along which to compute the norm
Returns:
Norm of the array x.
"""
is_zero = jp.allclose(x, 0.0)
# temporarily swap x with ones if is_zero, then swap back
x = jp.where(is_zero, jp.ones_like(x), x)
n = jp.linalg.norm(x, axis=axis)
n = jp.where(is_zero, 0.0, n)
return n
def normalize_with_norm(
x: jax.Array, axis: Optional[Union[Tuple[int, ...], int]] = None
) -> Tuple[jax.Array, jax.Array]:
"""Normalizes an array.
Args:
x: A jnp.array
axis: The axis along which to compute the norm
Returns:
A tuple of (normalized array x, the norm).
"""
n = norm(x, axis=axis)
x = x / (n + 1e-6 * (n == 0.0))
return x, n
def normalize(
x: jax.Array, axis: Optional[Union[Tuple[int, ...], int]] = None
) -> jax.Array:
"""Normalizes an array.
Args:
x: A jnp.array
axis: The axis along which to compute the norm
Returns:
normalized array x
"""
return normalize_with_norm(x, axis=axis)[0]
def rotate(vec: jax.Array, quat: jax.Array) -> jax.Array:
"""Rotates a vector vec by a unit quaternion quat.
Args:
vec: (3,) a vector
quat: (4,) a quaternion
Returns:
ndarray(3) containing vec rotated by quat.
"""
if len(vec.shape) != 1:
raise ValueError('vec must have no batch dimensions.')
s, u = quat[0], quat[1:]
r = 2 * (jp.dot(u, vec) * u) + (s * s - jp.dot(u, u)) * vec
r = r + 2 * s * jp.cross(u, vec)
return r
def quat_inv(q: jp.ndarray) -> jp.ndarray:
"""Calculates the inverse of quaternion q.
Args:
q: (4,) quaternion [w, x, y, z]
Returns:
The inverse of q, where qmult(q, inv_quat(q)) = [1, 0, 0, 0].
"""
return q * jp.array([1, -1, -1, -1])
def quat_sub(u: jax.Array, v: jax.Array) -> jax.Array:
"""Subtracts two quaternions (u - v) as a 3D velocity."""
q = quat_mul(quat_inv(v), u)
axis, angle = quat_to_axis_angle(q)
return axis * angle
def quat_mul(u: jax.Array, v: jax.Array) -> jax.Array:
"""Multiplies two quaternions.
Args:
u: (4,) quaternion (w,x,y,z)
v: (4,) quaternion (w,x,y,z)
Returns:
A quaternion u * v.
"""
return jp.array([
u[0] * v[0] - u[1] * v[1] - u[2] * v[2] - u[3] * v[3],
u[0] * v[1] + u[1] * v[0] + u[2] * v[3] - u[3] * v[2],
u[0] * v[2] - u[1] * v[3] + u[2] * v[0] + u[3] * v[1],
u[0] * v[3] + u[1] * v[2] - u[2] * v[1] + u[3] * v[0],
])
def quat_mul_axis(q: jax.Array, axis: jax.Array) -> jax.Array:
"""Multiplies a quaternion and an axis.
Args:
q: (4,) quaternion (w,x,y,z)
axis: (3,) axis (x,y,z)
Returns:
A quaternion q * axis
"""
return jp.array([
-q[1] * axis[0] - q[2] * axis[1] - q[3] * axis[2],
q[0] * axis[0] + q[2] * axis[2] - q[3] * axis[1],
q[0] * axis[1] + q[3] * axis[0] - q[1] * axis[2],
q[0] * axis[2] + q[1] * axis[1] - q[2] * axis[0],
])
# TODO(erikfrey): benchmark this against brax's quat_to_3x3
def quat_to_mat(q: jax.Array) -> jax.Array:
"""Converts a quaternion into a 9-dimensional rotation matrix."""
q = jp.outer(q, q)
return jp.array([
[
q[0, 0] + q[1, 1] - q[2, 2] - q[3, 3],
2 * (q[1, 2] - q[0, 3]),
2 * (q[1, 3] + q[0, 2]),
],
[
2 * (q[1, 2] + q[0, 3]),
q[0, 0] - q[1, 1] + q[2, 2] - q[3, 3],
2 * (q[2, 3] - q[0, 1]),
],
[
2 * (q[1, 3] - q[0, 2]),
2 * (q[2, 3] + q[0, 1]),
q[0, 0] - q[1, 1] - q[2, 2] + q[3, 3],
],
])
def quat_to_axis_angle(q: jax.Array) -> Tuple[jax.Array, jax.Array]:
"""Converts a quaternion into axis and angle."""
axis, sin_a_2 = normalize_with_norm(q[1:])
angle = 2 * jp.arctan2(sin_a_2, q[0])
angle = jp.where(angle > jp.pi, angle - 2 * jp.pi, angle)
return axis, angle
def axis_angle_to_quat(axis: jax.Array, angle: jax.Array) -> jax.Array:
"""Provides a quaternion that describes rotating around axis by angle.
Args:
axis: (3,) axis (x,y,z)
angle: () float angle to rotate by
Returns:
A quaternion that rotates around axis by angle
"""
s, c = jp.sin(angle * 0.5), jp.cos(angle * 0.5)
return jp.insert(axis * s, 0, c)
def quat_integrate(q: jax.Array, v: jax.Array, dt: jax.Array) -> jax.Array:
"""Integrates a quaternion given angular velocity and dt."""
v, norm_ = normalize_with_norm(v)
angle = dt * norm_
q_res = axis_angle_to_quat(v, angle)
q_res = quat_mul(q, q_res)
return normalize(q_res)
def inert_mul(i: jax.Array, v: jax.Array) -> jax.Array:
"""Multiply inertia by motion, producing force.
Args:
i: (10,) inertia (inertia matrix, position, mass)
v: (6,) spatial motion
Returns:
resultant force
"""
tri_id = jp.array([[0, 3, 4], [3, 1, 5], [4, 5, 2]]) # cinert inr order
inr, pos, mass = i[tri_id], i[6:9], i[9]
ang = jp.dot(inr, v[:3]) + jp.cross(pos, v[3:])
vel = mass * v[3:] - jp.cross(pos, v[:3])
return jp.concatenate((ang, vel))
def transform_motion(vel: jax.Array, offset: jax.Array, rotmat: jax.Array):
"""Transform spatial motion.
Args:
vel: (6,) spatial motion (3 angular, 3 linear)
offset: (3,) translation
rotmat: (3, 3) rotation
Returns:
6d spatial velocity
"""
# TODO(robotics-simulation): are quaternions faster here
ang, vel = vel[:3], vel[3:]
vel = rotmat.T @ (vel - jp.cross(offset, ang))
ang = rotmat.T @ ang
return jp.concatenate([ang, vel])
def motion_cross(u, v):
"""Cross product of two motions.
Args:
u: (6,) spatial motion
v: (6,) spatial motion
Returns:
resultant spatial motion
"""
ang = jp.cross(u[:3], v[:3])
vel = jp.cross(u[3:], v[:3]) + jp.cross(u[:3], v[3:])
return jp.concatenate((ang, vel))
def motion_cross_force(v, f):
"""Cross product of a motion and force.
Args:
v: (6,) spatial motion
f: (6,) force
Returns:
resultant force
"""
ang = jp.cross(v[:3], f[:3]) + jp.cross(v[3:], f[3:])
vel = jp.cross(v[:3], f[3:])
return jp.concatenate((ang, vel))
def orthogonals(a: jax.Array) -> Tuple[jax.Array, jax.Array]:
"""Returns orthogonal vectors `b` and `c`, given a vector `a`."""
y, z = jp.array([0, 1, 0]), jp.array([0, 0, 1])
b = jp.where((-0.5 < a[1]) & (a[1] < 0.5), y, z)
b = b - a * a.dot(b)
# normalize b. however if a is a zero vector, zero b as well.
b = normalize(b) * jp.any(a)
return b, jp.cross(a, b)
def make_frame(a: jax.Array) -> jax.Array:
"""Makes a right-handed 3D frame given a direction."""
a = normalize(a)
b, c = orthogonals(a)
return jp.array([a, b, c])
# Geometry.
def closest_segment_point(
a: jax.Array, b: jax.Array, pt: jax.Array
) -> jax.Array:
"""Returns the closest point on the a-b line segment to a point pt."""
ab = b - a
t = jp.dot(pt - a, ab) / (jp.dot(ab, ab) + 1e-6)
return a + jp.clip(t, 0.0, 1.0) * ab
def closest_segment_point_and_dist(
a: jax.Array, b: jax.Array, pt: jax.Array
) -> Tuple[jax.Array, jax.Array]:
"""Returns closest point on the line segment and the distance squared."""
closest = closest_segment_point(a, b, pt)
dist = (pt - closest).dot(pt - closest)
return closest, dist
def closest_segment_to_segment_points(
a0: jax.Array, a1: jax.Array, b0: jax.Array, b1: jax.Array
) -> Tuple[jax.Array, jax.Array]:
"""Returns closest points between two line segments."""
# Gets the closest segment points by first finding the closest points
# between two lines. Points are then clipped to be on the line segments
# and edge cases with clipping are handled.
dir_a, len_a = normalize_with_norm(a1 - a0)
dir_b, len_b = normalize_with_norm(b1 - b0)
# Segment mid-points.
half_len_a = len_a * 0.5
half_len_b = len_b * 0.5
a_mid = a0 + dir_a * half_len_a
b_mid = b0 + dir_b * half_len_b
# Translation between two segment mid-points.
trans = a_mid - b_mid
# Parametrize points on each line as follows:
# point_on_a = a_mid + t_a * dir_a
# point_on_b = b_mid + t_b * dir_b
# and analytically minimize the distance between the two points.
dira_dot_dirb = dir_a.dot(dir_b)
dira_dot_trans = dir_a.dot(trans)
dirb_dot_trans = dir_b.dot(trans)
denom = 1 - dira_dot_dirb * dira_dot_dirb
orig_t_a = (-dira_dot_trans + dira_dot_dirb * dirb_dot_trans) / (denom + 1e-6)
orig_t_b = dirb_dot_trans + orig_t_a * dira_dot_dirb
t_a = jp.clip(orig_t_a, -half_len_a, half_len_a)
t_b = jp.clip(orig_t_b, -half_len_b, half_len_b)
best_a = a_mid + dir_a * t_a
best_b = b_mid + dir_b * t_b
# Resolve edge cases where both closest points are clipped to the segment
# endpoints by recalculating the closest segment points for the current
# clipped points, and then picking the pair of points with smallest
# distance. An example of this edge case is when lines intersect but line
# segments don't.
new_a, d1 = closest_segment_point_and_dist(a0, a1, best_b)
new_b, d2 = closest_segment_point_and_dist(b0, b1, best_a)
best_a = jp.where(d1 < d2, new_a, best_a)
best_b = jp.where(d1 < d2, best_b, new_b)
return best_a, best_b
+202
View File
@@ -0,0 +1,202 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Tests for math."""
from absl.testing import absltest
from absl.testing import parameterized
import jax.numpy as jp
from mujoco.mjx._src import math
import numpy as np
def _get_rand_point(seed=None):
if seed is not None:
np.random.seed(seed)
verts = np.random.randn(1, 3)
return verts[0, :]
def _get_rand_line_segment(seed=None):
if seed is not None:
np.random.seed(seed)
verts = np.random.randn(2, 3)
return verts[0, :], verts[1, :]
def _get_rand_unit(seed: int):
np.random.seed(seed)
theta = np.random.random(1) * 2 * np.pi
a = (np.random.random(1) - 0.5) * 2.0
phi = np.arccos(a)
x = np.sin(phi) * np.cos(theta)
y = np.sin(phi) * np.sin(theta)
z = np.cos(phi)
return jp.array([x, y, z]).squeeze()
class OrthoganalsTest(parameterized.TestCase):
"""Tests the orthogonals function."""
@parameterized.parameters(range(30))
def test_orthogonals(self, i):
a = _get_rand_unit(i)
b, c = math.orthogonals(a)
np.testing.assert_almost_equal(jp.linalg.norm(a), 1)
np.testing.assert_almost_equal(jp.linalg.norm(b), 1)
np.testing.assert_almost_equal(jp.linalg.norm(c), 1)
self.assertAlmostEqual(np.abs(a.dot(b)), 0, 6)
self.assertAlmostEqual(np.abs(b.dot(c)), 0, 6)
self.assertAlmostEqual(np.abs(a.dot(c)), 0, 6)
def _minimize(fn, sample_fn, lb, ub, tol, max_iter=20, seed=42):
"""Minimize a function using the cross-entropy method."""
assert lb.shape == ub.shape, "bounds need to have the same shape"
np.random.seed(seed)
i, n = 0, 1_000
mu = (ub + lb) * 0.5
sigma = (ub - lb) * 0.5
size = lb.shape[0]
val, prev_val = fn(mu), None
while prev_val is None or np.abs(val - prev_val) > tol:
params = sample_fn(mu, sigma, n, size, lb, ub)
vals = np.array([fn(p) for p in params])
if val < vals.min(): # early exit
return mu
idx = vals.argsort()
best_idx = idx[: int(n * 0.05)]
mu = params[best_idx].mean(axis=0)
sigma = params[best_idx].std(axis=0) + 1e-10
prev_val = val
val = fn(mu)
i += 1
if i == max_iter:
break
return mu
def _closest_segment_to_segment_points(a0, a1, b0, b1):
dir_a = a1 - a0
len_a = np.sqrt(dir_a.dot(dir_a))
half_len_a = len_a / 2
dir_a = dir_a / len_a
dir_b = b1 - b0
len_b = np.sqrt(dir_b.dot(dir_b))
half_len_b = len_b / 2
dir_b = dir_b / len_b
a_mid = a0 + dir_a * half_len_a
b_mid = b0 + dir_b * half_len_b
# Parametrize both line segments.
def fn(t):
best_a = a_mid + dir_a * t[0]
best_b = b_mid + dir_b * t[1]
return (best_a - best_b).dot(best_a - best_b)
def sample_fn(mu, sigma, n, size, lb, ub):
params = np.random.normal(mu, sigma, size=(n, size))
params = np.clip(params, lb, ub)
return params
lb = np.array([-half_len_a, -half_len_b])
ub = np.array([half_len_a, half_len_b])
ta, tb = _minimize(fn, sample_fn, lb, ub, tol=1e-4)
best_a = a_mid + dir_a * ta
best_b = b_mid + dir_b * tb
return best_a, best_b
class ClosestSegmentSegmentPointsTest(parameterized.TestCase):
"""Tests for closest segment-to-segment points."""
def test_closest_segments_points(self):
a0 = jp.array([0.73432405, 0.12372768, 0.20272314])
a1 = jp.array([1.10600128, 0.88555209, 0.65209485])
b0 = jp.array([0.85599262, 0.61736299, 0.9843583])
b1 = jp.array([1.84270939, 0.92891793, 1.36343326])
best_a, best_b = math.closest_segment_to_segment_points(a0, a1, b0, b1)
self.assertSequenceAlmostEqual(best_a, [1.09063, 0.85404, 0.63351], 5)
self.assertSequenceAlmostEqual(best_b, [0.99596, 0.66156, 1.03813], 5)
def test_intersecting_segments(self):
"""Tests segments that intersect."""
a0, a1 = jp.array([0.0, 0.0, -1.0]), jp.array([0.0, 0.0, 1.0])
b0, b1 = jp.array([-1.0, 0.0, 0.0]), jp.array([1.0, 0.0, 0.0])
best_a, best_b = math.closest_segment_to_segment_points(a0, a1, b0, b1)
self.assertSequenceAlmostEqual(best_a, [0.0, 0.0, 0.0], 5)
self.assertSequenceAlmostEqual(best_b, [0.0, 0.0, 0.0], 5)
def test_intersecting_lines(self):
"""Tests that intersecting lines get clipped."""
a0, a1 = jp.array([0.2, 0.2, 0.0]), jp.array([1.0, 1.0, 0.0])
b0, b1 = jp.array([0.2, 0.4, 0.0]), jp.array([1.0, 2.0, 0.0])
best_a, best_b = math.closest_segment_to_segment_points(a0, a1, b0, b1)
self.assertSequenceAlmostEqual(best_a, [0.3, 0.3, 0.0], 2)
self.assertSequenceAlmostEqual(best_b, [0.2, 0.4, 0.0], 2)
def test_parallel_segments(self):
"""Tests that parallel segments have closest points at the midpoint."""
a0, a1 = jp.array([0.0, 0.0, -1.0]), jp.array([0.0, 0.0, 1.0])
b0, b1 = jp.array([1.0, 0.0, -1.0]), jp.array([1.0, 0.0, 1.0])
best_a, best_b = math.closest_segment_to_segment_points(a0, a1, b0, b1)
self.assertSequenceAlmostEqual(best_a, [0.0, 0.0, 0.0], 5)
self.assertSequenceAlmostEqual(best_b, [1.0, 0.0, 0.0], 5)
def test_parallel_offset_segments(self):
"""Tests that offset parallel segments are close at segment endpoints."""
a0, a1 = jp.array([0.0, 0.0, -1.0]), jp.array([0.0, 0.0, 1.0])
b0, b1 = jp.array([1.0, 0.0, 1.0]), jp.array([1.0, 0.0, 3.0])
best_a, best_b = math.closest_segment_to_segment_points(a0, a1, b0, b1)
self.assertSequenceAlmostEqual(best_a, [0.0, 0.0, 1.0], 5)
self.assertSequenceAlmostEqual(best_b, [1.0, 0.0, 1.0], 5)
def test_zero_length_segments(self):
"""Test that zero length segments don't return NaNs."""
a0, a1 = jp.array([0.0, 0.0, -1.0]), jp.array([0.0, 0.0, -1.0])
b0, b1 = jp.array([1.0, 0.0, 0.1]), jp.array([1.0, 0.0, 0.1])
best_a, best_b = math.closest_segment_to_segment_points(a0, a1, b0, b1)
self.assertSequenceAlmostEqual(best_a, [0.0, 0.0, -1.0], 5)
self.assertSequenceAlmostEqual(best_b, [1.0, 0.0, 0.1], 5)
def test_overlapping_segments(self):
"""Tests that perfectly overlapping segments intersect at the midpoints."""
a0, a1 = jp.array([0.0, 0.0, -1.0]), jp.array([0.0, 0.0, 1.0])
b0, b1 = jp.array([0.0, 0.0, -1.0]), jp.array([0.0, 0.0, 1.0])
best_a, best_b = math.closest_segment_to_segment_points(a0, a1, b0, b1)
self.assertSequenceAlmostEqual(best_a, [0.0, 0.0, 0.0], 5)
self.assertSequenceAlmostEqual(best_b, [0.0, 0.0, 0.0], 5)
params = list(zip(np.repeat(np.arange(10), 10), np.tile(np.arange(10), 10)))
@parameterized.parameters(*params)
def test_closest_segment_to_segment_points(self, i, j):
a0, a1 = _get_rand_line_segment(i)
b0, b1 = _get_rand_line_segment(j)
expected = _closest_segment_to_segment_points(a0, a1, b0, b1)
ans = math.closest_segment_to_segment_points(a0, a1, b0, b1)
expected_dist = (expected[0] - expected[1]).dot(expected[0] - expected[1])
test_dist = (ans[0] - ans[1]).dot(ans[0] - ans[1])
self.assertAlmostEqual(expected_dist, test_dist, 4)
if __name__ == "__main__":
absltest.main()
+232
View File
@@ -0,0 +1,232 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Mesh processing."""
import itertools
from typing import Dict, Optional, Sequence, Tuple
import mujoco
# pylint: disable=g-importing-member
from mujoco.mjx._src.types import GeomType
from mujoco.mjx._src.types import Model
# pylint: enable=g-importing-member
import numpy as np
from scipy import spatial
import trimesh
_BOX_CORNERS = list(itertools.product((-1, 1), (-1, 1), (-1, 1)))
# pyformat: disable
# Rectangular box faces using a counter-clockwise winding order convention.
_BOX_FACES = [
0, 4, 5, 1, # left
0, 2, 6, 4, # bottom
6, 7, 5, 4, # front
2, 3, 7, 6, # right
1, 5, 7, 3, # top
0, 1, 3, 2, # back
]
# pyformat: enable
_MAX_HULL_FACE_VERTICES = 20
_CONVEX_CACHE: Dict[Tuple[int, int], Dict[str, np.ndarray]] = {}
_DERIVED_ARGS = [
'geom_convex_face',
'geom_convex_vert',
'geom_convex_edge',
'geom_convex_facenormal',
]
DERIVED = {(Model, d) for d in _DERIVED_ARGS}
def _box(size: np.ndarray):
"""Creates a mesh for a box with rectangular faces."""
box_corners = np.array(_BOX_CORNERS)
vert = box_corners * size.reshape(-1, 3)
face = np.array([_BOX_FACES]).reshape(-1, 4)
return vert, face
def _get_face_norm(vert: np.ndarray, face: np.ndarray) -> np.ndarray:
"""Calculates face normals given vertices and face indexes."""
assert len(vert.shape) == 2 and len(face.shape) == 2, (
f'vert and face should have dim of 2, got {len(vert.shape)} and '
f'{len(face.shape)}'
)
face_vert = vert[face, :]
# use CCW winding order convention
edge0 = face_vert[:, 1, :] - face_vert[:, 0, :]
edge1 = face_vert[:, -1, :] - face_vert[:, 0, :]
face_norm = np.cross(edge0, edge1)
face_norm = face_norm / np.linalg.norm(face_norm, axis=1).reshape((-1, 1))
return face_norm
def _get_unique_edges(vert: np.ndarray, face: np.ndarray) -> np.ndarray:
"""Returns unique edges.
Args:
vert: (n_vert, 3) vertices
face: (n_face, n_vert) face index array
Returns:
edges: tuples of vertex indexes for each edge
"""
r_face = np.roll(face, 1, axis=1)
edges = np.concatenate(np.array([face, r_face]).T)
# do a first pass to remove duplicates
edges.sort(axis=1)
edges = np.unique(edges, axis=0)
edges = edges[edges[:, 0] != edges[:, 1]] # get rid of edges from padded face
# get normalized edge directions
edge_vert = vert.take(edges, axis=0)
edge_dir = edge_vert[:, 0] - edge_vert[:, 1]
norms = np.sqrt(np.sum(edge_dir**2, axis=1))
edge_dir = edge_dir / norms.reshape((-1, 1))
# get the first unique edge for all pairwise comparisons
diff1 = edge_dir[:, None, :] - edge_dir[None, :, :]
diff2 = edge_dir[:, None, :] + edge_dir[None, :, :]
matches = (np.linalg.norm(diff1, axis=-1) < 1e-6) | (
np.linalg.norm(diff2, axis=-1) < 1e-6
)
matches = np.tril(matches).sum(axis=-1)
unique_edge_idx = np.where(matches == 1)[0]
return edges[unique_edge_idx]
def _convex_hull_2d(points: np.ndarray, normal: np.ndarray) -> np.ndarray:
"""Calculates the convex hull for a set of points on a plane."""
# project points onto the closest axis plane
best_axis = np.abs(np.eye(3).dot(normal)).argmax()
axis = np.eye(3)[best_axis]
d = points.dot(axis).reshape((-1, 1))
axis_points = points - d * axis
axis_points = axis_points[:, list({0, 1, 2} - {best_axis})]
# get the polygon face, and make the points ccw wrt the face normal
c = spatial.ConvexHull(axis_points)
order_ = np.where(axis.dot(normal) > 0, 1, -1)
order_ *= np.where(best_axis == 1, -1, 1)
hull_point_idx = c.vertices[::order_]
assert (axis_points - c.points).sum() == 0
return hull_point_idx
def _merge_coplanar(tm: trimesh.Trimesh) -> np.ndarray:
"""Merges coplanar facets."""
if not tm.facets:
return tm.faces.copy() # no facets
if not tm.faces.shape[0]:
raise ValueError('Mesh has no faces.')
# Get faces.
face_idx = set(range(tm.faces.shape[0])) - set(np.concatenate(tm.facets))
face_idx = np.array(list(face_idx))
faces = tm.faces[face_idx] if face_idx.shape[0] > 0 else np.array([])
# Get facets.
facets = []
for i, facet in enumerate(tm.facets):
point_idx = np.unique(tm.faces[facet])
points = tm.vertices[point_idx]
normal = tm.facets_normal[i]
# convert triangulated facet to a polygon
hull_point_idx = _convex_hull_2d(points, normal)
face = point_idx[hull_point_idx]
# resize faces that exceed max polygon vertices
every = face.shape[0] // _MAX_HULL_FACE_VERTICES + 1
face = face[::every]
facets.append(face)
# Pad facets so that they can be stacked.
max_len = max(f.shape[0] for f in facets) if facets else faces.shape[1]
assert max_len <= _MAX_HULL_FACE_VERTICES
for i, f in enumerate(facets):
if f.shape[0] < max_len:
f = np.pad(f, (0, max_len - f.shape[0]), 'edge')
facets[i] = f
if not faces.shape[0]:
assert facets
return np.array(facets) # no faces, return facets
# Merge faces and facets.
faces = np.pad(faces, ((0, 0), (0, max_len - faces.shape[1])), 'edge')
return np.concatenate([faces, facets])
def _get_faces_verts(
m: mujoco.MjModel,
) -> Tuple[Sequence[np.ndarray], Sequence[np.ndarray]]:
"""Extracts mesh faces and vertices from MjModel."""
verts, faces = [], []
for i in range(m.nmesh):
last = (i + 1) >= m.nmesh
face_start = m.mesh_faceadr[i]
face_end = m.mesh_faceadr[i + 1] if not last else m.mesh_face.shape[0]
face = m.mesh_face[face_start:face_end]
faces.append(face)
vert_start = m.mesh_vertadr[i]
vert_end = m.mesh_vertadr[i + 1] if not last else m.mesh_vert.shape[0]
vert = m.mesh_vert[vert_start:vert_end]
verts.append(vert)
return verts, faces
def _geom_mesh_kwargs(
vert: np.ndarray, face: np.ndarray
) -> Dict[str, np.ndarray]:
"""Generates convex mesh attributes for mjx.Model."""
tm = trimesh.Trimesh(vertices=vert, faces=face)
tm_convex = trimesh.convex.convex_hull(tm)
vert = np.array(tm_convex.vertices)
face = _merge_coplanar(tm_convex)
return {
'geom_convex_face': face,
'geom_convex_vert': vert,
'geom_convex_edge': _get_unique_edges(vert, face),
'geom_convex_facenormal': _get_face_norm(vert, face),
}
def get(m: mujoco.MjModel) -> Dict[str, Sequence[Optional[np.ndarray]]]:
"""Derives geom mesh attributes for mjx.Model from MjModel."""
kwargs = {k: [] for k in _DERIVED_ARGS}
verts, faces = _get_faces_verts(m)
for geomid in range(m.ngeom):
dataid = m.geom_dataid[geomid]
typ = m.geom_type[geomid]
if typ == GeomType.BOX:
vert, face = _box(m.geom_size[geomid])
elif dataid >= 0:
vert, face = verts[dataid], faces[dataid]
else:
kwargs = {k: kwargs[k] + [None] for k in _DERIVED_ARGS}
continue
key = (hash(vert.data.tobytes()), hash(face.data.tobytes()))
if key not in _CONVEX_CACHE:
_CONVEX_CACHE[key] = _geom_mesh_kwargs(vert, face)
kwargs = {k: kwargs[k] + [_CONVEX_CACHE[key][k]] for k in _DERIVED_ARGS}
return kwargs
+119
View File
@@ -0,0 +1,119 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Tests for mesh.py."""
from absl.testing import absltest
from mujoco.mjx._src import mesh
import numpy as np
class GeomMeshKwargsTest(absltest.TestCase):
def test_pyramid(self):
"""Tests that a triangulated pyramid converts to merged coplanar faces."""
vert = np.array([
[-0.025, 0.05, 0.05],
[-0.025, -0.05, -0.05],
[-0.025, -0.05, 0.05],
[-0.025, 0.05, -0.05],
[0.075, 0.0, 0.0],
])
face = np.array(
[[0, 1, 2], [0, 3, 1], [0, 4, 3], [0, 2, 4], [2, 1, 4], [1, 3, 4]]
)
h = mesh._geom_mesh_kwargs(vert, face)
# get index of vertices in h['geom_convex_vert'] for vertices in vert
dist = np.repeat(vert, vert.shape[0], axis=0) - np.tile(
h['geom_convex_vert'], (vert.shape[0], 1)
)
dist = (dist**2).sum(axis=1).reshape((vert.shape[0], -1))
vidx = np.argmin(dist, axis=0)
# check verts
np.testing.assert_array_equal(h['geom_convex_vert'], vert[vidx])
# check face vertices
map_ = {v: k for k, v in enumerate(vidx)}
h_face = np.vectorize(map_.get)(h['geom_convex_face'])
face_verts = sorted([tuple(sorted(set(s))) for s in h_face.tolist()])
expected_face_verts = sorted([
(0, 3, 4), (1, 3, 4), (0, 2, 4), (0, 1, 2, 3), (1, 2, 4)])
self.assertSequenceEqual(
face_verts,
expected_face_verts,
)
# check edges
unique_edge = np.vectorize(map_.get)(h['geom_convex_edge'])
unique_edge = np.array(sorted(unique_edge.tolist()))
np.testing.assert_array_equal(
unique_edge,
np.array([[0, 2], [0, 3], [0, 4], [1, 4], [2, 4], [3, 4]]),
)
# face normals
self.assertEqual(h['geom_convex_facenormal'].shape, (5, 3))
class ConvexHull2DTest(absltest.TestCase):
def test_convex_hull_2d_axis1(self):
"""Tests for the correct winding order of a polgyon with +y normal."""
pts = np.array([
[-0.04634297, -0.06652775, 0.05853534],
[-0.01877651, -0.08309858, -0.05236476],
[0.02362804, -0.08010745, 0.05499557],
[0.04066505, -0.09034877, -0.01354446],
[-0.07255043, -0.06837638, -0.00781699],
])
normal = np.array([-0.18467607, -0.97768016, 0.10018111])
idx = mesh._convex_hull_2d(pts, normal)
expected = np.cross(pts[idx][1] - pts[idx][0], pts[idx][2] - pts[idx][0])
expected /= np.linalg.norm(expected)
np.testing.assert_array_almost_equal(normal, expected)
def test_convex_hull_2d_axis2(self):
"""Tests for the correct winding order for a polgyon with +z normal."""
pts = np.array([
[0.08607829, -0.03881998, -0.03291714],
[-0.01877651, -0.08309858, -0.05236476],
[0.05470364, 0.00027677, -0.08371042],
[-0.01010019, -0.02708892, -0.0957297],
[0.04066505, -0.09034877, -0.01354446],
])
normal = np.array([0.3839915, -0.60171936, -0.70034587])
idx = mesh._convex_hull_2d(pts, normal)
expected = np.cross(pts[idx][1] - pts[idx][0], pts[idx][2] - pts[idx][0])
expected /= np.linalg.norm(expected)
np.testing.assert_array_almost_equal(normal, expected)
class UniqueEdgesTest(absltest.TestCase):
def test_tetrahedron_edges(self):
"""Tests unique edges for a tetrahedron."""
vert = np.array(
[[-0.1, 0.0, -0.1], [0.0, 0.1, 0.1], [0.1, 0.0, -0.1], [0.0, -0.1, 0.1]]
)
face = np.array([[0, 1, 2], [0, 2, 3], [0, 3, 1], [2, 1, 3]])
idx = mesh._get_unique_edges(vert, face)
np.testing.assert_array_equal(
idx, np.array([[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]])
)
if __name__ == '__main__':
absltest.main()
+141
View File
@@ -0,0 +1,141 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Passive forces."""
from typing import Tuple
import jax
from jax import numpy as jp
from mujoco.mjx._src import math
from mujoco.mjx._src import scan
from mujoco.mjx._src import support
# pylint: disable=g-importing-member
from mujoco.mjx._src.types import Data
from mujoco.mjx._src.types import DisableBit
from mujoco.mjx._src.types import JointType
from mujoco.mjx._src.types import Model
# pylint: enable=g-importing-member
def _inertia_box_fluid_model(
m: Model,
inertia: jax.Array,
mass: jax.Array,
root_com: jax.Array,
xipos: jax.Array,
ximat: jax.Array,
cvel: jax.Array,
) -> Tuple[jax.Array, jax.Array]:
"""Fluid forces based on inertia-box approximation."""
box = jp.repeat(inertia[None, :], 3, axis=0)
box *= jp.ones((3, 3)) - 2 * jp.eye(3)
box = 6.0 * jp.clip(jp.sum(box, axis=-1), a_min=1e-12)
box = jp.sqrt(box / jp.maximum(mass, 1e-12)) * (mass > 0.0)
# transform to local coordinate frame
offset = xipos - root_com
lvel = math.transform_motion(cvel, offset, ximat)
lwind = ximat.T @ m.opt.wind
lvel = lvel.at[3:].add(-lwind)
# set viscous force and torque
diam = jp.mean(box, axis=-1)
lfrc_ang = lvel[:3] * -jp.pi * diam**3 * m.opt.viscosity
lfrc_vel = lvel[3:] * -3.0 * jp.pi * diam * m.opt.viscosity
# add lift and drag force and torque
scale_vel = jp.array([box[1] * box[2], box[0] * box[2], box[0] * box[1]])
scale_ang = jp.array([
box[0] * (box[1] ** 4 + box[2] ** 4),
box[1] * (box[0] ** 4 + box[2] ** 4),
box[2] * (box[0] ** 4 + box[1] ** 4),
])
lfrc_vel -= 0.5 * m.opt.density * scale_vel * jp.abs(lvel[3:]) * lvel[3:]
lfrc_ang -= (
1.0 * m.opt.density * scale_ang * jp.abs(lvel[:3]) * lvel[:3] / 64.0
)
# rotate to global orientation: lfrc -> bfrc
force, torque = ximat @ lfrc_vel, ximat @ lfrc_ang
return force, torque
def passive(m: Model, d: Data) -> Data:
"""Adds all passive forces."""
if m.opt.disableflags & DisableBit.PASSIVE:
return d
# joint-level springs
def fn(jnt_typs, stiffness, qpos_spring, qpos):
qpos_i = 0
qfrcs = []
for i in range(len(jnt_typs)):
jnt_typ = JointType(jnt_typs[i])
q = qpos[qpos_i : qpos_i + jnt_typ.qpos_width()]
qs = qpos_spring[qpos_i : qpos_i + jnt_typ.qpos_width()]
qfrc = jp.zeros(jnt_typ.dof_width())
if jnt_typ == JointType.FREE:
qfrc = qfrc.at[:3].set(-stiffness[i] * (q[:3] - qs[:3]))
qfrc = qfrc.at[3:6].set(-stiffness[i] * math.quat_sub(q[3:7], qs[3:7]))
elif jnt_typ == JointType.BALL:
qfrc = -stiffness[i] * math.quat_sub(q, qs)
elif jnt_typ in (
JointType.SLIDE,
JointType.HINGE,
):
qfrc = -stiffness[i] * (q - qs)
else:
raise RuntimeError(f'unrecognized joint type: {jnt_typ}')
qfrcs.append(qfrc)
qpos_i += jnt_typ.qpos_width()
return jp.concatenate(qfrcs)
qfrc_passive = scan.flat(
m,
fn,
'jjqq',
'v',
m.jnt_type,
m.jnt_stiffness,
m.qpos_spring,
d.qpos,
)
# dof-level dampers
qfrc_passive -= m.dof_damping * d.qvel
# TODO(robotics-simulation): body-level gravity compensation
# body-level viscosity, lift and drag
if m.opt.has_fluid_params:
force, torque = jax.vmap(
_inertia_box_fluid_model, in_axes=(None, 0, 0, 0, 0, 0, 0)
)(
m,
m.body_inertia,
m.body_mass,
d.subtree_com[jp.array(m.body_rootid)],
d.xipos,
d.ximat,
d.cvel,
)
qfrc_target = jax.vmap(support.apply_ft, in_axes=(None, None, 0, 0, 0, 0))(
m, d, force, torque, d.xipos, jp.arange(m.nbody)
)
qfrc_passive += jp.sum(qfrc_target, axis=0)
d = d.replace(qfrc_passive=qfrc_passive)
return d
+114
View File
@@ -0,0 +1,114 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Tests passive forces."""
import itertools
from absl.testing import absltest
from absl.testing import parameterized
from etils import epath
import jax
import jax.numpy as jp
import mujoco
from mujoco import mjx
import numpy as np
def _assert_attr_eq(a, b, attr, step, fname, atol=1e-5, rtol=1e-5):
err_msg = f'mismatch: {attr} at step {step} in {fname}'
a, b = getattr(a, attr), getattr(b, attr)
np.testing.assert_allclose(a, b, err_msg=err_msg, atol=atol, rtol=rtol)
class PassiveTest(parameterized.TestCase):
@parameterized.parameters(enumerate(('ant.xml', 'mixed_joint_pendulum.xml')))
def test_stiffness_damping(self, seed, fname):
"""Tests stiffness and damping on Ant."""
np.random.seed(seed)
path = epath.resource_path('mujoco.mjx') / 'test_data'
path /= fname
m = mujoco.MjModel.from_xml_string(path.read_text())
# set stiffness/damping
m.jnt_stiffness = np.random.uniform(size=m.njnt)
m.dof_damping = np.random.uniform(size=m.nv)
d = mujoco.MjData(m)
d.qvel = np.random.random(m.nv) # random kick
mx = mjx.device_put(m)
dx = mjx.make_data(mx)
passive_jit_fn = jax.jit(mjx.passive)
for i in range(100):
qpos, qvel = d.qpos.copy(), d.qvel.copy()
mujoco.mj_step(m, d)
dx = passive_jit_fn(mx, dx.replace(qpos=qpos, qvel=qvel))
_assert_attr_eq(d, dx, 'qfrc_passive', i, fname)
@parameterized.parameters(
itertools.product(range(3), ('triple_pendulum.xml',))
)
def test_fluid(self, seed, fname):
np.random.seed(seed)
path = epath.resource_path('mujoco.mjx') / 'test_data'
path /= fname
m = mujoco.MjModel.from_xml_string(path.read_text())
# set density/viscosity/wind
m.opt.density = np.random.uniform()
m.opt.viscosity = np.random.uniform()
m.opt.wind = np.random.uniform()
passive_jit_fn = jax.jit(mjx.passive)
mx = mjx.device_put(m)
d = mujoco.MjData(m)
d.qvel = np.random.random(m.nv) # random kick
for i in range(100):
mujoco.mj_step(m, d)
dx = mjx.device_put(d)
mujoco.mj_passive(m, d)
dx = passive_jit_fn(mx, dx)
_assert_attr_eq(d, dx, 'qfrc_passive', i, fname)
def test_disable_passive(self):
m = mujoco.MjModel.from_xml_string("""
<mujoco>
<option density="1" viscosity="2" wind="0.1 0.2 0.3">
<flag passive="disable"/>
</option>
<worldbody>
<body>
<joint damping="1" axis="1 0 0" type="ball"/>
<geom pos="0 0.5 0" size=".15" mass="1" type="sphere"/>
</body>
</worldbody>
</mujoco>
""")
mx = mjx.device_put(m)
d = mujoco.MjData(m)
dx = mjx.device_put(d)
dx = dx.replace(qvel=jp.ones(mx.nv))
passive_jit_fn = jax.jit(mjx.passive)
dx = passive_jit_fn(mx, dx)
np.testing.assert_equal(dx.qfrc_passive, np.zeros(mx.nv))
if __name__ == '__main__':
absltest.main()
+432
View File
@@ -0,0 +1,432 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Scan across data ordered by body joint types and kinematic tree order."""
from typing import Any, Callable, TypeVar
import jax
from jax import numpy as jp
# pylint: disable=g-importing-member
from mujoco.mjx._src.types import JointType
from mujoco.mjx._src.types import Model
from mujoco.mjx._src.types import TrnType
# pylint: enable=g-importing-member
import numpy as np
Y = TypeVar('Y')
# TODO(erikfrey): re-check if this really helps perf
def _take(obj: Y, idx: np.ndarray) -> Y:
"""Takes idxs on any pytree given to it.
XLA executes x[jp.array([1, 2, 3])] slower than x[1:4], so we detect when
take indices are contiguous, and convert them to slices.
Args:
obj: an input pytree
idx: indices to take
Returns:
obj pytree with leaves taken by idxs
"""
if isinstance(obj, np.ndarray):
return obj[idx]
def take(x):
# TODO(erikfrey): if this helps perf, add support for striding too
if (
len(idx.shape) == 1
and idx.size > 0
and (idx == np.arange(idx[0], idx[0] + idx.size)).all()
and (idx > 0).all()
):
x = x[idx[0] : idx[-1] + 1]
else:
x = x.take(jp.array(idx), axis=0, mode='wrap')
return x
return jax.tree_map(take, obj)
def _q_bodyid(m: Model) -> np.ndarray:
"""Returns the bodyid for each qpos adress."""
q_bodyids = [np.array([], dtype=np.int32)]
for jnt_type, jnt_bodyid in zip(m.jnt_type, m.jnt_bodyid):
width = {JointType.FREE: 7, JointType.BALL: 4}.get(jnt_type, 1)
q_bodyids.append(np.repeat(jnt_bodyid, width))
return np.concatenate(q_bodyids)
def _q_jointid(m: Model) -> np.ndarray:
"""Returns the jointid for each qpos adress."""
q_jointid = [np.array([], dtype=np.int32)]
for i, jnt_type in enumerate(m.jnt_type):
width = {JointType.FREE: 7, JointType.BALL: 4}.get(jnt_type, 1)
q_jointid.append(np.repeat(i, width))
return np.concatenate(q_jointid)
def _index(haystack: np.ndarray, needle: np.ndarray) -> np.ndarray:
"""Returns indexes in haystack for elements in needle."""
idx = np.argsort(haystack)
sorted_haystack = haystack[idx]
sorted_idx = np.searchsorted(sorted_haystack, needle)
idx = np.take(idx, sorted_idx, mode='clip')
idx[haystack[idx] != needle] = -1
return idx
def _nvmap(f: Callable[..., Y], *args) -> Y:
"""A vmap that accepts numpy arrays.
Numpy arrays are statically vmapped, and the elements are passed to f as
static arguments. The implication is that all the elements of numpy array
arguments must be the same.
Args:
f: function to be mapped over
*args: args to be mapped along, passed to f
Returns:
the result of vmapping f over args
Raises:
RuntimeError: if numpy arg elements do not match
"""
for arg in args:
if isinstance(arg, np.ndarray) and not np.all(arg == arg[0]):
raise RuntimeError(f'numpy arg elements do not match: {arg}')
np_args = [a[0] if isinstance(a, np.ndarray) else None for a in args]
args = [a if n is None else None for n, a in zip(np_args, args)]
in_axes = [None if a is None else 0 for a in args]
def outer_f(*args, np_args=np_args):
args = [a if n is None else n for n, a in zip(args, np_args)]
return f(*args)
return jax.vmap(outer_f, in_axes=in_axes)(*args)
def _check_input(m: Model, args: Any, in_types: str) -> None:
"""Checks that scan input has the right shape."""
size = {'b': m.nbody, 'j': m.njnt, 'q': m.nq, 'v': m.nv, 'u': m.nu, 'a': m.na}
for idx, (arg, typ) in enumerate(zip(args, in_types)):
if len(arg) != size[typ]:
raise IndexError(
(
f'f argument "{idx}" with type "{typ}" has length "{len(arg)}"'
f' which does not match the in_types[{idx}] expected length of '
f'"{size[typ]}".'
)
)
def _check_output(
y: jax.Array, take_ids: np.ndarray, typ: str, idx: int
) -> None:
"""Checks that scan output has the right shape."""
if y.shape[0] != take_ids.shape[0]:
raise IndexError(
(
f'f output "{idx}" with type "{typ}" has shape "{y.shape[0]}" '
f'which does not match the out_types[{idx}] expected size of'
f' "{take_ids.shape[0]}".'
)
)
def flat(
m: Model,
f: Callable[..., Y],
in_types: str,
out_types: str,
*args,
group_by: str = 'j',
) -> Y:
r"""Scan a function across bodies or actuators.
Scan group data according to type and batch shape then calls vmap(f) on it.\
Args:
m: an mjx model
f: a function to be scanned with the following type signature:
def f(key, *args) -> y
where
``key`` gives grouping key for this function instance
``*args`` are input arguments with types matching ``in_types``
``y`` is an output arguments with types matching ``out_type``
in_types: string specifying the type of each input arg:
'b': split according to bodies
'j': split according to joint types
'q': split according to generalized coordinates (len(qpos))
'v': split according to degrees of freedom (len(qvel))
'u': split according to actuators
'a': split according to actuator activations
out_types: string specifying the types the output dimension matches
*args: the input arguments corresponding to ``in_types``
group_by: the type to group by, either joints or actuators
Returns:
The stacked outputs of ``f`` matching the model's order.
Raises:
IndexError: if function output shape does not match out_types shape
"""
_check_input(m, args, in_types)
if group_by not in {'j', 'u'}:
raise NotImplementedError(f'group by type "{group_by}" not implemented.')
def key_j(ids):
if any(t in 'jqv' for t in in_types + out_types):
return tuple(m.jnt_type[ids])
return ()
def key_u(ids_u, ids_j):
return (
m.actuator_biastype[ids_u],
m.actuator_gaintype[ids_u],
m.actuator_dyntype[ids_u],
m.actuator_trntype[ids_u],
m.jnt_type[ids_j],
)
def type_ids_j(m, i):
return {
'b': i,
'j': np.nonzero(m.jnt_bodyid == i)[0],
'v': np.nonzero(m.dof_bodyid == i)[0],
'q': np.nonzero(_q_bodyid(m) == i)[0],
}
def type_ids_u(m, i):
typ_ids = {
'u': i,
'a': m.actuator_actadr[i],
'j': (
m.actuator_trnid[i]
if m.actuator_trntype[i] == TrnType.JOINT
else np.array(-1)
),
}
# v/q associated with joint transmissions
typ_ids.update({
'v': np.nonzero(m.dof_jntid == typ_ids['j'])[0],
'q': np.nonzero(_q_jointid(m) == typ_ids['j'])[0],
})
return typ_ids
# build up a grouping of type take-ids in body/actuator order
key_typ_ids, order = {}, []
all_types = set(in_types + out_types)
n_items = {'j': m.nbody, 'u': m.nu}[group_by]
for i in np.arange(n_items, dtype=np.int32):
typ_ids = type_ids_j(m, i) if group_by == 'j' else type_ids_u(m, i)
# create grouping key
key = (
key_j(typ_ids['j'])
if group_by == 'j'
else key_u(typ_ids['u'], typ_ids['j'])
)
order.append((key, typ_ids))
# add ids per type to the corresponding group
for t in all_types:
out = key_typ_ids.setdefault(key, {})
val = np.expand_dims(typ_ids[t], axis=0)
out[t] = np.concatenate((out[t], val)) if t in out else val
key_typ_ids = list(sorted(key_typ_ids.items()))
# use this grouping to take the right data subsets and call vmap(f)
ys = []
for _, typ_ids in key_typ_ids:
# only execute f if we would actually take something from the result
if any(typ_ids[v].size > 0 for v in out_types):
f_args = [_take(arg, typ_ids[typ]) for arg, typ in zip(args, in_types)]
y = _nvmap(f, *f_args)
ys.append(y)
else:
ys.append(None)
# remove None results from the final output
key_typ_ids = [v for y, v in zip(ys, key_typ_ids) if y is not None]
ys = [y for y in ys if y is not None]
ys_keys = set([k for k, *_ in key_typ_ids])
order = [o for k, o in order if k in ys_keys]
# get the original input order
order = [[o[t] for o in order] for t in all_types]
order = [
np.concatenate(o) if isinstance(o[0], np.ndarray) else np.array(o)
for o in order
]
order = dict(zip(all_types, order))
# concatenate back to a single tree and drop the grouping dimension
f_ret_is_seq = isinstance(ys[0], (list, tuple))
ys = ys if f_ret_is_seq else [[y] for y in ys]
flat_ = {'j': 'b', 'u': 'uaj'}[group_by]
ys = [
[v if typ in flat_ else jp.concatenate(v) for v, typ in zip(y, out_types)]
for y in ys
]
ys = jax.tree_map(lambda *x: jp.concatenate(x), *ys)
# put concatenated results back in order
reordered_ys = []
for i, (y, typ) in enumerate(zip(ys, out_types)):
_check_output(y, order[typ], typ, i)
ids = np.concatenate([np.hstack(v[typ]) for _, v in key_typ_ids])
input_order = order[typ][np.where(order[typ] != -1)]
reordered_ys.append(_take(y, _index(ids, input_order)))
y = reordered_ys if f_ret_is_seq else reordered_ys[0]
return y
def body_tree(
m: Model,
f: Callable[..., Y],
in_types: str,
out_types: str,
*args,
reverse: bool = False,
) -> Y:
r"""Scan ``f`` across bodies in tree order, carrying results up/down the tree.
This function groups bodies according to level and attached joints, then calls
vmap(f) on them.\
Args:
m: an mjx mjmodel
f: a function to be scanned with the following type signature:
def f(y, *args) -> y
where
``y`` is the carry value and return value
``*args`` are input arguments with types matching ``in_types``
in_types: string specifying the type of each input arg:
'b': split according to bodies
'j': split according to joint types
'q': split according to generalized coordinates (len(qpos))
'v': split according to degrees of freedom (len(qvel))
out_types: string specifying the types the output dimension matches
*args: the input arguments corresponding to ``in_types``
reverse: if True, scans up the body tree from leaves to root, otherwise
root to leaves
Returns:
The stacked outputs of ``f`` matching the model's body order.
Raises:
IndexError: if function output shape does not match out_types shape
"""
_check_input(m, args, in_types)
depth_fn = lambda i, p=m.body_parentid: int(i > 0) and 1 + depth_fn(p[i])
typ_body_id = {
'j': m.jnt_bodyid,
'v': m.dof_bodyid,
'q': _q_bodyid(m),
}
key_parents = {}
# build up groupings of bodies and type ids using (level, (jnt_type,)) keys
key_typ_ids, key_body_ids = {}, {}
for body_id in np.arange(m.nbody, dtype=np.int32):
depth = depth_fn(body_id)
# create grouping key
if any(t in 'jqv' for t in in_types + out_types):
jnts = np.nonzero(typ_body_id['j'] == body_id)[0]
jnts_p = np.nonzero(typ_body_id['j'] == m.body_parentid[body_id])[0]
key = depth, tuple(m.jnt_type[jnts])
parent_key = depth - 1, tuple(m.jnt_type[jnts_p])
else:
key, parent_key = (depth, ()), (depth - 1, ())
key_parents[key] = parent_key
body_ids = key_body_ids.get(key, np.array([], dtype=np.int32))
key_body_ids[key] = np.append(body_ids, body_id)
# add ids per type
for t in set(in_types + out_types):
out = key_typ_ids.setdefault(key, {})
id_ = body_id if t == 'b' else np.nonzero(typ_body_id[t] == body_id)[0]
id_ = np.expand_dims(id_, axis=0)
out[t] = np.concatenate((out[t], id_)) if t in out else id_
key_typ_ids = list(sorted(key_typ_ids.items(), reverse=reverse))
# use this grouping to take the right data subsets and call vmap(f)
key_y = {}
for key, typ_ids in key_typ_ids:
carry = None
if reverse:
child_keys = [k for k, v in key_parents.items() if v == key]
for child_key in child_keys:
y = key_y[child_key]
body_ids = key_body_ids[key]
parent_ids = m.body_parentid[key_body_ids[child_key]]
id_map = _index(body_ids, parent_ids)
def index_sum(x, i=id_map, s=body_ids.size):
return jax.ops.segment_sum(x, i, s)
y = jax.tree_map(index_sum, y)
carry = y if carry is None else jax.tree_map(jp.add, carry, y)
else:
parent_key = key_parents[key]
y = key_y.get(parent_key)
if y is not None:
body_ids = key_body_ids[parent_key]
parent_ids = m.body_parentid[key_body_ids[key]]
take_fn = lambda x, i=_index(body_ids, parent_ids): _take(x, i)
carry = jax.tree_map(take_fn, y)
f_args = [_take(arg, typ_ids[typ]) for arg, typ in zip(args, in_types)]
key_y[key] = _nvmap(f, carry, *f_args)
# slice None results from the final output
key_typ_ids = [(k, v) for k, v in key_typ_ids if key_y[k] is not None]
# concatenate back to a single tree and drop the grouping dimension
ys = [key_y[key] for key, _ in key_typ_ids]
f_ret_is_seq = isinstance(ys[0], (list, tuple))
ys = ys if f_ret_is_seq else [[y] for y in ys]
ys = [
[v if typ == 'b' else jp.concatenate(v) for v, typ in zip(y, out_types)]
for y in ys
]
ys = jax.tree_map(lambda *x: jp.concatenate(x), *ys)
# put concatenated results back into body order
reordered_ys = []
for i, (y, typ) in enumerate(zip(ys, out_types)):
ids = np.concatenate([np.hstack(v[typ]) for _, v in key_typ_ids])
take_ids = _index(ids, np.sort(ids))
_check_output(y, take_ids, typ, i)
reordered_ys.append(_take(y, take_ids))
y = reordered_ys if f_ret_is_seq else reordered_ys[0]
return y
+227
View File
@@ -0,0 +1,227 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Tests for scan functions."""
from absl.testing import absltest
from jax import numpy as jp
import mujoco
from mujoco import mjx
# pylint: disable=g-importing-member
from mujoco.mjx._src import scan
from mujoco.mjx._src.types import JointType
# pylint: enable=g-importing-member
import numpy as np
class ScanTest(absltest.TestCase):
_MULTI_DOF_XML = """
<mujoco>
<compiler inertiafromgeom="true"/>
<worldbody>
<body>
<joint type="free"/>
<geom size=".15" mass="1" type="sphere"/>
<body>
<joint axis="1 0 0" pos="1 0 0" type="ball"/>
<geom size=".15" mass="2" type="sphere"/>
</body>
<body>
<joint axis="1 0 0" pos="2 0 0" type="hinge"/>
<joint axis="0 1 0" pos="3 0 0" type="slide"/>
<geom size=".15" mass="3" type="sphere"/>
</body>
</body>
</worldbody>
</mujoco>
"""
def test_flat_empty(self):
"""Test scanning over just world body."""
m = mujoco.MjModel.from_xml_string("""
<mujoco model="world_body">
<worldbody/>
</mujoco>
""")
m = mjx.device_put(m)
def fn(body_id):
return body_id + 1
b_in = jp.array([1])
b_expect = jp.array([2])
b_out = scan.flat(m, fn, 'b', 'b', b_in)
np.testing.assert_equal(np.array(b_out), np.array(b_expect))
def test_flat_joints(self):
"""Tests scanning over bodies with joints of different types."""
m = mujoco.MjModel.from_xml_string(self._MULTI_DOF_XML)
m = mjx.device_put(m)
# we will test two functions:
# 1) j_fn receives jnt_types as a jp array
# 2) s_fn receives jnt_types as a static np array and can switch on it
j_fn = lambda jnt_pos, val: val + jp.sum(jnt_pos)
s_fn = lambda jnt_types, val: val + sum(jnt_types)
b_in = jp.array([[0, 0], [1, 1], [2, 2], [3, 3]])
b_expect = jp.array([[0, 0], [1, 1], [3, 3], [8, 8]])
b_out = scan.flat(m, j_fn, 'jb', 'b', m.jnt_pos, b_in)
np.testing.assert_equal(np.array(b_out), np.array(b_expect))
b_out = scan.flat(m, s_fn, 'jb', 'b', m.jnt_type, b_in)
np.testing.assert_equal(np.array(b_out), np.array(b_expect))
# None should be omitted from the results
def no_free(jnt_types, val):
if tuple(jnt_types) == (JointType.FREE,):
return None
return val + sum(jnt_types)
b_expect = jp.array([[0, 0], [3, 3], [8, 8]])
b_out = scan.flat(m, no_free, 'jb', 'b', m.jnt_type, b_in)
np.testing.assert_equal(np.array(b_out), np.array(b_expect))
# we should not call functions for which we know we will discard the results
def no_world(jnt_types, val):
if jnt_types.size == 0:
self.fail('world has no dofs, should not be called')
return val + sum(jnt_types)
v_in = jp.ones((m.nv, 1))
scan.flat(m, no_world, 'jv', 'v', m.jnt_type, v_in)
def test_body_tree(self):
"""Tests tree scanning over bodies with different joint counts."""
m = mujoco.MjModel.from_xml_string(self._MULTI_DOF_XML)
m = mjx.device_put(m)
# we will test two functions:
# 1) j_fn receives jnt_pos which is a jp array
# 2) s_fn receives jnt_types which is a static np array
def j_fn(carry, jnt_pos, val):
carry = jp.zeros_like(val) if carry is None else carry
return carry + val + jp.sum(jnt_pos)
def s_fn(carry, jnt_types, val):
carry = jp.zeros_like(val) if carry is None else carry
return carry + val + sum(jnt_types)
b_in = jp.array([[0, 0], [1, 1], [2, 2], [3, 3]])
b_expect = jp.array([[0, 0], [1, 1], [4, 4], [9, 9]])
b_out = scan.body_tree(m, j_fn, 'jb', 'b', m.jnt_pos, b_in)
np.testing.assert_equal(np.array(b_out), np.array(b_expect))
b_out = scan.body_tree(m, s_fn, 'jb', 'b', m.jnt_type, b_in)
np.testing.assert_equal(np.array(b_out), np.array(b_expect))
# and reverse too:
b_expect = jp.array([[12, 12], [12, 12], [3, 3], [8, 8]])
b_out = scan.body_tree(m, j_fn, 'jb', 'b', m.jnt_pos, b_in, reverse=True)
np.testing.assert_equal(np.array(b_out), np.array(b_expect))
b_out = scan.body_tree(m, s_fn, 'jb', 'b', m.jnt_type, b_in, reverse=True)
np.testing.assert_equal(np.array(b_out), np.array(b_expect))
# None should be omitted from the results
def no_free(carry, jnt_types, val):
if tuple(jnt_types) == (JointType.FREE,):
return None
carry = jp.zeros_like(val) if carry is None else carry
return carry + val + sum(jnt_types)
b_expect = jp.array([[0, 0], [3, 3], [8, 8]])
b_out = scan.body_tree(m, no_free, 'jb', 'b', m.jnt_type, b_in)
np.testing.assert_equal(np.array(b_out), np.array(b_expect))
_MULTI_ACT_XML = """
<mujoco>
<option timestep="0.02"/>
<compiler autolimits="true"/>
<default>
<geom contype="0" conaffinity="0"/>
</default>
<worldbody>
<body pos="0 0 -0.5">
<joint type="free" name="joint0" range="-37.81 86.15"/>
<geom pos="0 0.5 0" size=".15" mass="1" type="sphere"/>
<body pos="0 0 -0.5">
<joint axis="1 0 0" type="hinge" name="joint1" range="-32.88 5.15" actuatorfrcrange="-0.48 0.72"/>
<geom pos="0 0.5 0" size=".15" mass="1" type="sphere"/>
<body pos="0 0 -0.5">
<joint axis="0 1 0" type="slide" name="joint2" range="-83.31 54.77"/>
<joint axis="1 0 0" type="slide" name="joint3" range="-87.84 60.58" actuatorfrcrange="-0.62 0.28"/>
<joint axis="0 1 0" type="hinge" name="joint4" range="-49.96 83.03" actuatorfrcrange="-0.01 0.64"/>
<geom pos="0 0.5 0" size=".15" mass="1" type="sphere"/>
<body pos="0 0 -0.5">
<joint axis="1 0 0" type="slide" name="joint5" range="-1.53 87.07" actuatorfrcrange="-0.06 0.26"/>
<joint axis="0 1 0" type="slide" name="joint6" range="-63.57 9.95"/>
<joint axis="1 0 0" type="hinge" name="joint7" range="-22.50 41.20" actuatorfrcrange="-0.41 0.84"/>
<geom pos="0 0.5 0" size=".15" mass="1" type="sphere"/>
<body pos="0 0 -0.5">
<joint axis="0 1 0" type="hinge" name="joint8" range="-34.67 72.56"/>
<joint axis="1 0 0" type="hinge" name="joint9" range="-1.81 16.22" actuatorfrcrange="-0.41 0.14"/>
<joint axis="1 0 0" type="ball" name="joint10" range="0.00 6.02" actuatorfrcrange="-0.74 0.12"/>
<geom pos="0 0.5 0" size=".15" mass="1" type="sphere"/>
</body>
</body>
</body>
</body>
</body>
</worldbody>
<actuator>
<position joint="joint6" gear="1"/>
<intvelocity joint="joint1" kp="2000" actrange="-0.7 2.3"/>
<motor joint="joint3" gear="42"/>
<velocity joint="joint2" kv="123"/>
<position joint="joint0" ctrlrange="-0.9472 0.9472"/>
<intvelocity joint="joint7" kp="2000" actrange="-0.7 2.3"/>
<general joint="joint5" ctrlrange="0.1 2.34346" biastype="affine" gainprm="35 0 0" biasprm="0 -35 -0.65"/>
<general joint="joint4" ctrlrange="0.1 2.34346" biastype="affine" gainprm="35 0 0" biasprm="0 -35 -0.65"/>
</actuator>
</mujoco>
"""
def testscan_actuators(self):
"""Tests scanning over actuators."""
m = mujoco.MjModel.from_xml_string(self._MULTI_ACT_XML)
m = mjx.device_put(m)
fn = lambda *args: args
args = (
m.actuator_gear,
m.jnt_type,
jp.arange(m.nq),
jp.arange(m.nv),
jp.array([1.4, 1.1]),
)
gear, jnt_typ, qadr, vadr, act = scan.flat(
m, fn, 'ujqva', 'ujqva', *args, group_by='u'
)
np.testing.assert_array_equal(gear, m.actuator_gear)
np.testing.assert_array_equal(jnt_typ, m.jnt_type[m.actuator_trnid])
np.testing.assert_array_equal(act, jp.array([1.4, 1.1]))
expected_vadr = np.concatenate(
[np.nonzero(m.dof_jntid == trnid)[0] for trnid in m.actuator_trnid]
)
np.testing.assert_array_equal(vadr, expected_vadr)
expected_qadr = np.concatenate(
[np.nonzero(scan._q_jointid(m) == i)[0] for i in m.actuator_trnid]
)
np.testing.assert_array_equal(qadr, expected_qadr)
if __name__ == '__main__':
absltest.main()
+467
View File
@@ -0,0 +1,467 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Core smooth dynamics functions."""
import jax
from jax import numpy as jp
import mujoco
from mujoco.mjx._src import math
from mujoco.mjx._src import scan
# pylint: disable=g-importing-member
from mujoco.mjx._src.types import Data
from mujoco.mjx._src.types import DisableBit
from mujoco.mjx._src.types import JointType
from mujoco.mjx._src.types import Model
# pylint: enable=g-importing-member
def kinematics(m: Model, d: Data) -> Data:
"""Converts position/velocity from generalized coordinates to maximal."""
def fn(carry, jnt_typs, jnt_pos, jnt_axis, qpos, qpos0, pos, quat):
# calculate joint anchors, axes, body pos and quat in global frame
# also normalize qpos while we're at it
if carry is not None:
_, _, _, parent_pos, parent_quat, _ = carry
pos = parent_pos + math.rotate(pos, parent_quat)
quat = math.quat_mul(parent_quat, quat)
anchors, axes = [], []
qpos_i = 0
for i, jnt_typ in enumerate(jnt_typs):
if jnt_typ == JointType.FREE:
anchor, axis = qpos[qpos_i : qpos_i + 3], jp.array([0.0, 0.0, 1.0])
else:
anchor = math.rotate(jnt_pos[i], quat) + pos
axis = math.rotate(jnt_axis[i], quat)
anchors, axes = anchors + [anchor], axes + [axis]
if jnt_typ == JointType.FREE:
pos = qpos[qpos_i : qpos_i + 3]
quat = math.normalize(qpos[qpos_i + 3 : qpos_i + 7])
qpos = qpos.at[qpos_i + 3 : qpos_i + 7].set(quat)
qpos_i += 7
elif jnt_typ == JointType.BALL:
qloc = math.normalize(qpos[qpos_i : qpos_i + 4])
qpos = qpos.at[qpos_i : qpos_i + 4].set(qloc)
quat = math.quat_mul(quat, qloc)
pos = anchor - math.rotate(jnt_pos[i], quat) # off-center rotation
qpos_i += 4
elif jnt_typ == JointType.HINGE:
angle = qpos[qpos_i] - qpos0[qpos_i]
qloc = math.axis_angle_to_quat(jnt_axis[i], angle)
quat = math.quat_mul(quat, qloc)
pos = anchor - math.rotate(jnt_pos[i], quat) # off-center rotation
qpos_i += 1
elif jnt_typ == JointType.SLIDE:
pos += axis * (qpos[qpos_i] - qpos0[qpos_i])
qpos_i += 1
else:
raise RuntimeError(f'unrecognized joint type: {jnt_typ}')
anchor = jp.stack(anchors) if anchors else jp.empty((0, 3))
axis = jp.stack(axes) if axes else jp.empty((0, 3))
mat = math.quat_to_mat(quat)
return qpos, anchor, axis, pos, quat, mat
qpos, xanchor, xaxis, xpos, xquat, xmat = scan.body_tree(
m,
fn,
'jjjqqbb',
'qjjbbb',
m.jnt_type,
m.jnt_pos,
m.jnt_axis,
d.qpos,
m.qpos0,
m.body_pos,
m.body_quat,
)
@jax.vmap
def local_to_global(pos1, quat1, pos2, quat2):
pos = pos1 + math.rotate(pos2, quat1)
mat = math.quat_to_mat(math.quat_mul(quat1, quat2))
return pos, mat
# TODO(erikfrey): confirm that quats are more performant for mjx than mats
xipos, ximat = local_to_global(xpos, xquat, m.body_ipos, m.body_iquat)
geom_xpos, geom_xmat = local_to_global(
xpos[m.geom_bodyid], xquat[m.geom_bodyid], m.geom_pos, m.geom_quat
)
d = d.replace(qpos=qpos, xanchor=xanchor, xaxis=xaxis, xpos=xpos)
d = d.replace(xquat=xquat, xmat=xmat, xipos=xipos, ximat=ximat)
d = d.replace(geom_xpos=geom_xpos, geom_xmat=geom_xmat)
return d
def com_pos(m: Model, d: Data) -> Data:
"""Maps inertias and motion dofs to global frame centered at subtree-CoM."""
# calculate center of mass of each subtree
def subtree_sum(carry, xipos, body_mass):
pos, mass = xipos * body_mass, body_mass
if carry is not None:
subtree_pos, subtree_mass = carry
pos, mass = pos + subtree_pos, mass + subtree_mass
return pos, mass
pos, mass = scan.body_tree(
m, subtree_sum, 'bb', 'bb', d.xipos, m.body_mass, reverse=True
)
cond = jp.tile(mass < jp.array(mujoco.mjMINVAL), (3, 1)).T
subtree_com = jp.where(cond, d.xipos, jax.vmap(jp.divide)(pos, mass))
d = d.replace(subtree_com=subtree_com)
# map inertias to frame centered at subtree_com
@jax.vmap
def inert_com(inert, ximat, off, mass):
h = jp.cross(off, -jp.eye(3))
inert = ximat @ jp.diag(inert) @ ximat.T + h @ h.T * mass
# cinert is triu(inert), mass * off, mass
inert = inert[(jp.array([0, 1, 2, 0, 0, 1]), jp.array([0, 1, 2, 1, 2, 2]))]
return jp.concatenate([inert, off * mass, jp.expand_dims(mass, 0)])
root_com = subtree_com[jp.array(m.body_rootid)]
offset = d.xipos - root_com
cinert = inert_com(m.body_inertia, d.ximat, offset, m.body_mass)
d = d.replace(cinert=cinert)
# map motion dofs to global frame centered at subtree_com
def cdof_fn(jnt_typs, root_com, xmat, xanchor, xaxis):
cdofs = []
dof_com_fn = lambda a, o: jp.concatenate([a, jp.cross(a, o)])
for i, jnt_typ in enumerate(jnt_typs):
offset = root_com - xanchor[i]
if jnt_typ == JointType.FREE:
cdofs.append(jp.eye(3, 6, 3)) # free translation
cdofs.append(jax.vmap(dof_com_fn, in_axes=(0, None))(xmat.T, offset))
elif jnt_typ == JointType.BALL:
cdofs.append(jax.vmap(dof_com_fn, in_axes=(0, None))(xmat.T, offset))
elif jnt_typ == JointType.HINGE:
cdof = dof_com_fn(xaxis[i], offset)
cdofs.append(jp.expand_dims(cdof, 0))
elif jnt_typ == JointType.SLIDE:
cdof = jp.concatenate((jp.zeros((3,)), xaxis[i]))
cdofs.append(jp.expand_dims(cdof, 0))
else:
raise RuntimeError(f'unrecognized joint type: {jnt_typ}')
cdof = jp.concatenate(cdofs) if cdofs else jp.empty((0, 6))
return cdof
cdof = scan.flat(
m,
cdof_fn,
'jbbjj',
'v',
m.jnt_type,
root_com,
d.xmat,
d.xanchor,
d.xaxis,
)
d = d.replace(cdof=cdof)
return d
def crb(m: Model, d: Data) -> Data:
"""Runs composite rigid body inertia algorithm."""
def crb_fn(crb_child, crb_body):
if crb_child is not None:
crb_body += crb_child
return crb_body
crb_body = scan.body_tree(m, crb_fn, 'b', 'b', d.cinert, reverse=True)
crb_body = crb_body.at[0].set(0.0)
d = d.replace(crb=crb_body)
# TODO(erikfrey): do centralized take fn?
crb_dof = jp.take(crb_body, jp.array(m.dof_bodyid), axis=0)
crb_cdof = jax.vmap(math.inert_mul)(crb_dof, d.cdof)
dof_i, dof_j, diag = [], [], []
for i in range(m.nv):
diag.append(len(dof_i))
j = i
while j > -1:
dof_i, dof_j = dof_i + [i], dof_j + [j]
j = m.dof_parentid[j]
crb_codf_i = jp.take(crb_cdof, jp.array(dof_i), axis=0)
cdof_j = jp.take(d.cdof, jp.array(dof_j), axis=0)
qm = jax.vmap(jp.dot)(crb_codf_i, cdof_j)
# add armature to diagonal
qm = qm.at[jp.array(diag)].add(m.dof_armature)
d = d.replace(qM=qm)
return d
def factor_m(
m: Model,
d: Data,
qM: jax.Array, # pylint:disable=invalid-name
) -> Data:
"""Gets sparse L'*D*L factorizaton of inertia-like matrix M, assumed spd."""
# build up indices for where we will do backwards updates over qLD
# TODO(erikfrey): do fewer updates by combining non-overlapping ranges
dof_madr = jp.array(m.dof_Madr)
updates = {}
madr_ds = []
for i in range(m.nv):
madr_d = madr_ij = m.dof_Madr[i]
j = i
while True:
madr_ds.append(madr_d)
madr_ij, j = madr_ij + 1, m.dof_parentid[j]
if j == -1:
break
madr_j_range = tuple(m.dof_Madr[j : j + 2])
updates.setdefault(madr_j_range, []).append((madr_d, madr_ij))
qld = qM
for (out_beg, out_end), vals in sorted(updates.items(), reverse=True):
madr_d, madr_ij = jp.array(vals).T
@jax.vmap
def off_diag_fn(madr_d, madr_ij, qld=qld, width=out_end - out_beg):
qld_row = jax.lax.dynamic_slice(qld, (madr_ij,), (width,))
return -(qld_row[0] / qld[madr_d]) * qld_row
qld_update = jp.sum(off_diag_fn(madr_d, madr_ij), axis=0)
qld = qld.at[out_beg:out_end].add(qld_update)
# TODO(erikfrey): determine if this minimum value guarding is necessary:
# qld = qld.at[dof_madr].set(jp.maximum(qld[dof_madr], _MJ_MINVAL))
qld_diag = qld[dof_madr]
qld = (qld / qld[jp.array(madr_ds)]).at[dof_madr].set(qld_diag)
d = d.replace(qLD=qld, qLDiagInv=1 / qld_diag)
return d
def solve_m(m: Model, d: Data, x: jax.Array) -> jax.Array:
"""Computes sparse backsubstitution: x = inv(L'*D*L)*y ."""
updates_i, updates_j = {}, {}
for i in range(m.nv):
madr_ij, j = m.dof_Madr[i], i
while True:
madr_ij, j = madr_ij + 1, m.dof_parentid[j]
if j == -1:
break
updates_i.setdefault(i, []).append((madr_ij, j))
updates_j.setdefault(j, []).append((madr_ij, i))
# x <- inv(L') * x
for j, vals in sorted(updates_j.items(), reverse=True):
madr_ij, i = jp.array(vals).T
x = x.at[j].add(-jp.sum(d.qLD[madr_ij] * x[i]))
# x <- inv(D) * x
x = x * d.qLDiagInv
# x <- inv(L) * x
for i, vals in sorted(updates_i.items()):
madr_ij, j = jp.array(vals).T
x = x.at[i].add(-jp.sum(d.qLD[madr_ij] * x[j]))
return x
def dense_m(m: Model, d: Data) -> jax.Array:
"""Reconstitute dense mass matrix from qM."""
is_, js, madr_ijs = [], [], []
for i in range(m.nv):
madr_ij, j = m.dof_Madr[i], i
while True:
madr_ij, j = madr_ij + 1, m.dof_parentid[j]
if j == -1:
break
is_, js, madr_ijs = is_ + [i], js + [j], madr_ijs + [madr_ij]
i, j, madr_ij = (jp.array(x, dtype=jp.int32) for x in (is_, js, madr_ijs))
mat = jp.zeros((m.nv, m.nv)).at[(i, j)].set(d.qM[madr_ij])
# diagonal, upper triangular, lower triangular
mat = jp.diag(d.qM[jp.array(m.dof_Madr)]) + mat + mat.T
return mat
def mul_m(m: Model, d: Data, vec: jax.Array) -> jax.Array:
"""Multiply vector by inertia matrix."""
diag_mul = d.qM[jp.array(m.dof_Madr)] * vec
is_, js, madr_ijs = [], [], []
for i in range(m.nv):
madr_ij, j = m.dof_Madr[i], i
while True:
madr_ij, j = madr_ij + 1, m.dof_parentid[j]
if j == -1:
break
is_, js, madr_ijs = is_ + [i], js + [j], madr_ijs + [madr_ij]
i, j, madr_ij = (jp.array(x, dtype=jp.int32) for x in (is_, js, madr_ijs))
out = diag_mul.at[i].add(d.qM[madr_ij] * vec[j])
out = out.at[j].add(d.qM[madr_ij] * vec[i])
return out
def com_vel(m: Model, d: Data) -> Data:
"""Computes cvel, cdof_dot."""
# forward scan down tree: accumulate link center of mass velocity
def fn(parent, jnt_typs, cdof, qvel):
cvel = jp.zeros((6,)) if parent is None else parent[0]
cross_fn = jax.vmap(math.motion_cross, in_axes=(None, 0))
cdof_x_qvel = jax.vmap(jp.multiply)(cdof, qvel)
dof_beg = 0
cdof_dots = []
for jnt_typ in jnt_typs:
dof_end = dof_beg + JointType(jnt_typ).dof_width()
if jnt_typ == JointType.FREE:
cvel += jp.sum(cdof_x_qvel[:3], axis=0)
cdof_ang_dot = cross_fn(cvel, cdof[3:])
cvel += jp.sum(cdof_x_qvel[3:], axis=0)
cdof_dots.append(jp.concatenate((jp.zeros((3, 6)), cdof_ang_dot)))
else:
cdof_dots.append(cross_fn(cvel, cdof[dof_beg:dof_end]))
cvel += jp.sum(cdof_x_qvel[dof_beg:dof_end], axis=0)
dof_beg = dof_end
cdof_dot = jp.concatenate(cdof_dots) if cdof_dots else jp.empty((0, 6))
return cvel, cdof_dot
cvel, cdof_dot = scan.body_tree(
m,
fn,
'jvv',
'bv',
m.jnt_type,
d.cdof,
d.qvel,
)
d = d.replace(cvel=cvel, cdof_dot=cdof_dot)
return d
def rne(m: Model, d: Data) -> Data:
"""Computes inverse dynamics using the recursive Newton-Euler algorithm."""
# forward scan over tree: accumulate link center of mass acceleration
def cacc_fn(cacc, cdof_dot, qvel):
if cacc is None:
if m.opt.disableflags & DisableBit.GRAVITY:
cacc = jp.zeros((6,))
else:
cacc = jp.concatenate((jp.zeros((3,)), -m.opt.gravity))
cacc += jp.sum(jax.vmap(jp.multiply)(cdof_dot, qvel), axis=0)
return cacc
cacc = scan.body_tree(m, cacc_fn, 'vv', 'b', d.cdof_dot, d.qvel)
def frc(cinert, cacc, cvel):
frc = math.inert_mul(cinert, cacc)
frc += math.motion_cross_force(cvel, math.inert_mul(cinert, cvel))
return frc
loc_cfrc = jax.vmap(frc)(d.cinert, cacc, d.cvel)
# backward scan up tree: accumulate body forces
def cfrc_fn(cfrc_child, cfrc):
if cfrc_child is not None:
cfrc += cfrc_child
return cfrc
cfrc = scan.body_tree(m, cfrc_fn, 'b', 'b', loc_cfrc, reverse=True)
qfrc_bias = jax.vmap(jp.dot)(d.cdof, cfrc[jp.array(m.dof_bodyid)])
d = d.replace(qfrc_bias=qfrc_bias)
return d
def transmission(m: Model, d: Data) -> Data:
"""Computes actuator/transmission lengths and moments."""
if not m.nu:
return d
def fn(gear, jnt_typ, m_i, m_j, qpos):
# handles joint transmissions only
if jnt_typ == JointType.FREE:
length = jp.zeros(1)
moment = gear
m_i = jp.repeat(m_i, 6)
m_j = m_j + jp.arange(6)
elif jnt_typ == JointType.BALL:
axis, _ = math.quat_to_axis_angle(qpos)
length = jp.dot(axis, gear[:3])[None]
moment = gear[:3]
m_i = jp.repeat(m_i, 3)
m_j = m_j + jp.arange(3)
elif jnt_typ in (JointType.SLIDE, JointType.HINGE):
length = qpos * gear[0]
moment = gear[:1]
m_i, m_j = m_i[None], m_j[None]
else:
raise RuntimeError(f'unrecognized joint type: {jnt_typ}')
return length, moment, m_i, m_j
length, m_val, m_i, m_j = scan.flat(
m,
fn,
'ujujq',
'uvvv',
m.actuator_gear,
m.jnt_type,
jp.arange(m.nu),
jp.array(m.jnt_dofadr),
d.qpos,
group_by='u',
)
moment = jp.zeros((m.nu, m.nv)).at[m_i, m_j].set(m_val)
length = length.reshape((m.nu,))
d = d.replace(actuator_length=length, actuator_moment=moment)
return d
+166
View File
@@ -0,0 +1,166 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Tests for smooth dynamics functions."""
from absl.testing import absltest
from absl.testing import parameterized
import jax
from jax import numpy as jp
import mujoco
from mujoco import mjx
from mujoco.mjx._src import test_util
# pylint: disable=g-importing-member
from mujoco.mjx._src.types import DisableBit
# pylint: enable=g-importing-member
import numpy as np
def _assert_eq(a, b, name, step, fname, atol=1e-5, rtol=1e-5):
err_msg = f'mismatch: {name} at step {step} in {fname}'
np.testing.assert_allclose(a, b, err_msg=err_msg, atol=atol, rtol=rtol)
def _assert_attr_eq(a, b, attr, step, fname, atol=1e-5, rtol=1e-5):
err_msg = f'mismatch: {attr} at step {step} in {fname}'
a, b = getattr(a, attr), getattr(b, attr)
np.testing.assert_allclose(a, b, err_msg=err_msg, atol=atol, rtol=rtol)
class SmoothTest(parameterized.TestCase):
@parameterized.parameters(enumerate(test_util.TEST_FILES))
def test_smooth(self, seed, fname):
"""Tests mujoco mj smooth functions match mujoco_mjx smooth functions."""
if fname in ('convex.xml', 'weld.xml'):
return
np.random.seed(seed)
m = test_util.load_test_file(fname)
d = mujoco.MjData(m)
kinematics_jit_fn = jax.jit(mjx.kinematics)
com_pos_jit_fn = jax.jit(mjx.com_pos)
crb_jit_fn = jax.jit(mjx.crb)
factor_m_fn = jax.jit(mjx.factor_m)
com_vel_jit_fn = jax.jit(mjx.com_vel)
rne_jit_fn = jax.jit(mjx.rne)
mul_m_jit_fn = jax.jit(mjx.mul_m)
transmission_jit_fn = jax.jit(mjx.transmission)
mx = mjx.device_put(m)
dx = mjx.make_data(mx)
# give the system a little kick to ensure we have non-identity rotations
d.qvel = np.random.random(m.nv)
for i in range(100):
qpos, qvel = d.qpos.copy(), d.qvel.copy()
mujoco.mj_step(m, d)
# kinematics
dx = kinematics_jit_fn(mx, dx.replace(qpos=qpos, qvel=qvel))
_assert_attr_eq(d, dx, 'xanchor', i, fname)
_assert_attr_eq(d, dx, 'xaxis', i, fname)
_assert_attr_eq(d, dx, 'xpos', i, fname)
_assert_attr_eq(d, dx, 'xquat', i, fname)
_assert_eq(d.xmat.reshape((-1, 3, 3)), dx.xmat, 'xmat', i, fname)
_assert_attr_eq(d, dx, 'xipos', i, fname)
_assert_eq(d.ximat.reshape((-1, 3, 3)), dx.ximat, 'ximat', i, fname)
_assert_attr_eq(d, dx, 'geom_xpos', i, fname)
_assert_eq(
d.geom_xmat.reshape((-1, 3, 3)),
dx.geom_xmat,
'geom_xmat',
i,
fname,
)
# com_pos
dx = com_pos_jit_fn(mx, dx)
_assert_attr_eq(d, dx, 'subtree_com', i, fname)
_assert_attr_eq(d, dx, 'cinert', i, fname)
_assert_attr_eq(d, dx, 'cdof', i, fname)
# crb
dx = crb_jit_fn(mx, dx)
_assert_attr_eq(d, dx, 'crb', i, fname)
_assert_attr_eq(d, dx, 'qM', i, fname)
# factor_m
dx = factor_m_fn(mx, dx, dx.qM)
_assert_attr_eq(d, dx, 'qLD', i, fname, atol=1e-3)
_assert_attr_eq(d, dx, 'qLDiagInv', i, fname, atol=1e-3, rtol=1e-4)
# com_vel
dx = com_vel_jit_fn(mx, dx)
_assert_attr_eq(d, dx, 'cvel', i, fname)
_assert_attr_eq(d, dx, 'cdof_dot', i, fname)
# rne
dx = rne_jit_fn(mx, dx)
_assert_attr_eq(d, dx, 'qfrc_bias', i, fname, atol=1e-4)
# mul_m (auxilliary function, not part of smooth step)
vec = np.random.random(m.nv)
mjx_vec = mul_m_jit_fn(mx, dx, jp.array(vec))
mj_vec = np.zeros(m.nv)
mujoco.mj_mulM(m, d, mj_vec, vec)
_assert_eq(mj_vec, mjx_vec, 'mul_m', i, fname, atol=1e-4)
# transmission
dx = transmission_jit_fn(mx, dx)
_assert_attr_eq(d, dx, 'actuator_length', i, fname)
_assert_attr_eq(d, dx, 'actuator_moment', i, fname)
class DisableGravityTest(absltest.TestCase):
def test_disabled(self):
m = mujoco.MjModel.from_xml_string("""
<mujoco>
<option timestep="0.01"/>
<worldbody>
<body>
<joint type="free"/>
<geom size="0.1"/>
</body>
</worldbody>
</mujoco>
""")
mx = mjx.device_put(m)
d = mujoco.MjData(m)
dx = mjx.device_put(d)
# test with gravity
step_jit_fn = jax.jit(mjx.step)
dx = step_jit_fn(mx, dx)
np.testing.assert_array_almost_equal(
dx.qpos, np.array([0.0, 0.0, -9.81e-4, 1.0, 0.0, 0.0, 0.0]), decimal=7
)
# test with gravity disabled
mx = mx.tree_replace(
{'opt.disableflags': mx.opt.disableflags | DisableBit.GRAVITY}
)
dx = mjx.device_put(d)
step_jit_fn = jax.jit(mjx.step)
dx = step_jit_fn(mx, dx)
np.testing.assert_equal(
dx.qpos, np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0])
)
if __name__ == '__main__':
absltest.main()
+356
View File
@@ -0,0 +1,356 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""CG and Newton solvers."""
from typing import Optional
import jax
from jax import numpy as jp
import mujoco
from mujoco.mjx._src import math
from mujoco.mjx._src import smooth
# pylint: disable=g-importing-member
from mujoco.mjx._src.dataclasses import PyTreeNode
from mujoco.mjx._src.types import Data
from mujoco.mjx._src.types import DisableBit
from mujoco.mjx._src.types import Model
# pylint: enable=g-importing-member
class _CGContext(PyTreeNode):
"""Data updated during each cg solver iteration.
Attributes:
qacc: acceleration (from Data) (nv,)
qfrc_constraint: constraint force (from Data) (nv,)
Jaref: Jac*qacc - aref (nefc,)
efc_force: constraint force in constraint space (nefc,)
M: dense mass matrix, populated for nv < 100 (nv, nv)
Ma: M*qacc (nv,)
grad: gradient of master cost (nv,)
Mgrad: M / grad (nv,)
search: linesearch vector (nv,)
gauss: gauss Cost
cost: constraint + Gauss cost
prev_cost: cost from previous cg iter
solver_niter: number of solver iterations
"""
qacc: jax.Array
qfrc_constraint: jax.Array
Jaref: jax.Array # pylint: disable=invalid-name
efc_force: jax.Array
M: Optional[jax.Array]
Ma: jax.Array # pylint: disable=invalid-name
grad: jax.Array
Mgrad: jax.Array # pylint: disable=invalid-name
search: jax.Array
gauss: jax.Array
cost: jax.Array
prev_cost: jax.Array
solver_niter: jax.Array
@classmethod
def create(cls, m: Model, d: Data, grad: bool = True) -> '_CGContext':
jaref = d.efc_J @ d.qacc - d.efc_aref
# TODO(robotics-team): determine nv at which sparse mul is faster
M = smooth.dense_m(m, d) if m.nv < 100 else None # pylint: disable=invalid-name
ma = smooth.mul_m(m, d, d.qacc) if M is None else M @ d.qacc
nv_0 = jp.zeros((m.nv,))
ctx = _CGContext(
qacc=d.qacc,
qfrc_constraint=d.qfrc_constraint,
Jaref=jaref,
efc_force=-jaref * d.efc_D,
M=M,
Ma=ma,
grad=nv_0,
Mgrad=nv_0,
search=nv_0,
gauss=0.0,
cost=jp.inf,
prev_cost=0.0,
solver_niter=0,
)
ctx = _cg_update_constraint(m, d, ctx)
if grad:
ctx = _cg_update_gradient(m, d, ctx)
ctx = ctx.replace(search=-ctx.Mgrad) # start with preconditioned gradient
return ctx
class _LSPoint(PyTreeNode):
"""Line search evaluation point.
Attributes:
alpha: step size that reduces f(x + alpha * p) given search direction p
cost: line search cost
deriv_0: first derivative of quadratic
deriv_1: second derivative of quadratic
"""
alpha: jax.Array
cost: jax.Array
deriv_0: jax.Array
deriv_1: jax.Array
@classmethod
def create(
cls,
ctx: _CGContext,
alpha: jax.Array,
jv: jax.Array,
quad: jax.Array,
quad_gauss: jax.Array,
) -> '_LSPoint':
"""Creates a linesearch point with first and second derivatives."""
# roughly corresponds to CGEval in mujoco/src/engine/engine_solver.c
# TODO(robotics-team): change this to support equality, friction constraints
active = (ctx.Jaref + alpha * jv) < 0
quad = jax.vmap(jp.multiply)(quad, active) # only active
quad_total = quad_gauss + jp.sum(quad, axis=0)
cost = alpha * alpha * quad_total[2] + alpha * quad_total[1] + quad_total[0]
deriv_0 = 2 * alpha * quad_total[2] + quad_total[1]
deriv_1 = 2 * quad_total[2]
return _LSPoint(alpha=alpha, cost=cost, deriv_0=deriv_0, deriv_1=deriv_1)
class _LSContext(PyTreeNode):
"""Data updated during each cg line search iteration.
Attributes:
lo: low point bounding the line search interval
hi: high point bounding the line search interval
swap: True if low or hi was swapped in the line search iteration
ls_iter: number of linesearch iterations
"""
lo: _LSPoint
hi: _LSPoint
swap: jax.Array
ls_iter: jax.Array
def _while_loop_scan(cond_fun, body_fun, init_val, max_iter):
"""Scan-based implementation (jit ok, reverse-mode autodiff ok)."""
def _iter(val):
next_val = body_fun(val)
next_cond = cond_fun(next_val)
return next_val, next_cond
def _fun(tup, it):
val, cond = tup
# When cond is met, we start doing no-ops.
return jax.lax.cond(cond, _iter, lambda x: (x, False), val), it
init = (init_val, cond_fun(init_val))
return jax.lax.scan(_fun, init, None, length=max_iter)[0][0]
def _cg_update_constraint(m: Model, d: Data, ctx: _CGContext) -> _CGContext:
"""Updates constraint force and resulting cost given latst CG iteration.
Corresponds to CGupdateConstraint in mujoco/src/engine/engine_solver.c
Args:
m: model defining constraints
d: data which contains latest qacc and smooth terms
ctx: current CG context
Returns:
context with new constraint force and costs
"""
del m
# TODO(robotics-team): add equality, friction constraints
# also consider moving to _constraint.py to match mujoco layout
jaref = ctx.Jaref * (ctx.Jaref < 0) # non-negative constraints
efc_force = -jaref * d.efc_D
qfrc_constraint = d.efc_J.T @ efc_force
gauss = 0.5 * jp.dot(ctx.Ma - d.qfrc_smooth, ctx.qacc - d.qacc_smooth)
cost = 0.5 * jp.sum(jaref * jaref * d.efc_D) + gauss
ctx = ctx.replace(
qfrc_constraint=qfrc_constraint,
gauss=gauss,
cost=cost,
prev_cost=ctx.cost,
efc_force=efc_force,
)
return ctx
def _cg_update_gradient(m: Model, d: Data, ctx: _CGContext) -> _CGContext:
"""Updates grad and M / grad given latest CG iteration.
Corresponds to CGupdateGradient in mujoco/src/engine/engine_solver.c
Args:
m: model defining constraints
d: data which contains latest smooth terms
ctx: current CG contet
Returns:
context with new grad and M / grad
"""
grad = ctx.Ma - d.qfrc_smooth - ctx.qfrc_constraint
mgrad = smooth.solve_m(m, d, grad)
ctx = ctx.replace(grad=grad, Mgrad=mgrad)
return ctx
def _rescale(m: Model, value: jax.Array) -> jax.Array:
return value / (m.stat.meaninertia * max(1, m.nv))
def _cg_search(m: Model, d: Data, ctx: _CGContext) -> _CGContext:
"""Performs a zoom linesearch to find optimal search step size.
Args:
m: model defining search options and other needed terms
d: data with inertia matrix and other needed terms
ctx: current CG context
Returns:
updated context with new qacc, Ma, Jaref
"""
smag = math.norm(ctx.search) * m.stat.meaninertia * max(1, m.nv)
gtol = m.opt.tolerance * m.opt.ls_tolerance * smag
# compute Mv, Jv
mv = smooth.mul_m(m, d, ctx.search) if ctx.M is None else ctx.M @ ctx.search
jv = d.efc_J @ ctx.search
# prepare quadratics
quad_gauss = jp.stack((
ctx.gauss,
jp.dot(ctx.search, ctx.Ma) - jp.dot(ctx.search, d.qfrc_smooth),
0.5 * jp.dot(ctx.search, mv),
))
quad = jp.stack((0.5 * ctx.Jaref * ctx.Jaref, jv * ctx.Jaref, 0.5 * jv * jv))
quad = (quad * d.efc_D).T
point_fn = lambda alpha: _LSPoint.create(ctx, alpha, jv, quad, quad_gauss)
def cond(ctx: _LSContext) -> jax.Array:
done = ctx.ls_iter >= m.opt.ls_iterations
done |= ~ctx.swap # if we did not adjust the interval
done |= (ctx.lo.deriv_0 < 0) & (ctx.lo.deriv_0 > -gtol)
done |= (ctx.hi.deriv_0 > 0) & (ctx.hi.deriv_0 < gtol)
return ~done
def body(ctx: _LSContext) -> _LSContext:
# always compute new bracket boundaries and a midpoint
lo, hi = ctx.lo, ctx.hi
lo_next = point_fn(lo.alpha - lo.deriv_0 / lo.deriv_1)
hi_next = point_fn(hi.alpha - hi.deriv_0 / hi.deriv_1)
mid = point_fn(0.5 * (lo.alpha + hi.alpha))
# we swap lo/hi if:
# 1) they are not correctly at a bracket boundary (e.g. lo.deriv_0 > 0), OR
# 2) if moving to next or mid narrows the bracket
swap_lo_next = (lo.deriv_0 > 0) | (lo.deriv_0 < lo_next.deriv_0)
lo = jax.tree_map(lambda x, y: jp.where(swap_lo_next, y, x), lo, lo_next)
swap_lo_mid = (mid.deriv_0 < 0) & (lo.deriv_0 < mid.deriv_0)
lo = jax.tree_map(lambda x, y: jp.where(swap_lo_mid, y, x), lo, mid)
swap_hi_next = (hi.deriv_0 < 0) | (hi.deriv_0 > hi_next.deriv_0)
hi = jax.tree_map(lambda x, y: jp.where(swap_hi_next, y, x), hi, hi_next)
swap_hi_mid = (mid.deriv_0 > 0) & (hi.deriv_0 > mid.deriv_0)
hi = jax.tree_map(lambda x, y: jp.where(swap_hi_mid, y, x), hi, mid)
swap = swap_lo_next | swap_lo_mid | swap_hi_next | swap_hi_mid
ctx = ctx.replace(lo=lo, hi=hi, swap=swap, ls_iter=ctx.ls_iter + 1)
return ctx
# initialize interval
p0 = point_fn(jp.array(0.0))
lo = point_fn(p0.alpha - p0.deriv_0 / p0.deriv_1)
lesser_fn = lambda x, y: jp.where(lo.deriv_0 < p0.deriv_0, x, y)
hi = jax.tree_map(lesser_fn, p0, lo)
lo = jax.tree_map(lesser_fn, lo, p0)
ls_ctx = _LSContext(lo=lo, hi=hi, swap=jp.array(True), ls_iter=0)
ls_ctx = _while_loop_scan(cond, body, ls_ctx, m.opt.ls_iterations)
# move to new solution if improved
lo, hi = ls_ctx.lo, ls_ctx.hi
improved = (lo.cost < p0.cost) | (hi.cost < p0.cost)
alpha = jp.where(lo.cost < hi.cost, lo.alpha, hi.alpha)
qacc = ctx.qacc + improved * ctx.search * alpha
ma = ctx.Ma + improved * mv * alpha
jaref = ctx.Jaref + improved * jv * alpha
ctx = ctx.replace(qacc=qacc, Ma=ma, Jaref=jaref)
return ctx
def cg_solve(m: Model, d: Data) -> Data:
"""Finds forces that satisfy constraints using conjugate gradient descent."""
def cond(ctx: _CGContext) -> jax.Array:
improvement = _rescale(m, ctx.prev_cost - ctx.cost)
gradient = _rescale(m, math.norm(ctx.grad))
done = ctx.solver_niter >= m.opt.iterations
done |= improvement < m.opt.tolerance
done |= gradient < m.opt.tolerance
return ~done
def body(ctx: _CGContext) -> _CGContext:
ctx = _cg_search(m, d, ctx)
prev_grad, prev_Mgrad = ctx.grad, ctx.Mgrad # pylint: disable=invalid-name
ctx = _cg_update_constraint(m, d, ctx)
ctx = _cg_update_gradient(m, d, ctx)
# polak-ribiere:
beta = jp.dot(ctx.grad, ctx.Mgrad - prev_Mgrad)
beta = beta / jp.maximum(mujoco.mjMINVAL, jp.dot(prev_grad, prev_Mgrad))
beta = jp.maximum(0, beta)
search = -ctx.Mgrad + beta * ctx.search
ctx = ctx.replace(search=search, solver_niter=ctx.solver_niter + 1)
return ctx
# warmstart:
qacc = d.qacc_smooth
if not m.opt.disableflags & DisableBit.WARMSTART:
warm = _CGContext.create(m, d.replace(qacc=d.qacc_warmstart), grad=False)
smth = _CGContext.create(m, d.replace(qacc=d.qacc_smooth), grad=False)
qacc = jp.where(warm.cost < smth.cost, d.qacc_warmstart, d.qacc_smooth)
d = d.replace(qacc=qacc)
ctx = jax.lax.while_loop(cond, body, _CGContext.create(m, d))
d = d.replace(
qacc_warmstart=ctx.qacc,
qacc=ctx.qacc,
qfrc_constraint=ctx.qfrc_constraint,
efc_force=ctx.efc_force,
)
return d
+130
View File
@@ -0,0 +1,130 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Tests for forward functions."""
from absl.testing import absltest
from absl.testing import parameterized
from etils import epath
import jax
import mujoco
from mujoco import mjx
import numpy as np
def _assert_attr_eq(a, b, attr, step, fname, atol=1e-2, rtol=1e-2):
err_msg = f'mismatch: {attr} at step {step} in {fname}'
a, b = getattr(a, attr), getattr(b, attr)
np.testing.assert_allclose(a, b, err_msg=err_msg, atol=atol, rtol=rtol)
class Solver64Test(parameterized.TestCase):
"""Tests solvers at 64 bit precision."""
def setUp(self):
super().setUp()
jax.config.update('jax_enable_x64', True)
def tearDown(self):
super().tearDown()
jax.config.update('jax_enable_x64', False)
@parameterized.parameters(enumerate(('ant.xml', 'humanoid.xml')))
def test_cg(self, seed, fname):
"""Test mjx cg solver matches mujoco cg solver at 64 bit precision."""
f = epath.resource_path('mujoco.mjx') / 'test_data' / fname
m = mujoco.MjModel.from_xml_string(f.read_text())
d = mujoco.MjData(m)
mx = mjx.device_put(m)
jax.config.update('jax_enable_x64', True)
forward_jit_fn = jax.jit(mjx.forward)
# give the system a little kick to ensure we have non-identity rotations
np.random.seed(seed)
d.qvel = 0.01 * np.random.random(m.nv)
for i in range(100):
# in order to avoid re-jitting, reuse the same mj_data shape
save = d.qpos, d.qvel, d.time, d.qacc_warmstart, d.qacc_smooth
d = mujoco.MjData(m)
d.qpos, d.qvel, d.time, d.qacc_warmstart, d.qacc_smooth = save
dx = mjx.device_put(d)
mujoco.mj_step(m, d)
dx = forward_jit_fn(mx, dx)
# at 64 bits the solutions returned by the two solvers are quite close
self.assertLessEqual(dx.solver_niter[0], d.solver_niter[0])
_assert_attr_eq(d, dx, 'qfrc_constraint', i, fname)
_assert_attr_eq(d, dx, 'qacc', i, fname)
class SolverTest(parameterized.TestCase):
@parameterized.parameters(enumerate(('ant.xml', 'humanoid.xml')))
def test_cg(self, seed, fname):
"""Test mjx cg solver is close to mj at 32 bit precision.
Args:
seed: int
fname: file to test
At lower float resolution there's wiggle room in valid forces that satisfy
constraints. So instead let's mainly validate that mjx is finding solutions
with as good cost as mujoco, even if the resulting forces/accelerations
are not quite the same.
"""
f = epath.resource_path('mujoco.mjx') / 'test_data' / fname
m = mujoco.MjModel.from_xml_string(f.read_text())
d = mujoco.MjData(m)
mx = mjx.device_put(m)
forward_jit_fn = jax.jit(mjx.forward)
# give the system a little kick to ensure we have non-identity rotations
np.random.seed(seed)
d.qvel = 0.01 * np.random.random(m.nv)
for i in range(100):
# in order to avoid re-jitting, reuse the same mj_data shape
save = d.qpos, d.qvel, d.time, d.qacc_warmstart, d.qacc_smooth
d = mujoco.MjData(m)
d.qpos, d.qvel, d.time, d.qacc_warmstart, d.qacc_smooth = save
dx = mjx.device_put(d)
mujoco.mj_step(m, d)
dx = forward_jit_fn(mx, dx)
def cost(qacc):
jaref = np.zeros(d.nefc)
mujoco.mj_mulJacVec(m, d, jaref, qacc)
jaref -= d.efc_aref
cost = np.array([0.0])
mujoco.mj_constraintUpdate(m, d, jaref, cost, 0)
return cost[0]
cost_mj, cost_mjx = cost(d.qacc), cost(dx.qacc)
self.assertLessEqual(
cost_mjx,
cost_mj * 1.01,
msg=f'mismatch: {fname} at step {i}, cost too high',
)
_assert_attr_eq(d, dx, 'qfrc_constraint', i, fname, atol=1e-1, rtol=1e-1)
_assert_attr_eq(d, dx, 'qacc', i, fname, atol=1e-1, rtol=1e-1)
if __name__ == '__main__':
absltest.main()
+81
View File
@@ -0,0 +1,81 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Engine support functions."""
from typing import Tuple
import jax
from jax import numpy as jp
from mujoco.mjx._src import scan
# pylint: disable=g-importing-member
from mujoco.mjx._src.types import Data
from mujoco.mjx._src.types import Model
# pylint: enable=g-importing-member
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."""
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]]
jacp = jax.vmap(lambda a, b=offset: a[3:] + jp.cross(a[:3], b))(d.cdof)
jacp = jax.vmap(jp.multiply)(jacp, mask)
jacr = jax.vmap(jp.multiply)(d.cdof[:, :3], mask)
return jacp, jacr
def jac_dif_pair(
m: Model,
d: Data,
pos: jax.Array,
body_1: jax.Array,
body_2: jax.Array,
) -> jax.Array:
"""Compute Jacobian difference for two body points."""
jacp2, _ = jac(m, d, pos, body_2)
jacp1, _ = jac(m, d, pos, body_1)
return jacp2 - jacp1
def apply_ft(
m: Model,
d: Data,
force: jax.Array,
torque: jax.Array,
point: jax.Array,
body_id: jax.Array,
) -> jax.Array:
"""Apply Cartesian force and torque."""
jacp, jacr = jac(m, d, point, body_id)
return jacp @ force + jacr @ torque
def xfrc_accumulate(m: Model, d: Data) -> jax.Array:
"""Accumulate xfrc_applied into a qfrc."""
qfrc = jax.vmap(apply_ft, in_axes=(None, None, 0, 0, 0, 0))(
m,
d,
d.xfrc_applied[:, :3],
d.xfrc_applied[:, 3:],
d.xipos,
jp.arange(m.nbody),
)
return jp.sum(qfrc, axis=0)
+81
View File
@@ -0,0 +1,81 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Tests for support."""
from absl.testing import absltest
from absl.testing import parameterized
import jax
from jax import numpy as jp
import mujoco
from mujoco import mjx
from mujoco.mjx._src import support
from mujoco.mjx._src import test_util
import numpy as np
class SupportTest(parameterized.TestCase):
@parameterized.parameters(set(test_util.TEST_FILES) - {'convex.xml'})
def test_jac(self, fname):
np.random.seed(0)
m = test_util.load_test_file(fname)
d = mujoco.MjData(m)
mujoco.mj_step(m, d)
mx = mjx.device_put(m)
dx = mjx.device_put(d)
point = np.random.randn(3)
body = np.random.choice(m.nbody)
jacp, jacr = jax.jit(support.jac)(mx, dx, point, body)
jacp_expected, jacr_expected = np.zeros((3, m.nv)), np.zeros((3, m.nv))
mujoco.mj_jac(m, d, jacp_expected, jacr_expected, point, body)
np.testing.assert_almost_equal(jacp, jacp_expected.T, 6)
np.testing.assert_almost_equal(jacr, jacr_expected.T, 6)
def test_xfrc_accumulate(self):
"""Tests that xfrc_accumulate ouput matches mj_xfrcAccumulate."""
np.random.seed(0)
m = test_util.load_test_file('ant.xml')
d = mujoco.MjData(m)
mujoco.mj_step(m, d)
mx = mjx.device_put(m)
dx = mjx.device_put(d)
self.assertFalse((dx.xipos == 0.0).all())
xfrc = np.random.rand(*dx.xfrc_applied.shape)
d.xfrc_applied[:] = xfrc
dx = dx.replace(xfrc_applied=jp.array(xfrc))
qfrc = jax.jit(support.xfrc_accumulate)(mx, dx)
qfrc_expected = np.zeros(m.nv)
for i in range(1, m.nbody):
mujoco.mj_applyFT(
m,
d,
d.xfrc_applied[i, :3],
d.xfrc_applied[i, 3:],
d.xipos[i],
i,
qfrc_expected,
)
np.testing.assert_almost_equal(qfrc, qfrc_expected, 6)
if __name__ == '__main__':
absltest.main()
+340
View File
@@ -0,0 +1,340 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Utilities for testing."""
import sys
from typing import Dict, List, Tuple
from xml.etree import ElementTree as ET
from etils import epath
import mujoco
import numpy as np
TEST_FILES: List[str] = [
'ant.xml',
'ball_pendulum.xml',
'cherry_pendulum.xml',
'convex.xml',
'humanoid.xml',
'mixed_joint_pendulum.xml',
'single_pendulum.xml',
'slide_pendulum.xml',
'triple_pendulum.xml',
'triple_pendulum_free.xml',
'weld.xml',
]
_ACTUATOR_TYPES = ['motor', 'velocity', 'position', 'general', 'intvelocity']
_JOINT_TYPES = ['free', 'hinge', 'slide', 'ball']
_JOINT_AXES = ['1 0 0', '0 1 0', '0 0 1']
_FRICTIONS = ['1.2 0.003 0.0002', '0.2 0.0001 0.0005']
_KP_POS = ['1', '2']
_KP_INTVEL = ['10000', '2000']
_KV_VEL = ['123', '1']
_PAIR_FRICTIONS = ['1.2 0.9 0.003 0.0002 0.0001']
_SOLREFS = ['0.04 1.01', '0.05 1.02', '0.03 1.1', '0.015 1.0']
_SOLIMPS = [
'0.75 0.94 0.002 0.2 2',
'0.8 0.99 0.001 0.3 6',
'0.6 0.9 0.003 0.1 1',
]
_DIMS = ['3']
_MARGINS = ['0.0', '0.01', '0.02']
_GAPS = ['0.0', '0.005']
_GEARS = ['20', '50', '100']
def p(pct: int) -> bool:
assert 0 <= pct <= 100
return np.random.uniform(low=0, high=100) < pct
def _make_joint(joint_type: str, name: str) -> Dict[str, str]:
"""Returns attributes for a joint."""
joint_attr = {'type': joint_type, 'name': name}
if joint_type not in ('free', 'ball'):
joint_attr['axis'] = np.random.choice(_JOINT_AXES)
lb, ub = -np.random.uniform() * 90, np.random.uniform() * 90
joint_attr['range'] = f'{lb:.2f} {ub:.2f}'
elif joint_type == 'ball':
joint_attr['axis'] = '1 0 0'
ub = np.random.uniform() * 90
joint_attr['range'] = f'0.0 {ub:.2f}'
if p(50) and joint_type != 'free':
lb, ub = -np.random.uniform(), np.random.uniform()
joint_attr['actuatorfrcrange'] = f'{lb:.2f} {ub:.2f}'
if joint_type not in ('free',):
joint_attr['damping'] = '{:.2f}'.format(np.random.uniform() * 20)
joint_attr['stiffness'] = '{:.2f}'.format(np.random.uniform() * 20)
return joint_attr
def _geom_solparams(
pair: bool = False, enable_contact: bool = True
) -> Dict[str, str]:
"""Returns geom solver parameters."""
params = {
'contype': np.random.choice(['0', '1']) if enable_contact else '0',
'conaffinity': np.random.choice(['0', '1']) if enable_contact else '0',
'priority': np.random.choice(['-1', '2']),
'solmix': np.random.choice(['0.0', '1.6']),
'friction': np.random.choice(_FRICTIONS),
'condim': np.random.choice(_DIMS),
}
pair_params = {
'solreffriction': np.random.choice(_SOLREFS),
'friction': np.random.choice(_PAIR_FRICTIONS),
'condim': np.random.choice(_DIMS),
}
params = pair_params if pair else params
params.update({
'solimp': np.random.choice(_SOLIMPS),
'solref': np.random.choice(_SOLREFS),
'margin': np.random.choice(_MARGINS),
'gap': np.random.choice(_GAPS),
})
return params
def _make_geom(
pos: str, size: float, name: str, enable_contact: bool = True
) -> Dict[str, str]:
"""Returns attributes for a sphere geom."""
attr = {
'pos': pos,
'type': 'sphere',
'name': name,
'size': f'{size:.2f}',
'mass': '1',
}
attr.update(_geom_solparams(pair=False, enable_contact=enable_contact))
return attr
def _make_actuator(actuator_type: str, joint: str) -> Dict[str, str]:
"""Returns attributes for an actuator."""
attr = {'joint': joint}
if actuator_type == 'motor':
attr['gear'] = np.random.choice(_GEARS)
elif actuator_type == 'position':
attr['kp'] = np.random.choice(_KP_POS)
elif actuator_type == 'general':
attr['biastype'] = 'affine'
attr['gainprm'] = '35 0 0'
attr['biasprm'] = '0 -35 -0.65'
elif actuator_type == 'intvelocity':
attr['kp'] = np.random.choice(_KP_INTVEL)
lb, ub = -np.random.uniform(), np.random.uniform()
attr['actrange'] = f'{lb:.2f} {ub:.2f}'
elif actuator_type == 'velocity':
attr['kv'] = np.random.choice(_KV_VEL)
if p(50) and actuator_type != 'intvelocity':
lb, ub = -np.random.uniform(), np.random.uniform()
attr['ctrlrange'] = f'{lb:.2f} {ub:.2f}'
if p(50):
lb, ub = -np.random.uniform(), np.random.uniform()
attr['forcerange'] = f'{lb*10:.2f} {ub*10:.2f}'
return attr
def create_mjcf(
seed: int,
min_trees: int = 1,
max_trees: int = 1,
max_tree_depth: int = 5,
body_pos: Tuple[float, float, float] = (0.0, 0.0, -0.5),
geom_pos: Tuple[float, float, float] = (0.0, 0.0, 0.0),
max_stacked_joints=4,
max_geoms_per_body=2,
max_contact_excludes=1,
max_contact_pairs=4,
disable_actuation_pct: int = 0,
add_actuators: bool = False,
root_always_free: bool = False,
enable_contact: bool = True,
) -> str:
"""Creates a random MJCF for testing.
Args:
seed: seed for rng
min_trees: minimum number of kinematic trees to generate
max_trees: maximum number of kinematic trees to generate
max_tree_depth: the maximum tree depth
body_pos: the default body position relative to the parent
geom_pos: the default geom position in the body frame
max_stacked_joints: maximum number of joints to stack for each body
max_geoms_per_body: maximum number of geoms per body
max_contact_excludes: maximum number of bodies to exlude from contact
max_contact_pairs: maximum number of explicit geom contact pairs in the xml
disable_actuation_pct: the percentage of time to disable actuation via the
disable flag
add_actuators: whether to add actuators
root_always_free: if True, the root body of each kinematic tree has a free
joint with the world
enable_contact: if False, disables all contacts via contype/conaffinity
Returns:
an XML string for the MuJoCo config
Raises:
AssertionError when args are not in the correct ranges
"""
np.random.seed(seed)
assert min_trees <= max_trees
assert max_tree_depth >= 1
assert 0 <= disable_actuation_pct <= 100
assert max_stacked_joints >= 1
assert max_geoms_per_body >= 1
assert max_contact_excludes >= 1
assert max_contact_pairs >= 1
mjcf = ET.Element('mujoco')
opt = ET.SubElement(mjcf, 'option', {'timestep': '0.005', 'solver': 'CG'})
world = ET.SubElement(mjcf, 'worldbody')
ET.SubElement(mjcf, 'compiler', {'autolimits': 'true'})
# disable flags
if p(disable_actuation_pct):
ET.SubElement(opt, 'flag', {'actuation': 'disable'})
ET.SubElement(
world,
'geom',
{
'name': 'plane',
'type': 'plane',
'contype': '1' if enable_contact else '0',
'conaffinity': '1' if enable_contact else '0',
'size': '40 40 40',
},
)
# kinematic trees
tree_depth = np.random.randint(1, max_tree_depth + 1)
def make_tree(body: ET.Element, depth: int) -> None:
if depth >= tree_depth:
return
z_pos = np.random.uniform(low=-1, high=1) * 0.01 # small jitter
pos = f'{body_pos[0]:.3f} {body_pos[1]:.3f} {body_pos[2] + z_pos:.3f}'
n_bodies = len(list(mjcf.iter('body')))
child = ET.SubElement(body, 'body', {'pos': pos, 'name': f'body{n_bodies}'})
n_joints = len(list(mjcf.iter('joint')))
for nj in range(np.random.randint(1, max_stacked_joints + 1)):
joint_type = np.random.choice(_JOINT_TYPES)
if nj == 0 and depth == 0 and root_always_free:
joint_type = 'free'
# free joint only allowed at top level
while joint_type == 'free' and (depth > 0 or nj > 0):
joint_type = np.random.choice(_JOINT_TYPES)
joint_attr = _make_joint(joint_type, name=f'joint{n_joints + nj}')
ET.SubElement(child, 'joint', joint_attr)
prev_joints = child.findall('joint')
had_ball_or_free = any(
[j.get('type') in ('ball', 'free') for j in prev_joints]
)
if had_ball_or_free:
break # do not stack more joints
n_geoms = len(list(mjcf.iter('geom')))
for _ in range(np.random.randint(1, max_geoms_per_body + 1)):
pos = ('{:.2f} ' * 3).format(*geom_pos).strip()
size = 0.2 + np.random.uniform(low=-1, high=1) * 0.02
geom_attr = _make_geom(
pos, size, name=f'geom{n_geoms}', enable_contact=enable_contact
)
ET.SubElement(child, 'geom', geom_attr)
n_geoms += 1
make_tree(child, depth + 1)
num_trees = np.random.randint(min_trees, max_trees + 1)
for _ in range(num_trees):
make_tree(world, 0)
# actuators
if add_actuators:
actuator = ET.SubElement(mjcf, 'actuator')
n_joints = len(list(mjcf.iter('joint')))
nu = np.random.randint(1, n_joints + 1)
actuators = []
for i in range(nu):
actuator_type = np.random.choice(_ACTUATOR_TYPES)
attr = _make_actuator(actuator_type, joint=f'joint{i}')
actuators.append((actuator_type, attr))
np.random.shuffle(actuators)
for typ, attr in actuators:
ET.SubElement(actuator, typ, attr)
# contact pairs
contact = ET.SubElement(mjcf, 'contact')
geoms = list(mjcf.iter('geom'))
geom_names = [geom.get('name') for geom in geoms]
n_geoms = len(geoms)
pairs = set()
for _ in range(min(max_contact_pairs, n_geoms * (n_geoms - 1) // 2)):
if p(80):
continue
geom1, geom2 = np.random.choice(geom_names, replace=False, size=2)
if geom1 > geom2:
geom1, geom2 = geom2, geom1
if (geom1, geom2) in pairs:
continue
pairs.add((geom1, geom2))
attr = {'geom1': geom1, 'geom2': geom2}
attr.update(_geom_solparams(pair=True))
ET.SubElement(contact, 'pair', attr)
# exclude contacts
bodies = list(mjcf.iter('body'))
body_names = [b.get('name') for b in bodies]
n_bodies = len(bodies)
for _ in range(min(max_contact_excludes, (n_bodies * (n_bodies - 1) // 2))):
if p(50):
continue
body1, body2 = np.random.choice(body_names, replace=False, size=2)
ET.SubElement(contact, 'exclude', {'body1': body1, 'body2': body2})
# ElementTree.indent is not available before Python 3.9
if sys.version_info.minor >= 9:
ET.indent(mjcf)
return ET.tostring(mjcf).decode('utf-8')
def load_test_file(name: str) -> mujoco.MjModel:
"""Loads a mujoco.MjModel based on the file name."""
path = epath.resource_path('mujoco.mjx') / 'test_data' / name
m = mujoco.MjModel.from_xml_path(path.as_posix())
return m
+38
View File
@@ -0,0 +1,38 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Tests for the test_util."""
from absl.testing import absltest
from etils import epath
from mujoco.mjx._src import test_util
class TestUtilTest(absltest.TestCase):
def test_files_in_test_data_match(self):
directory = epath.resource_path('mujoco.mjx') / 'test_data'
files = set([f.name for f in directory.glob('*.xml')])
self.assertSetEqual(
files,
set(test_util.TEST_FILES),
msg=(
'`_test_util.TEST_FILES` must match the files in the '
'test_data/*.xml directory'
),
)
if __name__ == '__main__':
absltest.main()
+660
View File
@@ -0,0 +1,660 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Base types used in MJX."""
import enum
from typing import Sequence
import jax
import jax.numpy as jp
import mujoco
# pylint: disable=g-importing-member
from mujoco.mjx._src.dataclasses import PyTreeNode
# pylint: enable=g-importing-member
import numpy as np
class DisableBit(enum.IntFlag):
"""Disable default feature bitflags.
Attributes:
CONSTRAINT: entire constraint solver
EQUALITY: equality constraints
FRICTIONLOSS: joint and tendon frictionloss constraints
LIMIT: joint and tendon limit constraints
CONTACT: contact constraints
PASSIVE: passive forces
GRAVITY: gravitational forces
CLAMPCTRL: clamp control to specified range
WARMSTART: warmstart constraint solver
ACTUATION: apply actuation forces
REFSAFE: integrator safety: make ref[0]>=2*timestep
"""
CONSTRAINT = mujoco.mjtDisableBit.mjDSBL_CONSTRAINT
EQUALITY = mujoco.mjtDisableBit.mjDSBL_EQUALITY
LIMIT = mujoco.mjtDisableBit.mjDSBL_LIMIT
CONTACT = mujoco.mjtDisableBit.mjDSBL_CONTACT
PASSIVE = mujoco.mjtDisableBit.mjDSBL_PASSIVE
GRAVITY = mujoco.mjtDisableBit.mjDSBL_GRAVITY
CLAMPCTRL = mujoco.mjtDisableBit.mjDSBL_CLAMPCTRL
WARMSTART = mujoco.mjtDisableBit.mjDSBL_WARMSTART
ACTUATION = mujoco.mjtDisableBit.mjDSBL_ACTUATION
REFSAFE = mujoco.mjtDisableBit.mjDSBL_REFSAFE
EULERDAMP = mujoco.mjtDisableBit.mjDSBL_EULERDAMP
FILTERPARENT = mujoco.mjtDisableBit.mjDSBL_FILTERPARENT
# unsupported: FRICTIONLOSS, SENSOR, MIDPHASE
class JointType(enum.IntEnum):
"""Type of degree of freedom.
Attributes:
FREE: global position and orientation (quat) (7,)
BALL: orientation (quat) relative to parent (4,)
SLIDE: sliding distance along body-fixed axis (1,)
HINGE: rotation angle (rad) around body-fixed axis (1,)
"""
FREE = mujoco.mjtJoint.mjJNT_FREE
BALL = mujoco.mjtJoint.mjJNT_BALL
SLIDE = mujoco.mjtJoint.mjJNT_SLIDE
HINGE = mujoco.mjtJoint.mjJNT_HINGE
def dof_width(self) -> int:
return {0: 6, 1: 3, 2: 1, 3: 1}[self.value]
def qpos_width(self) -> int:
return {0: 7, 1: 4, 2: 1, 3: 1}[self.value]
class IntegratorType(enum.IntEnum):
"""Integrator mode.
Attributes:
EULER: semi-implicit Euler
RK4: 4th-order Runge Kutta
"""
EULER = mujoco.mjtIntegrator.mjINT_EULER
RK4 = mujoco.mjtIntegrator.mjINT_RK4
# unsupported: IMPLICIT, IMPLICITFAST
class GeomType(enum.IntEnum):
"""Type of geometry.
Attributes:
PLANE: plane
HFIELD: height field
SPHERE: sphere
CAPSULE: capsule
ELLIPSOID: ellipsoid
CYLINDER: cylinder
BOX: box
MESH: mesh
"""
PLANE = mujoco.mjtGeom.mjGEOM_PLANE
HFIELD = mujoco.mjtGeom.mjGEOM_HFIELD
SPHERE = mujoco.mjtGeom.mjGEOM_SPHERE
CAPSULE = mujoco.mjtGeom.mjGEOM_CAPSULE
ELLIPSOID = mujoco.mjtGeom.mjGEOM_ELLIPSOID
CYLINDER = mujoco.mjtGeom.mjGEOM_CYLINDER
BOX = mujoco.mjtGeom.mjGEOM_BOX
MESH = mujoco.mjtGeom.mjGEOM_MESH
# unsupported: NGEOMTYPES, ARROW*, LINE, SKIN, LABEL, NONE
class ConeType(enum.IntEnum):
"""Type of friction cone.
Attributes:
PYRAMIDAL: pyramidal
"""
PYRAMIDAL = mujoco.mjtCone.mjCONE_PYRAMIDAL
# unsupported: ELLIPTIC
class SolverType(enum.IntEnum):
"""Constraint solver algorithm.
Attributes:
CG: Conjugate gradient (primal)
"""
# unsupported: PGS, NEWTON
CG = mujoco.mjtSolver.mjSOL_CG
class EqType(enum.IntEnum):
"""Type of equality constraint.
Attributes:
CONNECT: connect two bodies at a point (ball joint)
WELD: fix relative position and orientation of two bodies
JOINT: couple the values of two scalar joints with cubic
"""
CONNECT = mujoco.mjtEq.mjEQ_CONNECT
WELD = mujoco.mjtEq.mjEQ_WELD
# unsupported: JOINT, TENDON, DISTANCE
class TrnType(enum.IntEnum):
"""Type of actuator transmission.
Attributes:
JOINT: force on joint
"""
JOINT = mujoco.mjtTrn.mjTRN_JOINT
# unsupported: JOINTINPARENT, SLIDERCRANK, TENDON, SITE, BODY
class DynType(enum.IntEnum):
"""Type of actuator dynamics.
Attributes:
NONE: no internal dynamics; ctrl specifies force
INTEGRATOR: integrator: da/dt = u
"""
NONE = mujoco.mjtDyn.mjDYN_NONE
INTEGRATOR = mujoco.mjtDyn.mjDYN_INTEGRATOR
FILTER = mujoco.mjtDyn.mjDYN_FILTER
# unsupported: FILTEREXACT, MUSCLE, USER
class GainType(enum.IntEnum):
"""Type of actuator gain.
Attributes:
FIXED: fixed gain
AFFINE: const + kp*length + kv*velocity
"""
FIXED = mujoco.mjtGain.mjGAIN_FIXED
AFFINE = mujoco.mjtGain.mjGAIN_AFFINE
# unsupported: MUSCLE, USER
class BiasType(enum.IntEnum):
"""Type of actuator bias.
Attributes:
NONE: no bias
AFFINE: const + kp*length + kv*velocity
"""
NONE = mujoco.mjtBias.mjBIAS_NONE
AFFINE = mujoco.mjtBias.mjBIAS_AFFINE
# unsupported: MUSCLE, USER
class Option(PyTreeNode):
"""Physics options.
Attributes:
timestep: timestep
tolerance: main solver tolerance
ls_tolerance: CG/Newton linesearch tolerance
gravity: gravitational acceleration (3,)
wind: wind (for lift, drag and viscosity)
density: density of medium
viscosity: viscosity of medium
has_fluid_params: automatically set by mjx if wind/density/viscosity are
nonzero. Not used by mj
integrator: integration mode
cone: type of friction cone
solver: solver algorithm
integrator: integration mode
iterations: number of main solver iterations
ls_iterations: maximum number of CG/Newton linesearch iterations
disableflags: bit flags for disabling standard features
"""
timestep: jax.Array
tolerance: jax.Array
ls_tolerance: jax.Array
# unsupported: apirate, impratio, noslip_tolerance, mpr_tolerance
gravity: jax.Array
wind: jax.Array
density: jax.Array
viscosity: jax.Array
has_fluid_params: bool
# unsupported: magnetic, o_margin, o_solref, o_solimp
integrator: IntegratorType
cone: ConeType
# unsupported: jacobian
solver: SolverType
iterations: int
ls_iterations: int
# unsupported: noslip_iterations, mpr_iterations
disableflags: DisableBit
# unsupported: enableflags
class Statistic(PyTreeNode):
"""Model statistics (in qpos0).
Attributes:
meaninertia: mean diagonal inertia
"""
meaninertia: jax.Array
# unsupported: meanmass, meansize, extent, center
class Model(PyTreeNode):
"""Static model of the scene that remains unchanged with each physics step.
Attributes:
nq: number of generalized coordinates = dim(qpos)
nv: number of degrees of freedom = dim(qvel)
nu: number of actuators/controls = dim(ctrl)
na: number of activation states = dim(act)
nbody: number of bodies
njnt: number of joints
ngeom: number of geoms
nmesh: number of meshes
npair: number of predefined geom pairs
nexclude: number of excluded geom pairs
neq: number of equality constraints
nnumeric: number of numeric custom fields
nM: number of non-zeros in sparse inertia matrix
opt: physics options
stat: model statistics
qpos0: qpos values at default pose (nq,)
qpos_spring: reference pose for springs (nq,)
body_parentid: id of body's parent (nbody,)
body_rootid: id of root above body (nbody,)
body_weldid: id of body that this body is welded to (nbody,)
body_jntnum: number of joints for this body (nbody,)
body_jntadr: start addr of joints; -1: no joints (nbody,)
body_dofnum: number of motion degrees of freedom (nbody,)
body_dofadr: start addr of dofs; -1: no dofs (nbody,)
body_geomnum: number of geoms (nbody,)
body_geomadr: start addr of geoms; -1: no geoms (nbody,)
body_pos: position offset rel. to parent body (nbody, 3)
body_quat: orientation offset rel. to parent body (nbody, 4)
body_ipos: local position of center of mass (nbody, 3)
body_iquat: local orientation of inertia ellipsoid (nbody, 4)
body_mass: mass (nbody,)
body_subtreemass: mass of subtree starting at this body (nbody,)
body_inertia: diagonal inertia in ipos/iquat frame (nbody, 3)
body_invweight0: mean inv inert in qpos0 (trn, rot) (nbody, 2)
jnt_type: type of joint (mjtJoint) (njnt,)
jnt_qposadr: start addr in 'qpos' for joint's data (njnt,)
jnt_dofadr: start addr in 'qvel' for joint's data (njnt,)
jnt_bodyid: id of joint's body (njnt,)
jnt_group: group for visibility (njnt,)
jnt_limited: does joint have limits (njnt,)
jnt_solref: constraint solver reference: limit (njnt, mjNREF)
jnt_solimp: constraint solver impedance: limit (njnt, mjNIMP)
jnt_pos: local anchor position (njnt, 3)
jnt_axis: local joint axis (njnt, 3)
jnt_stiffness: stiffness coefficient (njnt,)
jnt_range: joint limits (njnt, 2)
jnt_actfrcrange: range of total actuator force (njnt, 2)
jnt_margin: min distance for limit detection (njnt,)
dof_bodyid: id of dof's body (nv,)
dof_jntid: id of dof's joint (nv,)
dof_parentid: id of dof's parent; -1: none (nv,)
dof_Madr: dof address in M-diagonal (nv,)
dof_solref: constraint solver reference:frictionloss (nv, mjNREF)
dof_solimp: constraint solver impedance:frictionloss (nv, mjNIMP)
dof_frictionloss: dof friction loss (nv,)
dof_armature: dof armature inertia/mass (nv,)
dof_damping: damping coefficient (nv,)
dof_invweight0: diag. inverse inertia in qpos0 (nv,)
dof_M0: diag. inertia in qpos0 (nv,)
geom_type: geometric type (mjtGeom) (ngeom,)
geom_contype: geom contact type (ngeom,)
geom_conaffinity: geom contact affinity (ngeom,)
geom_condim: contact dimensionality (1, 3, 4, 6) (ngeom,)
geom_bodyid: id of geom's body (ngeom,)
geom_priority: geom contact priority (ngeom,)
geom_solmix: mixing coef for solref/imp in geom pair (ngeom,)
geom_solref: constraint solver reference: contact (ngeom, mjNREF)
geom_solimp: constraint solver impedance: contact (ngeom, mjNIMP)
geom_size: geom-specific size parameters (ngeom, 3)
geom_pos: local position offset rel. to body (ngeom, 3)
geom_quat: local orientation offset rel. to body (ngeom, 4)
geom_friction: friction for (slide, spin, roll) (ngeom, 3)
geom_margin: include in solver if dist<margin-gap (ngeom,)
geom_gap: include in solver if dist<margin-gap (ngeom,)
geom_convex_face: vertex face data, MJX only (ngeom,)
geom_convex_vert: vertex data, MJX only (ngeom,)
geom_convex_edge: unique edge data, MJX only (ngeom,)
geom_convex_facenormal: normal face data, MJX only (ngeom,)
pair_dim: contact dimensionality (npair,)
pair_geom1: id of geom1 (npair,)
pair_geom2: id of geom2 (npair,)
pair_solref: solver reference: contact normal (npair, mjNREF)
pair_solreffriction: solver reference: contact friction (npair, mjNREF)
pair_solimp: solver impedance: contact (npair, mjNIMP)
pair_margin: include in solver if dist<margin-gap (npair,)
pair_gap: include in solver if dist<margin-gap (npair,)
pair_friction: tangent1, 2, spin, roll1, 2 (npair, 5)
exclude_signature: (body1+1) << 16 + body2+1 (nexclude,)
eq_type: constraint type (mjtEq) (neq,)
eq_obj1id: id of object 1 (neq,)
eq_obj2id: id of object 2 (neq,)
eq_active0: initial enable/disable constraint state (neq,)
eq_solref: constraint solver reference (neq, mjNREF)
eq_solimp: constraint solver impedance (neq, mjNIMP)
eq_data: numeric data for constraint (neq, mjNEQDATA)
actuator_trntype: transmission type (mjtTrn) (nu,)
actuator_dyntype: dynamics type (mjtDyn) (nu,)
actuator_gaintype: gain type (mjtGain) (nu,)
actuator_biastype: bias type (mjtBias) (nu,)
actuator_trnid: transmission id: joint, tendon, site (nu, 2)
actuator_actadr: first activation address; -1: stateless (nu,)
actuator_actnum: number of activation variables (nu,)
actuator_ctrllimited: is control limited (nu,)
actuator_forcelimited: is force limited (nu,)
actuator_actlimited: is activation limited (nu,)
actuator_dynprm: dynamics parameters (nu, mjNDYN)
actuator_gainprm: gain parameters (nu, mjNGAIN)
actuator_biasprm: bias parameters (nu, mjNBIAS)
actuator_ctrlrange: range of controls (nu, 2)
actuator_forcerange: range of forces (nu, 2)
actuator_actrange: range of activations (nu, 2)
actuator_gear: scale length and transmitted force (nu, 6)
numeric_adr: address of field in numeric_data (nnumeric,)
numeric_data: array of all numeric fields (nnumericdata,)
name_numericadr: numeric name pointers (nnumeric,)
names: names of all objects, 0-terminated (nnames,)
"""
nq: int
nv: int
nu: int
na: int
nbody: int
njnt: int
ngeom: int
nmesh: int
npair: int
nexclude: int
neq: int
nnumeric: int
nM: int
opt: Option
stat: Statistic
qpos0: jax.Array
qpos_spring: jax.Array
body_parentid: np.ndarray
body_rootid: np.ndarray
body_weldid: np.ndarray
body_jntnum: np.ndarray
body_jntadr: np.ndarray
body_dofnum: np.ndarray
body_dofadr: np.ndarray
body_geomnum: np.ndarray
body_geomadr: np.ndarray
body_pos: jax.Array
body_quat: jax.Array
body_ipos: jax.Array
body_iquat: jax.Array
body_mass: jax.Array
body_subtreemass: jax.Array
body_inertia: jax.Array
body_invweight0: jax.Array
jnt_type: np.ndarray
jnt_qposadr: np.ndarray
jnt_dofadr: np.ndarray
jnt_bodyid: np.ndarray
jnt_limited: np.ndarray
jnt_actfrclimited: np.ndarray
jnt_solref: jax.Array
jnt_solimp: jax.Array
jnt_pos: jax.Array
jnt_axis: jax.Array
jnt_stiffness: jax.Array
jnt_range: jax.Array
jnt_actfrcrange: jax.Array
jnt_margin: jax.Array
dof_bodyid: np.ndarray
dof_jntid: np.ndarray
dof_parentid: np.ndarray
dof_Madr: np.ndarray
dof_solref: jax.Array
dof_solimp: jax.Array
dof_frictionloss: jax.Array
dof_armature: jax.Array
dof_damping: jax.Array
dof_invweight0: jax.Array
dof_M0: jax.Array
geom_type: np.ndarray
geom_contype: np.ndarray
geom_conaffinity: np.ndarray
geom_condim: np.ndarray
geom_bodyid: np.ndarray
geom_priority: np.ndarray
geom_solmix: jax.Array
geom_solref: jax.Array
geom_solimp: jax.Array
geom_size: jax.Array
geom_pos: jax.Array
geom_quat: jax.Array
geom_friction: jax.Array
geom_margin: jax.Array
geom_gap: jax.Array
pair_dim: np.ndarray
pair_geom1: np.ndarray
pair_geom2: np.ndarray
geom_convex_face: Sequence[jax.Array]
geom_convex_vert: Sequence[jax.Array]
geom_convex_edge: Sequence[jax.Array]
geom_convex_facenormal: Sequence[jax.Array]
pair_solref: jax.Array
pair_solreffriction: jax.Array
pair_solimp: jax.Array
pair_margin: jax.Array
pair_gap: jax.Array
pair_friction: jax.Array
exclude_signature: np.ndarray
eq_type: np.ndarray
eq_obj1id: np.ndarray
eq_obj2id: np.ndarray
eq_active0: np.ndarray
eq_solref: jax.Array
eq_solimp: jax.Array
eq_data: jax.Array
actuator_trntype: np.ndarray
actuator_dyntype: np.ndarray
actuator_gaintype: np.ndarray
actuator_biastype: np.ndarray
actuator_trnid: np.ndarray
actuator_actadr: np.ndarray
actuator_actnum: np.ndarray
actuator_ctrllimited: np.ndarray
actuator_forcelimited: np.ndarray
actuator_actlimited: np.ndarray
actuator_dynprm: jax.Array
actuator_gainprm: jax.Array
actuator_biasprm: jax.Array
actuator_ctrlrange: jax.Array
actuator_forcerange: jax.Array
actuator_actrange: jax.Array
actuator_gear: jax.Array
numeric_adr: np.ndarray
numeric_data: np.ndarray
name_numericadr: np.ndarray
names: bytes
class Contact(PyTreeNode):
"""Result of collision detection functions.
Attributes:
dist: distance between nearest points; neg: penetration
pos: position of contact point: midpoint between geoms (3,)
frame: normal is in [0-2] (9,)
includemargin: include if dist<includemargin=margin-gap (1,)
friction: tangent1, 2, spin, roll1, 2 (5,)
solref: constraint solver reference, normal direction (mjNREF,)
solreffriction: constraint solver reference, friction directions (mjNREF,)
solimp: constraint solver impedance (mjNIMP,)
dim: contact space dimensionality: 1, 3, 4 or 6
geom1: id of geom 1
geom2: id of geom 2
efc_address: address in efc; -1: not included
"""
dist: jax.Array
pos: jax.Array
frame: jax.Array
includemargin: jax.Array
friction: jax.Array
solref: jax.Array
solreffriction: jax.Array
solimp: jax.Array
# unsupported: mu, H
dim: np.ndarray
geom1: jax.Array
geom2: jax.Array
efc_address: np.ndarray
# unsupported: exclude
@classmethod
def zero(cls, shape=(0,)) -> 'Contact':
"""Returns a contact filled with zeros."""
return Contact(
dist=jp.zeros(shape),
pos=jp.zeros(shape + (3,)),
frame=jp.zeros(shape + (3, 3)),
includemargin=jp.zeros(shape),
friction=jp.zeros(shape + (5,)),
solref=jp.zeros(shape + (mujoco.mjNREF,)),
solreffriction=jp.zeros(shape + (mujoco.mjNREF,)),
solimp=jp.zeros(shape + (mujoco.mjNIMP,)),
dim=np.zeros(shape, dtype=np.int32),
geom1=jp.zeros(shape, dtype=jp.int32),
geom2=jp.zeros(shape, dtype=jp.int32),
efc_address=np.zeros(shape, dtype=np.int32),
)
class Data(PyTreeNode):
"""Dynamic state that updates each step.
Attributes:
solver_niter: number of solver iterations, per island (mjNISLAND,)
ne: number of equality constraints
nf: number of friction constraints
nl: number of limit constraints
nefc: number of constraints
ncon: nubmer of contacts
time: simulation time
qpos: position (nq,)
qvel: velocity (nv,)
act: actuator activation (na,)
qacc_warmstart: acceleration used for warmstart (nv,)
ctrl: control (nu,)
qfrc_applied: applied generalized force (nv,)
xfrc_applied: applied Cartesian force/torque (nbody, 6)
eq_active: enable/disable constraints (neq,)
qacc: acceleration (nv,)
act_dot: time-derivative of actuator activation (na,)
xpos: Cartesian position of body frame (nbody, 3)
xquat: Cartesian orientation of body frame (nbody, 4)
xmat: Cartesian orientation of body frame (nbody, 3, 3)
xipos: Cartesian position of body com (nbody, 3)
ximat: Cartesian orientation of body inertia (nbody, 3, 3)
xanchor: Cartesian position of joint anchor (njnt, 3)
xaxis: Cartesian joint axis (njnt, 3)
geom_xpos: Cartesian geom position (ngeom, 3)
geom_xmat: Cartesian geom orientation (ngeom, 3, 3)
subtree_com: center of mass of each subtree (nbody, 3)
cdof: com-based motion axis of each dof (nv, 6)
cinert: com-based body inertia and mass (nbody, 10)
actuator_length: actuator lengths (nu,)
actuator_moment: actuator moments (nu, nv)
crb: com-based composite inertia and mass (nbody, 10)
qM: total inertia (sparse) (nM,)
qLD: L'*D*L factorization of M (sparse) (nM,)
qLDiagInv: 1/diag(D) (nv,)
qLDiagSqrtInv: 1/sqrt(diag(D)) (nv,)
contact: list of all detected contacts (ncon,)
efc_J: constraint Jacobian (nefc, nv)
efc_frictionloss: frictionloss (friction) (nefc,)
efc_D: constraint mass (nefc,)
actuator_velocity: actuator velocities (nu,)
cvel: com-based velocity [3D rot; 3D tran] (nbody, 6)
cdof_dot: time-derivative of cdof (nv, 6)
qfrc_bias: C(qpos,qvel) (nv,)
qfrc_passive: passive force (nv,)
efc_aref: reference pseudo-acceleration (nefc,)
actuator_force: actuator force in actuation space (nu,)
qfrc_actuator: actuator force (nv,)
qfrc_smooth: net unconstrained force (nv,)
qacc_smooth: unconstrained acceleration (nv,)
qfrc_constraint: constraint force (nv,)
qfrc_inverse: net external force; should equal: (nv,)
qfrc_applied + J'*xfrc_applied + qfrc_actuator
efc_force: constraint force in constraint space (nefc,)
"""
# solver statistics:
solver_niter: jax.Array
# sizes (variable in MJ, constant in MJX)
ne: int
nf: int
nl: int
nefc: int
ncon: int
# global properties:
time: jax.Array
# state:
qpos: jax.Array
qvel: jax.Array
act: jax.Array
qacc_warmstart: jax.Array
# control:
ctrl: jax.Array
qfrc_applied: jax.Array
xfrc_applied: jax.Array
eq_active: jax.Array
# dynamics:
qacc: jax.Array
act_dot: jax.Array
# position dependent:
xpos: jax.Array
xquat: jax.Array
xmat: jax.Array
xipos: jax.Array
ximat: jax.Array
xanchor: jax.Array
xaxis: jax.Array
geom_xpos: jax.Array
geom_xmat: jax.Array
subtree_com: jax.Array
cdof: jax.Array
cinert: jax.Array
crb: jax.Array
actuator_length: jax.Array
actuator_moment: jax.Array
qM: jax.Array
qLD: jax.Array
qLDiagInv: jax.Array
qLDiagSqrtInv: jax.Array
contact: Contact
efc_J: jax.Array
efc_frictionloss: jax.Array
efc_D: jax.Array
# position, velocity dependent:
actuator_velocity: jax.Array
cvel: jax.Array
cdof_dot: jax.Array
qfrc_bias: jax.Array
qfrc_passive: jax.Array
efc_aref: jax.Array
# position, velcoity, control & acceleration dependent:
actuator_force: jax.Array
qfrc_actuator: jax.Array
qfrc_smooth: jax.Array
qacc_smooth: jax.Array
qfrc_constraint: jax.Array
qfrc_inverse: jax.Array
efc_force: jax.Array
+14
View File
@@ -0,0 +1,14 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
+116
View File
@@ -0,0 +1,116 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Run benchmarks on various devices."""
import sys
import time
from absl import flags
from etils import epath
import google_benchmark as benchmark
import jax
from jax import numpy as jp
import mujoco
from mujoco import mjx
FLAGS = flags.FLAGS
_PATHS = {
'humanoid': 'benchmark/model/humanoid/humanoid.xml',
'barkour': 'benchmark/model/barkour_v0/assets/barkour_v0_mjx.xml',
'shadow_hand': 'benchmark/model/shadow_hand/scene_right.xml',
}
_BATCH_SIZE = {
('humanoid', 'TPU v5 lite'): 1024,
('barkour', 'TPU v5 lite'): 1024,
('shadow_hand', 'TPU v5 lite'): 1024,
('humanoid', 'Tesla V100-SXM2-16GB'): 8192,
('barkour', 'Tesla V100-SXM2-16GB'): 8192,
('shadow_hand', 'Tesla V100-SXM2-16GB'): 4096,
('humanoid', 'cpu'): 64,
('barkour', 'cpu'): 64,
('shadow_hand', 'cpu'): 64,
}
flags.DEFINE_string('model', 'humanoid', 'Model to benchmark')
flags.DEFINE_string('device', 'cpu', 'Device benchmark is running on')
def _measure_fn(state, init_fn, step_fn, batch_size: int = 1024) -> float:
"""Reports jit time and op time for a function."""
step_count = 100 if FLAGS.device == 'cpu' else 1000
@jax.jit
def run_batch(seed: jp.ndarray):
rngs = jax.random.split(jax.random.PRNGKey(seed), batch_size)
init_state = jax.vmap(init_fn)(rngs)
@jax.vmap
def run(state):
def step(state, _):
state = step_fn(state)
return state, ()
return jax.lax.scan(step, state, (), length=step_count)
return run(init_state)
# run once to jit
beg = time.perf_counter()
jax.tree_util.tree_map(lambda x: x.block_until_ready(), run_batch(0))
first_t = time.perf_counter() - beg
times = []
while state:
beg = time.perf_counter()
batch = run_batch(jp.array(len(times)))
jax.tree_util.tree_map(lambda x: x.block_until_ready(), batch)
times.append(time.perf_counter() - beg)
op_time = jp.mean(jp.array(times))
batch_sps = batch_size * step_count / op_time
state.counters['jit_time'] = first_t - op_time
state.counters['batch_sps'] = batch_sps
@benchmark.option.unit(benchmark.kSecond)
def _run(state: benchmark.State):
"""Benchmark a model."""
f = epath.resource_path('mujoco.mjx') / _PATHS[FLAGS.model]
m = mujoco.MjModel.from_xml_path(f.as_posix())
m = mjx.device_put(m)
def init(rng):
d = mjx.make_data(m)
qvel = 0.01 * jax.random.normal(rng, shape=(m.nv,))
d = d.replace(qvel=qvel)
return d
def step(d):
return mjx.step(m, d)
batch_size = _BATCH_SIZE[(FLAGS.model, jax.devices()[0].device_kind)]
_measure_fn(state, init, step, batch_size=batch_size)
if __name__ == '__main__':
FLAGS(sys.argv)
benchmark.register(_run, name=FLAGS.model + '_' + FLAGS.device)
benchmark.main()
@@ -0,0 +1,35 @@
# Google Barkour v0 Joystick Policy
## Overview
This folder contains a training script for a flat-terrain joystick policy for the [Barkour v0 Quadruped](https://ai.googleblog.com/2023/05/barkour-benchmarking-animal-level.html) which demonstrates sim2real transfer.
`barkour_joystick.py` contains the environment definition, while the [colab](https://colab.research.google.com/github/google/brax/blob/main/experimental/barkour_v0/barkour_v0_joystick.ipynb) shows how to train the policy.
<p float="left">
<img src="assets/joystick.gif" width="400">
</p>
## MJCF Instructions
The MuJoCo config in `assets/barkour_v0_mjx.xml` was copied from https://github.com/deepmind/mujoco_menagerie/google_barkour_v0. The following edits were made to the MJCF specifically for brax:
* `meshdir` was changed from `assets` to `.`.
* `frictionloss` was removed. `damping` was changed to 0.5239.
* A custom `init_qpos` was added.
* A sphere geom `lowerLegFoot` was added to all feet. All other contacts were turned off.
* The compiler option was changed to `<option timestep="0.002" iterations="4" solver="CG"/>`.
* Non-visual geoms were removed from the torso, to speed up rendering.
## Publications
If you use this work in an academic context, please cite the following publication:
@misc{caluwaerts2023barkour,
title={Barkour: Benchmarking Animal-level Agility with Quadruped Robots},
author={Ken Caluwaerts and Atil Iscen and J. Chase Kew and Wenhao Yu and Tingnan Zhang and Daniel Freeman and Kuang-Huei Lee and Lisa Lee and Stefano Saliceti and Vincent Zhuang and Nathan Batchelor and Steven Bohez and Federico Casarini and Jose Enrique Chen and Omar Cortes and Erwin Coumans and Adil Dostmohamed and Gabriel Dulac-Arnold and Alejandro Escontrela and Erik Frey and Roland Hafner and Deepali Jain and Bauyrjan Jyenis and Yuheng Kuang and Edward Lee and Linda Luu and Ofir Nachum and Ken Oslund and Jason Powell and Diego Reyes and Francesco Romano and Feresteh Sadeghi and Ron Sloat and Baruch Tabanpour and Daniel Zheng and Michael Neunert and Raia Hadsell and Nicolas Heess and Francesco Nori and Jeff Seto and Carolina Parada and Vikas Sindhwani and Vincent Vanhoucke and Jie Tan},
year={2023},
eprint={2305.14654},
archivePrefix={arXiv},
primaryClass={cs.RO}
}
@@ -0,0 +1,243 @@
<mujoco model="barkour v0 brax">
<compiler angle="radian" meshdir="." texturedir="assets" autolimits="true"/>
<option timestep="0.002" iterations="4" ls_iterations="6" solver="CG">
<flag eulerdamp="disable"/>
</option>
<statistic meansize="0.183574"/>
<visual>
<headlight diffuse="0.6 0.6 0.6" ambient="0.3 0.3 0.3" specular="0 0 0"/>
<rgba haze="0.15 0.25 0.35 1"/>
<global azimuth="120" elevation="-20"/>
</visual>
<default>
<geom contype="0" conaffinity="0" type="mesh"/>
<joint range="-1.5708 1.5708" armature="0.01090125" damping="0.5239"/>
<default class="abductor">
<joint range="-1.0472 1.0472"/>
</default>
<default class="hip_front">
<joint range="-1.22173 3.24631"/>
</default>
<default class="hip_hind">
<joint range="-1.98968 2.46091"/>
</default>
<default class="knee">
<joint range="0 2.5132" axis="0 -0.0775009 0.996992"/>
</default>
<default class="multi_mode_controlled_actuator">
<general biastype="affine" gainprm="35 0 0 0 0 0 0 0 0 0" biasprm="0 -35 -0.65 0 0 0 0 0 0 0" forcerange="-18.0 18.0" ctrlrange="-2 2"/>
</default>
<default class="visual">
<geom contype="0" conaffinity="0" density="0" group="1"/>
<default class="visual_upper_right1">
<geom rgba="0.768627 0.886275 0.952941 1"/>
</default>
<default class="visual_upper_right2">
<geom rgba="0.972549 0.529412 0.00392157 1"/>
</default>
<default class="visual_abduction">
<geom rgba="0.537255 0.854902 0.827451 1"/>
</default>
<default class="visual_foot">
<geom rgba="0.301961 0.301961 0.301961 1"/>
</default>
</default>
<default class="collision">
<geom group="2"/>
<default class="upper_right1">
<geom rgba="0.768627 0.886275 0.952941 1"/>
</default>
<default class="upper_right2">
<geom rgba="0.972549 0.529412 0.00392157 1"/>
</default>
<default class="abduction">
<geom rgba="0.537255 0.854902 0.827451 1"/>
</default>
<default class="foot">
<geom rgba="0.301961 0.301961 0.301961 1"/>
</default>
</default>
<default class="lowerLegFootLeft">
<geom type="sphere" pos="-0.191284 -0.0191638 -0.013" size="0.014" contype="1"
conaffinity="0" rgba="1 0 0 1"/>
</default>
<default class="lowerLegFootRight">
<geom type="sphere" pos="-0.191284 -0.0191638 0.013" size="0.014" contype="1"
conaffinity="0" rgba="1 0 0 1"/>
</default>
</default>
<custom>
<numeric data="0.0 0.0 0.21 1.0 0.0 0.0 0.0 0.0 0.5 1.0 0.0 0.5 1.0 0.0 0.5 1.0 0.0 0.5 1.0" name="init_qpos"/>
</custom>
<asset>
<mesh file="head.stl"/>
<mesh file="powercable.stl"/>
<mesh file="handle.stl"/>
<mesh file="head_mount.stl"/>
<mesh file="body.stl"/>
<mesh file="abduction.stl"/>
<mesh file="upper_right_2.stl"/>
<mesh file="upper_right_3.stl"/>
<mesh file="upper_right_1.stl"/>
<mesh file="lower_leg_1to1.stl"/>
<mesh file="foot.stl"/>
<mesh file="upper_left_2.stl"/>
<mesh file="upper_left_1.stl"/>
<mesh file="upper_left_3.stl"/>
<texture type="skybox" builtin="gradient" rgb1="0.3 0.5 0.7" rgb2="0 0 0" width="512" height="3072"/>
<texture type="2d" name="groundplane" builtin="checker" mark="edge" rgb1="0.2 0.3 0.4" rgb2="0.1 0.2 0.3"
markrgb="0.8 0.8 0.8" width="300" height="300"/>
<material name="groundplane" texture="groundplane" texuniform="true" texrepeat="5 5" reflectance="0.2"/>
</asset>
<worldbody>
<site name="origin"/>
<light pos="0 0 1.5" dir="0 0 -1" directional="true"/>
<camera name="default" pos="0.846 -1.465 0.916" xyaxes="0.866 0.500 0.000 -0.171 0.296 0.940"/>
<geom name="floor" size="0 0 0.05" type="plane" conaffinity="1" material="groundplane"/>
<body name="chassis">
<camera name="track" pos="0.846 -1.465 0.916" xyaxes="0.866 0.500 0.000 -0.171 0.296 0.940" mode="trackcom"/>
<freejoint/>
<inertial pos="0.0196226 -0.00015133 0.0611588" quat="0.000990813 0.68703 0.000216603 0.726628" mass="4.48878" diaginertia="0.071033 0.0619567 0.0192519"/>
<geom class="visual" pos="-0.00448404 -0.000225838 0.0576402" rgba="0.647059 0.647059 0.647059 1" mesh="head"/>
<geom class="visual" pos="-0.00448404 -0.000225838 0.0576402" rgba="0.768627 0.886275 0.952941 1" mesh="powercable"/>
<geom class="visual" pos="-0.00448404 -0.000225838 0.0576402" rgba="0.917647 0.917647 0.917647 1" mesh="handle"/>
<geom class="visual" pos="-0.00448404 -0.000225838 0.0576402" rgba="0.231373 0.380392 0.705882 1" mesh="head_mount"/>
<geom class="visual" pos="-0.00448404 -0.000225838 0.0576402" rgba="0.984314 0.517647 0.862745 1" mesh="body"/>
<body name="abduction_1" pos="0.130533 -0.056 0.0508" quat="1.30945e-06 0.161152 -2.13816e-07 0.98693">
<inertial pos="-0.0521152 0.00350917 0.0171912" quat="0.387877 0.592262 0.592492 0.384358" mass="0.639437" diaginertia="0.000866008 0.000565866 0.000479767"/>
<joint class="abductor" name="abduction_front_right" axis="-0.94806 0 0.318092"/>
<geom class="visual_abduction" pos="-0.0540394 0.0217 0.0181312" mesh="abduction"/>
<body name="upper_right_asm_1" pos="-0.0540394 0.0217 0.0181312" quat="0.284632 0.284629 -0.647292 0.647289">
<inertial pos="-0.0253655 -0.0179374 -0.0465027" quat="-0.245689 0.639007 0.153351 0.712594" mass="0.942155" diaginertia="0.00539403 0.00519403 0.000795298"/>
<joint class="hip_front" name="hip_front_right" axis="0 0 -1"/>
<geom class="visual_upper_right2" mesh="upper_right_2"/>
<geom class="visual_upper_right2" mesh="upper_right_3"/>
<geom class="visual_upper_right1" mesh="upper_right_1"/>
<body name="lower_leg_1to1_front_right" pos="-0.193523 -0.104637 -0.0792" quat="0.312742 -0.0121371 0.0368314 -0.949046">
<inertial pos="-0.0577509 -0.0097034 0.0114624" quat="-0.047103 0.705359 -0.0102465 0.70721" mass="0.169623" diaginertia="0.000828741 0.000813964 3.49901e-05"/>
<joint class="knee" name="knee_front_right"/>
<geom class="visual" pos="0.00320019 0.0240604 -0.0141615" rgba="0.32549 0.529412 0.980392 1" mesh="lower_leg_1to1"/>
<geom class="visual_foot" pos="0.00320019 0.0240604 -0.0141615" mesh="foot"/>
<geom class="lowerLegFootRight"/>
</body>
</body>
</body>
<body name="abduction_2" pos="0.130533 0.056 0.0508" quat="0.161152 1.09564e-06 0.98693 1.09564e-06">
<inertial pos="-0.0521152 0.00350917 0.0171912" quat="0.387877 0.592262 0.592492 0.384358" mass="0.639437" diaginertia="0.000866008 0.000565866 0.000479767"/>
<joint class="abductor" name="abduction_front_left" axis="0.94806 0 -0.318092"/>
<geom class="visual_abduction" pos="-0.0540394 0.0217 0.0181312" mesh="abduction"/>
<body name="upper_left_asm_1" pos="-0.0540394 0.0217 0.0181312" quat="0.671818 0.671821 0.220587 -0.220588">
<inertial pos="0.0306562 0.00629189 -0.0466005" quat="-0.113342 0.751294 0.0555641 0.647784" mass="0.938791" diaginertia="0.00538157 0.00518445 0.000790347"/>
<joint class="hip_front" name="hip_front_left" axis="0 0 1"/>
<geom class="visual" rgba="0.980392 0.713725 0.00392157 1" mesh="upper_left_2"/>
<geom class="visual" rgba="0.498039 0.498039 0.498039 1" mesh="upper_left_1"/>
<geom class="visual" rgba="1 0.756863 0.054902 1" mesh="upper_left_3"/>
<body name="lower_leg_1to1_front_left" pos="0.208835 0.0691954 -0.0792" quat="0.0386264 0.995249 0.0893024 0.0034659">
<inertial pos="-0.0577509 -0.00780463 -0.0129639" quat="-0.047103 0.705359 -0.0102465 0.70721" mass="0.169623" diaginertia="0.000828741 0.000813964 3.49901e-05"/>
<joint class="knee" name="knee_front_left"/>
<geom class="visual" pos="0.00320019 0.0259591 -0.0385878" rgba="0.32549 0.529412 0.980392 1" mesh="lower_leg_1to1"/>
<geom class="visual_foot" pos="0.00320019 0.0259591 -0.0385878" mesh="foot"/>
<geom class="lowerLegFootLeft"/>
</body>
</body>
</body>
<body name="abduction_3" pos="-0.134667 -0.056 0.0508" quat="1.30945e-06 0.98693 2.13816e-07 -0.161152">
<inertial pos="-0.0521152 0.00350917 0.0171912" quat="0.387877 0.592262 0.592492 0.384358" mass="0.639437" diaginertia="0.000866008 0.000565866 0.000479767"/>
<joint class="abductor" name="abduction_hind_right" axis="0.94806 0 -0.318092"/>
<geom class="visual_abduction" pos="-0.0540394 0.0217 0.0181312" mesh="abduction"/>
<body name="upper_right_asm_2" pos="-0.0540394 0.0217 0.0181312" quat="0.64729 0.647292 0.28463 -0.284631">
<inertial pos="-0.0253655 -0.0179374 -0.0465027" quat="-0.245689 0.639007 0.153351 0.712594" mass="0.942155" diaginertia="0.00539403 0.00519403 0.000795298"/>
<joint class="hip_hind" name="hip_hind_right" axis="0 0 -1"/>
<geom class="visual_upper_right2" mesh="upper_right_2"/>
<geom class="visual_upper_right2" mesh="upper_right_3"/>
<geom class="visual_upper_right1" mesh="upper_right_1"/>
<body name="lower_leg_1to1_hind_right" pos="-0.193523 -0.104637 -0.0792" quat="0.312742 -0.0121371 0.0368314 -0.949046">
<inertial pos="-0.0577509 -0.0097034 0.0114624" quat="-0.047103 0.705359 -0.0102465 0.70721" mass="0.169623" diaginertia="0.000828741 0.000813964 3.49901e-05"/>
<joint class="knee" name="knee_hind_right"/>
<geom class="visual" pos="0.00320019 0.0240604 -0.0141615" rgba="0.32549 0.529412 0.980392 1" mesh="lower_leg_1to1"/>
<geom class="visual_foot" pos="0.00320019 0.0240604 -0.0141615" mesh="foot"/>
<geom class="lowerLegFootRight"/>
</body>
</body>
</body>
<body name="abduction_4" pos="-0.134667 0.056 0.0508" quat="0.98693 0 -0.161152 0">
<inertial pos="-0.0521152 0.00350917 0.0171912" quat="0.387877 0.592262 0.592492 0.384358" mass="0.639437" diaginertia="0.000866008 0.000565866 0.000479767"/>
<joint class="abductor" name="abduction_hind_left" axis="-0.94806 0 0.318092"/>
<geom class="visual_abduction" pos="-0.0540394 0.0217 0.0181312" mesh="abduction"/>
<body name="upper_left_asm_2" pos="-0.0540394 0.0217 0.0181312" quat="-0.220587 -0.220588 0.67182 -0.671818">
<inertial pos="0.0306562 0.00629189 -0.0466005" quat="-0.113342 0.751294 0.0555641 0.647784" mass="0.938791" diaginertia="0.00538157 0.00518445 0.000790347"/>
<joint class="hip_hind" name="hip_hind_left" axis="0 0 1"/>
<geom class="visual" rgba="0.498039 0.498039 0.498039 1" mesh="upper_left_1"/>
<geom class="visual" rgba="1 0.756863 0.054902 1" mesh="upper_left_3"/>
<geom class="visual" rgba="0.980392 0.713725 0.00392157 1" mesh="upper_left_2"/>
<body name="lower_leg_1to1_hind_left" pos="0.208835 0.0691954 -0.0792" quat="0.0386264 0.995249 0.0893024 0.0034659">
<inertial pos="-0.0577509 -0.00780463 -0.0129639" quat="-0.047103 0.705359 -0.0102465 0.70721" mass="0.169623" diaginertia="0.000828741 0.000813964 3.49901e-05"/>
<joint class="knee" name="knee_hind_left"/>
<geom class="visual" pos="0.00320019 0.0259591 -0.0385878" rgba="0.32549 0.529412 0.980392 1" mesh="lower_leg_1to1"/>
<geom class="visual_foot" pos="0.00320019 0.0259591 -0.0385878" mesh="foot"/>
<geom class="lowerLegFootLeft"/>
</body>
</body>
</body>
</body>
</worldbody>
<actuator>
<general name="abduction_front_left" class="multi_mode_controlled_actuator" joint="abduction_front_left"/>
<general name="hip_front_left" class="multi_mode_controlled_actuator" joint="hip_front_left"/>
<general name="knee_front_left" class="multi_mode_controlled_actuator" joint="knee_front_left"/>
<general name="abduction_hind_left" class="multi_mode_controlled_actuator" joint="abduction_hind_left"/>
<general name="hip_hind_left" class="multi_mode_controlled_actuator" joint="hip_hind_left"/>
<general name="knee_hind_left" class="multi_mode_controlled_actuator" joint="knee_hind_left"/>
<general name="abduction_front_right" class="multi_mode_controlled_actuator" joint="abduction_front_right"/>
<general name="hip_front_right" class="multi_mode_controlled_actuator" joint="hip_front_right"/>
<general name="knee_front_right" class="multi_mode_controlled_actuator" joint="knee_front_right"/>
<general name="abduction_hind_right" class="multi_mode_controlled_actuator" joint="abduction_hind_right"/>
<general name="hip_hind_right" class="multi_mode_controlled_actuator" joint="hip_hind_right"/>
<general name="knee_hind_right" class="multi_mode_controlled_actuator" joint="knee_hind_right"/>
</actuator>
<sensor>
<jointpos joint="abduction_front_left" name="abduction_front_left_pos"/>
<jointpos joint="hip_front_left" name="hip_front_left_pos"/>
<jointpos joint="knee_front_left" name="knee_front_left_pos"/>
<jointpos joint="abduction_hind_left" name="abduction_hind_left_pos"/>
<jointpos joint="hip_hind_left" name="hip_hind_left_pos"/>
<jointpos joint="knee_hind_left" name="knee_hind_left_pos"/>
<jointpos joint="abduction_front_right" name="abduction_front_right_pos"/>
<jointpos joint="hip_front_right" name="hip_front_right_pos"/>
<jointpos joint="knee_front_right" name="knee_front_right_pos"/>
<jointpos joint="abduction_hind_right" name="abduction_hind_right_pos"/>
<jointpos joint="hip_hind_right" name="hip_hind_right_pos"/>
<jointpos joint="knee_hind_right" name="knee_hind_right_pos"/>
<jointvel joint="abduction_front_left" name="abduction_front_left_vel"/>
<jointvel joint="hip_front_left" name="hip_front_left_vel"/>
<jointvel joint="knee_front_left" name="knee_front_left_vel"/>
<jointvel joint="abduction_hind_left" name="abduction_hind_left_vel"/>
<jointvel joint="hip_hind_left" name="hip_hind_left_vel"/>
<jointvel joint="knee_hind_left" name="knee_hind_left_vel"/>
<jointvel joint="abduction_front_right" name="abduction_front_right_vel"/>
<jointvel joint="hip_front_right" name="hip_front_right_vel"/>
<jointvel joint="knee_front_right" name="knee_front_right_vel"/>
<jointvel joint="abduction_hind_right" name="abduction_hind_right_vel"/>
<jointvel joint="hip_hind_right" name="hip_hind_right_vel"/>
<jointvel joint="knee_hind_right" name="knee_hind_right_vel"/>
<gyro site="origin" name="gyro"/>
<accelerometer site="origin" name="accelerometer"/>
<framequat objtype="site" objname="origin" name="orientation"/>
</sensor>
<keyframe>
<key name="standing" qpos="0 0 0.21 1 0 0 0 0 0.5 1.0 0 0.5 1.0 0 0.5 1.0 0 0.5 1.0"
ctrl="0 0.5 1.0 0 0.5 1.0 0 0.5 1.0 0 0.5 1.0"/>
</keyframe>
</mujoco>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

@@ -0,0 +1,26 @@
Humanoid
========
Degrees of Freedom: 27
Actuators: 21
This is a clone of the [MuJoCo Humanoid](https://github.com/google-deepmind/mujoco/blob/main/model/humanoid/humanoid.xml)
with the following changes:
* Solver switched to CG with 8 iterations
* Explicit contact pairs for feet and ground (compatible with
[OpenAI Gym Humanoid](https://gymnasium.farama.org/environments/mujoco/humanoid/)
environment)
This simplified humanoid model, introduced in [1], is designed for bipedal
locomotion behaviours. While several variants of it exist in the wild, this
version is based on the model in the DeepMind Control Suite [2], which has
fairly realistic actuator gains.
[1] [Synthesis and Stabilization of Complex Behaviors through Online Trajectory Optimization]
(https://doi.org/10.1109/IROS.2012.6386025).
[2] [DeepMind Control Suite](https://arxiv.org/abs/1801.00690).
![humanoid](humanoid.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

@@ -0,0 +1,256 @@
<!-- Copyright 2021 DeepMind Technologies Limited
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
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.
-->
<mujoco model="Humanoid">
<option timestep="0.005" solver="CG" iterations="6" ls_iterations="6">
<flag eulerdamp="disable"/>
</option>
<visual>
<map force="0.1" zfar="30"/>
<rgba haze="0.15 0.25 0.35 1"/>
<global offwidth="2560" offheight="1440" elevation="-20" azimuth="120"/>
</visual>
<statistic center="0 0 0.7"/>
<asset>
<texture type="skybox" builtin="gradient" rgb1=".3 .5 .7" rgb2="0 0 0" width="32" height="512"/>
<texture name="body" type="cube" builtin="flat" mark="cross" width="128" height="128" rgb1="0.8 0.6 0.4" rgb2="0.8 0.6 0.4" markrgb="1 1 1" random="0.01"/>
<material name="body" texture="body" texuniform="true" rgba="0.8 0.6 .4 1"/>
<texture name="grid" type="2d" builtin="checker" width="512" height="512" rgb1=".1 .2 .3" rgb2=".2 .3 .4"/>
<material name="grid" texture="grid" texrepeat="1 1" texuniform="true" reflectance=".2"/>
</asset>
<default>
<motor ctrlrange="-1 1" ctrllimited="true"/>
<default class="body">
<!-- geoms -->
<!-- TODO(robotics-simulation): support condim=1 for humanoid capsules. -->
<geom type="capsule" condim="3" friction=".7" solimp=".9 .99 .003" solref=".015 1" material="body" contype="0" conaffinity="0"/>
<default class="thigh">
<geom size=".06"/>
</default>
<default class="shin">
<geom fromto="0 0 0 0 0 -.3" size=".049"/>
</default>
<default class="foot">
<geom size=".027"/>
<default class="foot1">
<geom fromto="-.07 -.01 0 .14 -.03 0"/>
</default>
<default class="foot2">
<geom fromto="-.07 .01 0 .14 .03 0"/>
</default>
</default>
<default class="arm_upper">
<geom size=".04"/>
</default>
<default class="arm_lower">
<geom size=".031"/>
</default>
<default class="hand">
<geom type="sphere" size=".04"/>
</default>
<!-- joints -->
<joint type="hinge" damping=".2" stiffness="1" armature=".01" limited="true" solimplimit="0 .99 .01"/>
<default class="joint_big">
<joint damping="5" stiffness="10"/>
<default class="hip_x">
<joint range="-30 10"/>
</default>
<default class="hip_z">
<joint range="-60 35"/>
</default>
<default class="hip_y">
<joint axis="0 1 0" range="-150 20"/>
</default>
<default class="joint_big_stiff">
<joint stiffness="20"/>
</default>
</default>
<default class="knee">
<joint pos="0 0 .02" axis="0 -1 0" range="-160 2"/>
</default>
<default class="ankle">
<joint range="-50 50"/>
<default class="ankle_y">
<joint pos="0 0 .08" axis="0 1 0" stiffness="6"/>
</default>
<default class="ankle_x">
<joint pos="0 0 .04" stiffness="3"/>
</default>
</default>
<default class="shoulder">
<joint range="-85 60"/>
</default>
<default class="elbow">
<joint range="-100 50" stiffness="0"/>
</default>
</default>
</default>
<worldbody>
<geom name="floor" size="0 0 .05" type="plane" material="grid" condim="3"/>
<light name="spotlight" mode="targetbodycom" target="torso" diffuse=".8 .8 .8" specular="0.3 0.3 0.3" pos="0 -6 4" cutoff="30"/>
<body name="torso" pos="0 0 1.282" childclass="body">
<light name="top" pos="0 0 2" mode="trackcom"/>
<camera name="back" pos="-3 0 1" xyaxes="0 -1 0 1 0 2" mode="trackcom"/>
<camera name="side" pos="0 -3 1" xyaxes="1 0 0 0 1 2" mode="trackcom"/>
<freejoint name="root"/>
<geom name="torso" fromto="0 -.07 0 0 .07 0" size=".07"/>
<geom name="waist_upper" fromto="-.01 -.06 -.12 -.01 .06 -.12" size=".06"/>
<body name="head" pos="0 0 .19">
<geom name="head" type="sphere" size=".09"/>
<camera name="egocentric" pos=".09 0 0" xyaxes="0 -1 0 .1 0 1" fovy="80"/>
</body>
<body name="waist_lower" pos="-.01 0 -.26">
<geom name="waist_lower" fromto="0 -.06 0 0 .06 0" size=".06"/>
<joint name="abdomen_z" pos="0 0 .065" axis="0 0 1" range="-45 45" class="joint_big_stiff"/>
<joint name="abdomen_y" pos="0 0 .065" axis="0 1 0" range="-75 30" class="joint_big"/>
<body name="pelvis" pos="0 0 -.165">
<joint name="abdomen_x" pos="0 0 .1" axis="1 0 0" range="-35 35" class="joint_big"/>
<geom name="butt" fromto="-.02 -.07 0 -.02 .07 0" size=".09"/>
<body name="thigh_right" pos="0 -.1 -.04">
<joint name="hip_x_right" axis="1 0 0" class="hip_x"/>
<joint name="hip_z_right" axis="0 0 1" class="hip_z"/>
<joint name="hip_y_right" class="hip_y"/>
<geom name="thigh_right" fromto="0 0 0 0 .01 -.34" class="thigh"/>
<body name="shin_right" pos="0 .01 -.4">
<joint name="knee_right" class="knee"/>
<geom name="shin_right" class="shin"/>
<body name="foot_right" pos="0 0 -.39">
<joint name="ankle_y_right" class="ankle_y"/>
<joint name="ankle_x_right" class="ankle_x" axis="1 0 .5"/>
<geom name="foot1_right" class="foot1"/>
<geom name="foot2_right" class="foot2"/>
</body>
</body>
</body>
<body name="thigh_left" pos="0 .1 -.04">
<joint name="hip_x_left" axis="-1 0 0" class="hip_x"/>
<joint name="hip_z_left" axis="0 0 -1" class="hip_z"/>
<joint name="hip_y_left" class="hip_y"/>
<geom name="thigh_left" fromto="0 0 0 0 -.01 -.34" class="thigh"/>
<body name="shin_left" pos="0 -.01 -.4">
<joint name="knee_left" class="knee"/>
<geom name="shin_left" fromto="0 0 0 0 0 -.3" class="shin"/>
<body name="foot_left" pos="0 0 -.39">
<joint name="ankle_y_left" class="ankle_y"/>
<joint name="ankle_x_left" class="ankle_x" axis="-1 0 -.5"/>
<geom name="foot1_left" class="foot1"/>
<geom name="foot2_left" class="foot2"/>
</body>
</body>
</body>
</body>
</body>
<body name="upper_arm_right" pos="0 -.17 .06">
<joint name="shoulder1_right" axis="2 1 1" class="shoulder"/>
<joint name="shoulder2_right" axis="0 -1 1" class="shoulder"/>
<geom name="upper_arm_right" fromto="0 0 0 .16 -.16 -.16" class="arm_upper"/>
<body name="lower_arm_right" pos=".18 -.18 -.18">
<joint name="elbow_right" axis="0 -1 1" class="elbow"/>
<geom name="lower_arm_right" fromto=".01 .01 .01 .17 .17 .17" class="arm_lower"/>
<body name="hand_right" pos=".18 .18 .18">
<geom name="hand_right" zaxis="1 1 1" class="hand"/>
</body>
</body>
</body>
<body name="upper_arm_left" pos="0 .17 .06">
<joint name="shoulder1_left" axis="-2 1 -1" class="shoulder"/>
<joint name="shoulder2_left" axis="0 -1 -1" class="shoulder"/>
<geom name="upper_arm_left" fromto="0 0 0 .16 .16 -.16" class="arm_upper"/>
<body name="lower_arm_left" pos=".18 .18 -.18">
<joint name="elbow_left" axis="0 -1 -1" class="elbow"/>
<geom name="lower_arm_left" fromto=".01 -.01 .01 .17 -.17 .17" class="arm_lower"/>
<body name="hand_left" pos=".18 -.18 .18">
<geom name="hand_left" zaxis="1 -1 1" class="hand"/>
</body>
</body>
</body>
</body>
</worldbody>
<contact>
<exclude body1="waist_lower" body2="thigh_right"/>
<exclude body1="waist_lower" body2="thigh_left"/>
<pair geom1="foot1_left" geom2="floor"/>
<pair geom1="foot1_right" geom2="floor"/>
<pair geom1="foot2_left" geom2="floor"/>
<pair geom1="foot2_right" geom2="floor"/>
</contact>
<tendon>
<fixed name="hamstring_right" limited="true" range="-0.3 2">
<joint joint="hip_y_right" coef=".5"/>
<joint joint="knee_right" coef="-.5"/>
</fixed>
<fixed name="hamstring_left" limited="true" range="-0.3 2">
<joint joint="hip_y_left" coef=".5"/>
<joint joint="knee_left" coef="-.5"/>
</fixed>
</tendon>
<actuator>
<motor name="abdomen_y" gear="40" joint="abdomen_y"/>
<motor name="abdomen_z" gear="40" joint="abdomen_z"/>
<motor name="abdomen_x" gear="40" joint="abdomen_x"/>
<motor name="hip_x_right" gear="40" joint="hip_x_right"/>
<motor name="hip_z_right" gear="40" joint="hip_z_right"/>
<motor name="hip_y_right" gear="120" joint="hip_y_right"/>
<motor name="knee_right" gear="80" joint="knee_right"/>
<motor name="ankle_x_right" gear="20" joint="ankle_x_right"/>
<motor name="ankle_y_right" gear="20" joint="ankle_y_right"/>
<motor name="hip_x_left" gear="40" joint="hip_x_left"/>
<motor name="hip_z_left" gear="40" joint="hip_z_left"/>
<motor name="hip_y_left" gear="120" joint="hip_y_left"/>
<motor name="knee_left" gear="80" joint="knee_left"/>
<motor name="ankle_x_left" gear="20" joint="ankle_x_left"/>
<motor name="ankle_y_left" gear="20" joint="ankle_y_left"/>
<motor name="shoulder1_right" gear="20" joint="shoulder1_right"/>
<motor name="shoulder2_right" gear="20" joint="shoulder2_right"/>
<motor name="elbow_right" gear="40" joint="elbow_right"/>
<motor name="shoulder1_left" gear="20" joint="shoulder1_left"/>
<motor name="shoulder2_left" gear="20" joint="shoulder2_left"/>
<motor name="elbow_left" gear="40" joint="elbow_left"/>
</actuator>
<keyframe>
<!--
The values below are split into rows for readibility:
torso position
torso orientation
spinal
right leg
left leg
arms
-->
<key name="squat" qpos="0 0 0.596
0.988015 0 0.154359 0
0 0.4 0
-0.25 -0.5 -2.5 -2.65 -0.8 0.56
-0.25 -0.5 -2.5 -2.65 -0.8 0.56
0 0 0 0 0 0"/>
<key name="stand_on_left_leg" qpos="0 0 1.21948
0.971588 -0.179973 0.135318 -0.0729076
-0.0516 -0.202 0.23
-0.24 -0.007 -0.34 -1.76 -0.466 -0.0415
-0.08 -0.01 -0.37 -0.685 -0.35 -0.09
0.109 -0.067 -0.7 -0.05 0.12 0.16"/>
</keyframe>
</mujoco>
@@ -0,0 +1,28 @@
# Shadow Hand E3M5 Description (MJCF)
Requires MuJoCo 3.0.0 or later.
## Overview
This package contains assets of the "E3M5" version of the Shadow Hand robot,
including both right-handed and left-handed versions.
The original URDF and assets were provided directly by
[Shadow Robot Company](https://www.shadowrobot.com/) under the
[Apache 2.0 License](LICENSE).
The [original Menagerie Shadow Hand](https://github.com/google-deepmind/mujoco_menagerie/tree/main/shadow_hand)
has been modified for MJX simulation in the following ways:
* Solver switched to CG with 8 iterations
* Condim switched to 3 (MJX does not yet support condim != 3)
* Object converted from ellipsoid to sphere (MJX does not yet support ellipsoids)
* Explicit contact pairs for fingers and object
* Removed some unused geoms in the arm base
<p float="left">
<img src="shadow_hand.png" width="400">
</p>
## License
These models are released under an [Apache-2.0 License](LICENSE).
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,908 @@
####
#
# OBJ File Generated by Meshlab
#
####
# Object f_middle_E3M5.obj
#
# Vertices: 224
# Faces: 444
#
####
vn -1.975821 -5.020711 -0.278529
v -5.154970 -5.651640 5.676910
vn -3.328850 -4.153035 -1.748958
v -6.985930 -7.507730 8.862470
vn -4.326096 -2.957106 -1.768247
v -8.013970 -5.977890 7.982400
vn -5.106826 -1.997097 -1.350573
v -8.480970 -4.318420 6.987890
vn -6.087360 0.080981 1.091995
v -8.500170 0.021880 5.469900
vn -6.215165 -0.637174 0.034910
v -8.500170 -3.889610 12.777900
vn -5.876741 -1.418994 0.574094
v -8.489370 -3.705560 18.597891
vn -6.241298 0.262664 -0.501847
v -8.500170 0.021880 20.537901
vn -5.906881 1.835267 -0.599729
v -8.225670 3.737700 21.843500
vn -0.396249 6.244791 0.066189
v -3.500170 7.979650 1.218900
vn -0.124787 6.279845 0.029596
v -3.500170 7.049800 24.446890
vn 0.396132 6.244804 0.066361
v 3.492960 7.979650 1.218900
vn 0.125572 6.279818 0.029003
v 3.499830 7.049800 24.446890
vn -1.131282 6.142010 -0.169950
v -4.298070 7.002770 24.018900
vn -1.731693 5.941936 0.364020
v -4.757570 7.781740 2.145900
vn -3.890530 4.808362 0.255683
v -6.731770 6.685500 2.872400
vn -3.271605 5.251853 -0.183712
v -6.311270 6.251020 22.773901
vn -5.152781 3.420898 0.498048
v -7.752570 5.534480 3.444480
vn -4.975134 3.740874 -0.357759
v -7.627570 4.939400 22.204651
vn -6.052958 1.453399 0.502421
v -8.429670 3.694290 4.187910
vn -0.352079 -5.585024 2.009694
v -3.431690 -8.644050 15.628901
vn -0.618978 -6.189351 0.349946
v -3.500170 -8.933320 12.688900
vn 0.352146 -5.584894 2.009872
v 3.424480 -8.644050 15.628901
vn 0.618977 -6.189351 0.349946
v 3.492960 -8.933320 12.688900
vn -0.617464 -6.058264 -0.743055
v -3.500170 -8.976070 9.813900
vn 0.617463 -6.058264 -0.743055
v 3.492960 -8.976070 9.813900
vn -2.386746 -4.685711 2.391089
v -6.413370 -7.450560 17.154900
vn -0.038383 -4.901343 3.203433
v -4.732770 -7.886830 17.096901
vn -3.817267 -4.439030 0.980845
v -7.464270 -4.932370 19.659380
vn -0.093681 -5.289313 0.922057
v -4.000170 -5.214450 19.768900
vn 0.094543 -5.289427 0.922396
v 3.999830 -5.214450 19.768900
vn -0.602491 -5.180288 -2.913458
v -3.500170 -8.705390 8.825910
vn -0.254401 -4.985663 -0.658851
v -4.000170 -5.823190 5.848900
vn 0.255842 -4.985855 -0.659585
v 3.999830 -5.823190 5.848900
vn 0.602489 -5.180290 -2.913456
v 3.492960 -8.705390 8.825910
vn 0.115850 -5.586018 -2.851343
v 4.487960 -5.617100 20.772900
vn -5.679802 -2.330745 0.093700
v -8.119570 -5.837210 12.861900
vn -4.337475 -4.345920 0.189490
v -7.035670 -7.457820 12.930900
vn -4.113871 -4.178817 1.073790
v -7.076570 -7.152620 16.535900
vn -5.200893 -2.776820 1.144308
v -8.027070 -5.462310 17.552050
vn -2.295639 -5.676401 0.362745
v -5.413570 -8.540680 12.828341
vn -2.040112 -5.718785 1.194047
v -5.526400 -8.058870 15.991060
vn -2.172929 -5.124251 -1.466405
v -5.669070 -8.477780 9.505900
vn -0.115949 -5.585745 -2.851956
v -4.495170 -5.617100 20.772900
vn 0.330607 -5.785292 -1.997014
v 4.750360 -6.438000 22.179899
vn 0.570670 -6.025518 0.365783
v 4.757230 -6.973210 25.881910
vn 1.253543 -5.070250 2.666592
v 4.757230 -6.489650 27.705900
vn 1.035414 -3.918195 4.338571
v 4.757230 -4.190930 30.656900
vn 0.629466 -1.818101 5.742779
v 4.757230 -2.540570 31.571890
vn 0.861828 0.841504 5.859906
v 4.757230 1.179800 31.957901
vn 0.704351 2.428302 5.571692
v 4.757230 2.983010 31.401899
vn 0.725931 4.089355 4.379475
v 4.757230 4.574110 30.386900
vn 0.706091 5.553284 2.272709
v 4.757230 6.687160 27.300900
vn 0.903443 6.016993 0.162534
v 4.757230 7.057710 25.450911
vn -0.330650 -5.785307 -1.996895
v -4.757570 -6.438000 22.179899
vn -0.567091 -6.026638 0.366088
v -4.757570 -6.973210 25.881910
vn -1.246799 -5.074042 2.668208
v -4.757570 -6.489650 27.705900
vn -1.029382 -3.919869 4.341149
v -4.757570 -4.190930 30.656900
vn -0.625103 -1.818855 5.744062
v -4.757570 -2.540570 31.571890
vn -0.856462 0.841017 5.862324
v -4.757570 1.179800 31.957901
vn -0.700617 2.429208 5.572634
v -4.757570 2.983010 31.401899
vn -0.720712 4.090838 4.380729
v -4.757570 4.574110 30.386900
vn -0.700988 5.554998 2.273383
v -4.757570 6.687160 27.300900
vn -0.894664 6.020987 0.169189
v -4.757570 7.057710 25.450911
vn -2.156509 -5.429175 -0.915722
v -5.905570 -6.467490 22.900900
vn -2.571520 -5.055631 0.909265
v -5.905570 -6.714300 26.079901
vn -2.760185 -4.429241 3.436599
v -5.878870 -4.670700 29.331690
vn -2.517030 -2.450455 4.619818
v -5.905570 -3.059300 31.089901
vn -2.229681 -0.072035 5.230337
v -5.905570 0.043750 31.825911
vn -2.501201 2.299577 4.759172
v -5.905570 3.142020 31.069891
vn -2.123583 4.367735 2.980202
v -5.905570 5.558400 28.988899
vn -2.601006 5.117285 0.793377
v -5.905570 6.764830 26.036909
vn -5.776209 -1.678740 -0.670729
v -8.773640 -3.349110 22.568850
vn -5.926663 -1.590920 -0.313608
v -8.450030 -3.339630 20.409321
vn -5.447601 -2.033954 0.207273
v -8.999960 -3.586780 25.822010
vn -5.708444 -1.232529 1.251641
v -8.999960 -2.605590 27.581030
vn -4.415786 0.019221 4.171239
v -8.121470 0.021880 29.932899
vn -5.311327 1.758746 1.267951
v -8.999960 3.114340 27.469900
vn -4.415588 4.178071 0.156447
v -8.121470 4.950330 25.050900
vn -5.467701 0.005860 2.047906
v -9.000000 0.021880 28.726141
vn -5.219584 2.176666 -0.081094
v -8.999960 3.878120 25.230341
vn -6.177143 0.179311 -0.737004
v -9.000190 0.026420 23.152901
vn -3.502934 -4.525011 1.642803
v -6.878870 -6.812390 2.176910
vn -3.251389 -4.444415 -0.519670
v -6.523960 -4.865100 5.636130
vn -1.317918 -5.210125 2.722004
v -4.757570 -7.077680 3.798900
vn -1.134610 -5.750695 1.447569
v -4.757570 -7.783830 1.971900
vn -3.258571 -4.934119 -1.538218
v -6.878870 -6.538240 -2.893100
vn -1.182014 -5.905304 -0.021934
v -4.757570 -8.028080 0.028890
vn -1.141683 -5.599188 -1.603683
v -4.757570 -7.796000 -1.915110
vn -2.804239 -4.424251 -2.808778
v -6.878870 -5.854020 -4.573890
vn -0.643679 -4.786737 -3.685352
v -4.757570 -5.985130 -5.355090
vn -2.502592 -2.738398 -4.286158
v -6.405570 -4.618150 -6.293100
vn -0.439400 -3.443835 -5.120736
v -4.757570 -4.513470 -6.647100
vn -2.832245 -0.747272 -5.403884
v -5.905570 -1.568680 -7.337640
vn -0.973778 -1.644752 -5.681370
v -4.757570 -2.773520 -7.545100
vn -2.571455 0.861364 -5.150094
v -5.905570 1.770760 -7.620090
vn -0.817049 0.297736 -5.966962
v -4.757570 1.090100 -7.975100
vn -0.813927 2.304698 -5.613643
v -4.757570 2.984900 -7.481100
vn -2.542195 3.373481 -4.109332
v -5.905570 4.791360 -6.195100
vn -0.623470 3.519813 -5.012120
v -4.757570 4.704470 -6.544100
vn -3.712195 2.427079 -4.191787
v -6.878870 3.669480 -6.170100
vn -2.535884 4.738874 -2.377094
v -5.905570 6.942470 -3.641100
vn -0.838900 4.616839 -3.927192
v -4.757570 6.146920 -5.220100
vn -0.608599 5.500813 -2.702369
v -4.757570 7.226940 -3.586100
vn -4.538651 3.600815 0.213802
v -8.121470 6.008870 0.104330
vn -2.590973 5.394011 -0.119302
v -5.905570 7.634140 -0.422090
vn -1.036419 5.831363 -1.030521
v -4.757570 7.880620 -1.740100
vn 1.323168 -5.206586 2.722369
v 4.757230 -7.077680 3.798900
vn 1.137741 -5.749225 1.447183
v 4.757230 -7.783830 1.971900
vn 1.185190 -5.903691 -0.022018
v 4.757230 -8.028080 0.028890
vn 1.144988 -5.597528 -1.603007
v 4.757230 -7.796000 -1.915110
vn 0.646070 -4.786094 -3.684909
v 4.757230 -5.985130 -5.355090
vn 0.441208 -3.443483 -5.120489
v 4.757230 -4.513470 -6.647100
vn 0.978876 -1.643660 -5.679229
v 4.757230 -2.773520 -7.545100
vn 0.821955 0.297028 -5.965026
v 4.757230 1.090100 -7.975100
vn 0.819038 2.304303 -5.611849
v 4.757230 2.984900 -7.481100
vn 0.627055 3.518999 -5.011438
v 4.757230 4.704470 -6.544100
vn 0.844235 4.615387 -3.925911
v 4.757230 6.146920 -5.220100
vn 0.612092 5.500012 -2.701771
v 4.757230 7.226940 -3.586100
vn 1.042403 5.828549 -1.029587
v 4.757230 7.880620 -1.740100
vn -5.081741 -3.169341 1.411239
v -7.878870 -5.407860 2.463000
vn -5.431445 -2.324418 0.866331
v -8.500000 -3.558340 4.341910
vn -5.965299 -1.291361 -0.130787
v -9.000000 -3.599130 -1.173100
vn -6.021783 -0.734312 -0.996979
v -9.000000 -2.216030 -3.076100
vn -5.820709 0.061761 1.471896
v -9.000000 0.021880 4.392910
vn -5.524968 -0.008542 -1.914781
v -9.000000 0.021880 -4.580100
vn -6.003400 0.789261 -1.010776
v -9.000000 2.259790 -3.076100
vn -5.948116 1.307274 -0.199703
v -9.000000 3.744800 -1.099100
vn -4.681353 0.881030 -3.653100
v -8.121470 1.636160 -5.701100
vn -4.962337 -3.308012 0.017600
v -8.121470 -5.891000 -0.429100
vn -3.386144 -4.944589 0.183884
v -6.878870 -7.133550 0.480910
vn -4.989554 -3.416844 -0.858124
v -8.121470 -5.600890 -1.876100
vn -4.699954 -2.540347 -2.176459
v -8.121000 -4.852410 -4.186100
vn -4.528942 -0.789736 -3.801926
v -8.121470 -1.442780 -5.741090
vn -4.694910 3.473782 -1.227291
v -8.121000 5.473260 -2.536040
vn -4.507683 2.644774 -2.484791
v -8.121000 4.618140 -4.139520
vn -3.540627 -4.160904 2.684636
v -6.754660 -5.905320 3.906520
vn -4.977641 -2.524741 -0.660096
v -8.458230 -3.706150 5.688800
vn -4.119967 -3.565147 2.419044
v -8.121000 -3.838170 28.386730
vn -4.472468 -4.094103 0.578559
v -8.121000 -4.784040 25.948540
vn -4.160048 -2.325684 3.353160
v -8.121000 -3.164870 29.143299
vn -4.727426 -3.959540 -0.401267
v -7.969330 -4.728900 22.715111
vn -4.301860 3.896197 2.071576
v -8.121000 3.997300 28.018669
vn -4.324967 2.223240 3.423124
v -8.121000 3.127030 29.120550
vn 4.320310 2.227863 3.430520
v 8.113790 3.127030 29.120550
vn 4.294984 3.904343 2.075937
v 8.113790 3.997300 28.018669
vn 4.727426 -3.959540 -0.401266
v 7.962120 -4.728900 22.715111
vn 4.156006 -2.330211 3.360267
v 8.113790 -3.164880 29.143299
vn 4.467521 -4.102084 0.578252
v 8.113790 -4.784040 25.948540
vn 4.114889 -3.572841 2.424054
v 8.113790 -3.838170 28.386730
vn 4.977641 -2.524741 -0.660094
v 8.451020 -3.706150 5.688800
vn 3.540596 -4.160900 2.684620
v 6.747460 -5.905330 3.906520
vn 4.505536 2.649572 -2.489471
v 8.113790 4.618140 -4.139520
vn 4.691979 3.481025 -1.230083
v 8.113790 5.473260 -2.536040
vn 4.525122 -0.792267 -3.809892
v 8.114260 -1.442780 -5.741100
vn 4.698267 -2.544770 -2.180376
v 8.113790 -4.852410 -4.186100
vn 4.986269 -3.424754 -0.860209
v 8.114260 -5.600890 -1.876110
vn 3.389770 -4.943439 0.183984
v 6.871660 -7.133550 0.480900
vn 4.960076 -3.315449 0.017590
v 8.114260 -5.891000 -0.429100
vn 4.677627 0.883812 -3.660927
v 8.114260 1.636160 -5.701100
vn 5.943124 1.316251 -0.200224
v 9.000000 3.744800 -1.099100
vn 5.999335 0.794810 -1.017667
v 9.000000 2.259790 -3.076100
vn 5.515849 -0.008605 -1.924431
v 9.000000 0.021880 -4.580100
vn 5.809621 0.061768 1.487882
v 9.000000 0.021880 4.392910
vn 6.017956 -0.739562 -1.003839
v 9.000000 -2.216030 -3.076100
vn 5.960750 -1.299910 -0.131566
v 9.000000 -3.599130 -1.173100
vn 5.430431 -2.329072 0.871211
v 8.492790 -3.558340 4.341910
vn 5.080068 -3.172664 1.413554
v 7.871660 -5.407860 2.463000
vn 2.592579 5.393801 -0.117772
v 5.898360 7.634140 -0.422090
vn 4.536738 3.606666 0.215079
v 8.114260 6.008870 0.104320
vn 2.539719 4.738473 -2.377132
v 5.898360 6.942470 -3.641100
vn 3.712209 2.427073 -4.191782
v 6.871660 3.669480 -6.170100
vn 2.545941 3.373343 -4.108975
v 5.898360 4.791360 -6.195110
vn 2.575225 0.861660 -5.149790
v 5.898360 1.770760 -7.620090
vn 2.837510 -0.746383 -5.402541
v 5.898360 -1.568680 -7.337640
vn 2.504754 -2.738315 -4.286266
v 6.398360 -4.618150 -6.293100
vn 2.806519 -4.423785 -2.808745
v 6.871660 -5.854020 -4.573890
vn 3.261961 -4.932928 -1.538092
v 6.871660 -6.538240 -2.893100
vn 3.251392 -4.444405 -0.519667
v 6.516750 -4.865100 5.636130
vn 3.506286 -4.523795 1.642852
v 6.871660 -6.812390 2.176900
vn 6.174293 0.177592 -0.747126
v 9.000190 0.026420 23.152901
vn 5.208213 2.185383 -0.082852
v 8.999960 3.878120 25.230341
vn 5.458268 0.005854 2.057881
v 9.000000 0.021880 28.726141
vn 4.409956 4.186586 0.156118
v 8.114260 4.950330 25.050900
vn 5.300780 1.765974 1.274092
v 8.999960 3.114340 27.469900
vn 4.410225 0.019278 4.180248
v 8.114260 0.021880 29.932899
vn 5.700973 -1.239574 1.259032
v 8.999960 -2.605590 27.581030
vn 5.436850 -2.044216 0.204676
v 8.999960 -3.586780 25.822010
vn 5.926663 -1.590920 -0.313609
v 8.442820 -3.339630 20.409321
vn 5.775527 -1.684477 -0.676935
v 8.766430 -3.349110 22.568850
vn 2.604061 5.117111 0.792504
v 5.898360 6.764830 26.036909
vn 2.123708 4.367924 2.980122
v 5.898360 5.558400 28.988899
vn 2.505037 2.299492 4.758895
v 5.898360 3.142020 31.069891
vn 2.230662 -0.072842 5.230432
v 5.898360 0.043740 31.825911
vn 2.520874 -2.450161 4.619775
v 5.898360 -3.059300 31.089901
vn 2.766455 -4.427368 3.435123
v 5.871660 -4.670700 29.331690
vn 2.575577 -5.055537 0.908791
v 5.898360 -6.714300 26.079901
vn 2.156810 -5.429112 -0.916056
v 5.898360 -6.467490 22.900900
vn 2.172924 -5.124258 -1.466401
v 5.661860 -8.477780 9.505900
vn 2.040112 -5.718785 1.194047
v 5.519190 -8.058870 15.991060
vn 2.295639 -5.676401 0.362745
v 5.406360 -8.540680 12.828341
vn 5.200893 -2.776819 1.144308
v 8.019860 -5.462310 17.552050
vn 4.113871 -4.178818 1.073790
v 7.069360 -7.152620 16.535900
vn 4.337474 -4.345921 0.189494
v 7.028460 -7.457820 12.930900
vn 5.679803 -2.330744 0.093702
v 8.112360 -5.837210 12.861900
vn 3.817452 -4.438978 0.980867
v 7.457060 -4.932370 19.659380
vn 0.036775 -4.900473 3.203675
v 4.725560 -7.886830 17.096901
vn 2.386746 -4.685711 2.391089
v 6.406160 -7.450560 17.154900
vn 6.051692 1.458310 0.507014
v 8.422460 3.694290 4.187910
vn 4.975134 3.740875 -0.357759
v 7.620360 4.939400 22.204651
vn 5.152782 3.420897 0.498046
v 7.745360 5.534480 3.444480
vn 3.271605 5.251853 -0.183712
v 6.304060 6.251020 22.773901
vn 3.890531 4.808362 0.255683
v 6.724560 6.685500 2.872400
vn 1.731693 5.941936 0.364020
v 4.750360 7.781740 2.145900
vn 1.132973 6.141824 -0.170517
v 4.290860 7.002770 24.018900
vn 5.905301 1.839049 -0.604558
v 8.218460 3.737700 21.843500
vn 6.240402 0.262129 -0.507810
v 8.492960 0.021880 20.537901
vn 5.876742 -1.418994 0.574095
v 8.482160 -3.705560 18.597891
vn 6.215165 -0.637174 0.034910
v 8.492960 -3.889610 12.777900
vn 6.082349 0.081128 1.105070
v 8.492960 0.021880 5.469900
vn 5.106826 -1.997098 -1.350573
v 8.473760 -4.318420 6.987890
vn 4.326105 -2.957100 -1.768250
v 8.006760 -5.977890 7.982400
vn 3.328853 -4.153027 -1.748949
v 6.978730 -7.507730 8.862470
vn 1.978097 -5.018886 -0.276658
v 5.147760 -5.651640 5.676900
# 224 vertices, 0 vertices normals
f 1//1 2//2 3//3
f 5//5 4//4 6//6
f 5//5 6//6 7//7
f 5//5 7//7 8//8
f 9//9 5//5 8//8
f 10//10 11//11 12//12
f 13//13 12//12 11//11
f 14//14 11//11 10//10
f 14//14 10//10 15//15
f 15//15 16//16 14//14
f 17//17 14//14 16//16
f 16//16 18//18 17//17
f 19//19 17//17 18//18
f 9//9 19//19 18//18
f 18//18 20//20 9//9
f 20//20 5//5 9//9
f 21//21 22//22 23//23
f 24//24 23//23 22//22
f 22//22 25//25 24//24
f 26//26 24//24 25//25
f 27//27 28//28 29//29
f 30//30 29//29 28//28
f 28//28 21//21 30//30
f 30//30 21//21 23//23
f 31//31 30//30 23//23
f 2//2 1//1 32//32
f 1//1 33//33 32//32
f 34//34 35//35 32//32
f 33//33 34//34 32//32
f 6//6 4//4 3//3
f 37//37 6//6 3//3
f 37//37 3//3 2//2
f 38//38 37//37 2//2
f 41//41 42//42 39//39
f 38//38 41//41 39//39
f 38//38 2//2 43//43
f 41//41 38//38 43//43
f 43//43 25//25 41//41
f 21//21 42//42 41//41
f 41//41 22//22 21//21
f 41//41 25//25 22//22
f 36//36 44//44 30//30
f 44//44 45//45 55//55
f 45//45 46//46 55//55
f 56//56 55//55 46//46
f 46//46 47//47 56//56
f 57//57 56//56 47//47
f 47//47 48//48 57//57
f 58//58 57//57 48//48
f 48//48 49//49 58//58
f 59//59 58//58 49//49
f 49//49 50//50 59//59
f 60//60 59//59 50//50
f 50//50 51//51 60//60
f 61//61 60//60 51//51
f 51//51 52//52 61//61
f 62//62 61//61 52//52
f 52//52 53//53 62//62
f 63//63 62//62 53//53
f 53//53 54//54 63//63
f 64//64 63//63 54//54
f 13//13 64//64 54//54
f 64//64 13//13 11//11
f 64//64 11//11 14//14
f 29//29 44//44 55//55
f 65//65 29//29 55//55
f 65//65 55//55 56//56
f 66//66 65//65 56//56
f 56//56 57//57 66//66
f 67//67 66//66 57//57
f 67//67 57//57 58//58
f 68//68 67//67 58//58
f 58//58 59//59 68//68
f 69//69 68//68 59//59
f 59//59 60//60 69//69
f 69//69 60//60 61//61
f 70//70 69//69 61//61
f 61//61 62//62 70//70
f 71//71 70//70 62//62
f 62//62 63//63 71//71
f 72//72 71//71 63//63
f 63//63 64//64 72//72
f 72//72 64//64 14//14
f 72//72 14//14 17//17
f 73//73 74//74 29//29
f 68//68 69//69 77//77
f 69//69 70//70 77//77
f 71//71 72//72 79//79
f 72//72 17//17 79//79
f 19//19 79//79 17//17
f 81//81 78//78 79//79
f 9//9 81//81 79//79
f 79//79 19//19 9//9
f 9//9 8//8 82//82
f 9//9 82//82 81//81
f 76//76 80//80 82//82
f 75//75 76//76 82//82
f 73//73 8//8 74//74
f 8//8 73//73 82//82
f 75//75 82//82 73//73
f 86//86 85//85 83//83
f 90//90 91//91 89//89
f 87//87 90//90 89//89
f 90//90 92//92 91//91
f 93//93 91//91 92//92
f 98//98 97//97 96//96
f 99//99 100//100 98//98
f 96//96 99//99 98//98
f 99//99 96//96 101//101
f 101//101 102//102 99//99
f 103//103 100//100 99//99
f 99//99 102//102 103//103
f 104//104 103//103 102//102
f 106//106 102//102 105//105
f 10//10 107//107 106//106
f 35//35 26//26 32//32
f 85//85 34//34 33//33
f 34//34 85//85 108//108
f 85//85 86//86 108//108
f 109//109 108//108 86//86
f 86//86 88//88 109//109
f 110//110 109//109 88//88
f 88//88 89//89 110//110
f 111//111 110//110 89//89
f 89//89 91//91 111//111
f 112//112 111//111 91//91
f 91//91 93//93 112//112
f 113//113 112//112 93//93
f 93//93 95//95 113//113
f 114//114 113//113 95//95
f 95//95 97//97 114//114
f 115//115 114//114 97//97
f 97//97 98//98 115//115
f 116//116 115//115 98//98
f 98//98 100//100 116//116
f 117//117 116//116 100//100
f 100//100 103//103 117//117
f 118//118 117//117 103//103
f 103//103 104//104 118//118
f 119//119 118//118 104//104
f 104//104 107//107 119//119
f 120//120 119//119 107//107
f 107//107 10//10 120//120
f 12//12 120//120 10//10
f 2//2 32//32 43//43
f 121//121 122//122 123//123
f 124//124 123//123 125//125
f 126//126 124//124 125//125
f 25//25 32//32 26//26
f 127//127 129//129 126//126
f 105//105 20//20 18//18
f 105//105 128//128 20//20
f 20//20 125//125 5//5
f 131//131 83//83 130//130
f 87//87 131//131 130//130
f 130//130 132//132 87//87
f 132//132 133//133 87//87
f 90//90 87//87 133//133
f 92//92 90//90 133//133
f 94//94 92//92 134//134
f 134//134 129//129 94//94
f 96//96 94//94 129//129
f 101//101 96//96 129//129
f 105//105 18//18 106//106
f 16//16 106//106 18//18
f 25//25 43//43 32//32
f 121//121 123//123 130//130
f 132//132 130//130 123//123
f 123//123 133//133 132//132
f 123//123 124//124 133//133
f 124//124 126//126 134//134
f 129//129 134//134 126//126
f 7//7 74//74 8//8
f 7//7 29//29 74//74
f 31//31 36//36 30//30
f 7//7 40//40 29//29
f 27//27 29//29 40//40
f 40//40 39//39 27//27
f 42//42 27//27 39//39
f 28//28 27//27 42//42
f 42//42 21//21 28//28
f 30//30 44//44 29//29
f 87//87 89//89 131//131
f 88//88 131//131 89//89
f 86//86 131//131 88//88
f 83//83 131//131 86//86
f 92//92 133//133 134//134
f 124//124 134//134 133//133
f 83//83 121//121 130//130
f 125//125 127//127 126//126
f 125//125 128//128 127//127
f 20//20 128//128 125//125
f 5//5 125//125 122//122
f 122//122 125//125 123//123
f 127//127 136//136 129//129
f 136//136 101//101 129//129
f 101//101 136//136 102//102
f 135//135 102//102 136//136
f 102//102 135//135 105//105
f 135//135 128//128 105//105
f 6//6 40//40 7//7
f 6//6 37//37 40//40
f 37//37 39//39 40//40
f 37//37 38//38 39//39
f 10//10 106//106 15//15
f 15//15 106//106 16//16
f 3//3 4//4 84//84
f 1//1 3//3 84//84
f 137//137 1//1 84//84
f 83//83 1//1 137//137
f 83//83 137//137 121//121
f 137//137 84//84 121//121
f 84//84 122//122 121//121
f 122//122 138//138 5//5
f 138//138 4//4 5//5
f 4//4 138//138 84//84
f 138//138 122//122 84//84
f 1//1 83//83 85//85
f 1//1 85//85 33//33
f 78//78 81//81 82//82
f 78//78 82//82 80//80
f 65//65 142//142 29//29
f 142//142 73//73 29//29
f 140//140 73//73 142//142
f 139//139 75//75 140//140
f 75//75 139//139 76//76
f 141//141 76//76 139//139
f 141//141 68//68 77//77
f 70//70 144//144 77//77
f 143//143 78//78 144//144
f 78//78 143//143 79//79
f 143//143 71//71 79//79
f 71//71 143//143 144//144
f 71//71 144//144 70//70
f 68//68 141//141 67//67
f 141//141 139//139 67//67
f 67//67 139//139 66//66
f 139//139 140//140 66//66
f 73//73 140//140 75//75
f 66//66 140//140 65//65
f 140//140 142//142 65//65
f 128//128 135//135 136//136
f 128//128 136//136 127//127
f 76//76 141//141 80//80
f 77//77 80//80 141//141
f 77//77 144//144 80//80
f 78//78 80//80 144//144
f 94//94 96//96 97//97
f 94//94 97//97 95//95
f 92//92 94//94 95//95
f 92//92 95//95 93//93
f 102//102 107//107 104//104
f 102//102 106//106 107//107
f 171//171 120//120 169//169
f 171//171 119//119 120//120
f 176//176 113//113 114//114
f 176//176 114//114 175//175
f 175//175 114//114 115//115
f 175//175 115//115 174//174
f 185//185 145//145 183//183
f 186//186 183//183 145//145
f 186//186 148//148 183//183
f 187//187 183//183 148//148
f 161//161 162//162 153//153
f 161//161 153//153 154//154
f 149//149 198//198 147//147
f 197//197 198//198 149//149
f 190//190 188//188 149//149
f 150//150 197//197 149//149
f 196//196 197//197 150//150
f 148//148 196//196 150//150
f 195//195 196//196 148//148
f 192//192 193//193 145//145
f 192//192 145//145 146//146
f 146//146 184//184 192//192
f 185//185 184//184 146//146
f 146//146 145//145 185//185
f 193//193 186//186 145//145
f 148//148 186//186 195//195
f 148//148 150//150 187//187
f 188//188 187//187 150//150
f 150//150 149//149 188//188
f 149//149 147//147 190//190
f 147//147 206//206 190//190
f 198//198 206//206 147//147
f 185//185 183//183 181//181
f 185//185 181//181 182//182
f 224//224 34//34 108//108
f 224//224 108//108 180//180
f 151//151 179//179 167//167
f 221//221 179//179 151//151
f 151//151 220//220 221//221
f 167//167 220//220 151//151
f 179//179 168//168 167//167
f 152//152 168//168 179//179
f 180//180 168//168 152//152
f 152//152 224//224 180//180
f 179//179 224//224 152//152
f 224//224 179//179 222//222
f 222//222 179//179 221//221
f 214//214 213//213 169//169
f 12//12 214//214 169//169
f 205//205 203//203 204//204
f 205//205 202//202 203//203
f 219//219 202//202 205//205
f 219//219 218//218 202//202
f 154//154 170//170 161//161
f 171//171 170//170 154//154
f 154//154 153//153 171//171
f 172//172 171//171 153//153
f 153//153 160//160 172//172
f 162//162 160//160 153//153
f 167//167 166//166 164//164
f 220//220 167//167 164//164
f 209//209 164//164 161//161
f 164//164 162//162 161//161
f 164//164 163//163 162//162
f 180//180 159//159 168//168
f 165//165 156//156 155//155
f 176//176 155//155 156//156
f 180//180 109//109 158//158
f 109//109 110//110 158//158
f 110//110 111//111 158//158
f 178//178 158//158 111//111
f 31//31 206//206 36//36
f 200//200 207//207 23//23
f 207//207 200//200 208//208
f 200//200 203//203 208//208
f 202//202 208//208 203//203
f 208//208 202//202 206//206
f 218//218 206//206 202//202
f 218//218 189//189 206//206
f 218//218 217//217 189//189
f 160//160 163//163 155//155
f 165//165 155//155 163//163
f 166//166 156//156 165//165
f 166//166 157//157 156//156
f 157//157 166//166 159//159
f 168//168 159//159 166//166
f 26//26 35//35 199//199
f 213//213 211//211 169//169
f 170//170 169//169 211//211
f 172//172 160//160 174//174
f 174//174 160//160 175//175
f 155//155 175//175 160//160
f 175//175 155//155 176//176
f 176//176 156//156 177//177
f 177//177 156//156 178//178
f 157//157 178//178 156//156
f 159//159 178//178 157//157
f 178//178 159//159 158//158
f 158//158 159//159 180//180
f 209//209 220//220 164//164
f 170//170 209//209 161//161
f 170//170 211//211 209//209
f 162//162 163//163 160//160
f 163//163 164//164 165//165
f 165//165 164//164 166//166
f 168//168 166//166 167//167
f 223//223 199//199 35//35
f 12//12 169//169 120//120
f 169//169 170//170 171//171
f 119//119 171//171 118//118
f 173//173 118//118 171//171
f 118//118 173//173 117//117
f 172//172 173//173 171//171
f 173//173 172//172 174//174
f 174//174 116//116 173//173
f 173//173 116//116 117//117
f 116//116 174//174 115//115
f 113//113 176//176 112//112
f 177//177 112//112 176//176
f 178//178 111//111 177//177
f 177//177 111//111 112//112
f 109//109 180//180 108//108
f 188//188 190//190 181//181
f 217//217 181//181 190//190
f 190//190 189//189 217//217
f 188//188 181//181 187//187
f 187//187 181//181 183//183
f 216//216 182//182 181//181
f 216//216 181//181 217//217
f 184//184 216//216 210//210
f 216//216 184//184 182//182
f 182//182 184//184 185//185
f 210//210 212//212 184//184
f 191//191 184//184 212//212
f 192//192 184//184 191//191
f 194//194 186//186 193//193
f 195//195 186//186 194//194
f 190//190 206//206 189//189
f 191//191 212//212 215//215
f 191//191 215//215 54//54
f 53//53 191//191 54//54
f 191//191 53//53 192//192
f 52//52 192//192 53//53
f 192//192 52//52 193//193
f 51//51 193//193 52//52
f 193//193 51//51 194//194
f 194//194 51//51 50//50
f 49//49 194//194 50//50
f 194//194 49//49 195//195
f 48//48 195//195 49//49
f 195//195 48//48 196//196
f 196//196 48//48 47//47
f 196//196 47//47 197//197
f 46//46 197//197 47//47
f 197//197 46//46 198//198
f 198//198 46//46 45//45
f 198//198 45//45 206//206
f 206//206 45//45 36//36
f 54//54 215//215 13//13
f 201//201 24//24 26//26
f 201//201 23//23 24//24
f 23//23 201//201 200//200
f 199//199 201//201 26//26
f 201//201 199//199 204//204
f 204//204 199//199 223//223
f 204//204 203//203 201//201
f 201//201 203//203 200//200
f 204//204 223//223 205//205
f 205//205 223//223 222//222
f 205//205 222//222 219//219
f 219//219 222//222 221//221
f 224//224 35//35 34//34
f 35//35 224//224 223//223
f 207//207 31//31 23//23
f 31//31 207//207 206//206
f 208//208 206//206 207//207
f 209//209 216//216 220//220
f 211//211 216//216 209//209
f 216//216 211//211 210//210
f 210//210 211//211 212//212
f 213//213 212//212 211//211
f 212//212 213//213 215//215
f 214//214 215//215 213//213
f 215//215 214//214 12//12
f 215//215 12//12 13//13
f 216//216 217//217 220//220
f 220//220 217//217 218//218
f 220//220 218//218 219//219
f 220//220 219//219 221//221
f 224//224 222//222 223//223
f 36//36 45//45 44//44
# 444 faces, 0 coords texture
# End of File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,924 @@
####
#
# OBJ File Generated by Meshlab
#
####
# Object forearm_collision_E3M5.obj
#
# Vertices: 228
# Faces: 452
#
####
vn -1.014278 -5.425926 2.002443
v -13.116530 -65.924423 164.889297
vn -1.445568 -5.080638 1.266666
v -20.459780 -71.908623 156.506897
vn -0.487387 -5.259754 1.266666
v -6.898247 -74.443710 156.506897
vn 0.487387 -5.259754 1.266666
v 6.898201 -74.443710 156.506897
vn 0.000000 -5.519899 2.001652
v -0.000023 -67.212082 164.889297
vn -0.516356 -5.566404 2.105098
v -6.228134 -67.212082 164.889297
vn 0.000000 -6.200466 -0.708931
v -0.000023 -67.212082 125.617798
vn -1.139329 -6.094893 -0.709044
v -13.116530 -65.924423 125.617798
vn -0.573518 -6.188871 -0.719127
v -6.228134 -67.212082 125.617798
vn 1.445567 -5.080638 1.266666
v 20.459740 -71.908623 156.506897
vn 1.014279 -5.425926 2.002443
v 13.116490 -65.924423 164.889297
vn 0.516356 -5.566404 2.105098
v 6.228096 -67.212082 164.889297
vn 1.139330 -6.094893 -0.709044
v 13.116490 -65.924423 125.617798
vn 0.573518 -6.188871 -0.719127
v 6.228096 -67.212082 125.617798
vn -1.994041 -5.147196 2.004810
v -25.733641 -62.110180 164.889297
vn -2.354518 -4.728508 1.266667
v -33.324581 -66.924767 156.506897
vn -1.531405 -5.376383 2.105857
v -18.472281 -64.923264 164.889297
vn -2.239874 -5.781761 -0.709382
v -25.733641 -62.110180 125.617798
vn -1.701028 -5.978075 -0.719336
v -18.472281 -64.923264 125.617798
vn 2.354519 -4.728508 1.266666
v 33.324539 -66.924767 156.506897
vn 1.994040 -5.147196 2.004810
v 25.733601 -62.110180 164.889297
vn 1.531405 -5.376382 2.105858
v 18.472231 -64.923264 164.889297
vn 2.239873 -5.781761 -0.709382
v 25.733601 -62.110180 125.617798
vn 1.701028 -5.978075 -0.719336
v 18.472231 -64.923264 125.617798
vn -0.610107 -3.066856 -2.952755
v -13.168620 -66.203033 -0.000004
vn 0.000000 -3.126960 -2.952861
v -0.000023 -67.500031 -0.000004
vn 0.610107 -3.066856 -2.952755
v 13.168570 -66.203033 -0.000004
vn 0.000000 0.000000 -6.283185
v -0.000023 -0.000023 -0.000004
vn -0.610402 -3.069248 2.978756
v -13.168620 -66.203033 182.800003
vn 0.000000 -3.129395 2.979192
v -0.000023 -67.500031 182.800003
vn 0.610402 -3.069248 2.978756
v 13.168570 -66.203033 182.800003
vn 0.000000 0.000000 6.283185
v -0.000023 -0.000023 182.800003
vn -2.905908 -4.693203 2.008757
v -37.370762 -55.913849 164.889297
vn -3.183289 -4.215355 1.266666
v -45.054562 -59.661869 156.506897
vn -2.494045 -5.002877 2.107387
v -30.087360 -60.423538 164.889297
vn -3.264125 -5.271745 -0.709945
v -37.370762 -55.913849 125.617798
vn -2.770584 -5.563671 -0.719755
v -30.087360 -60.423538 125.617798
vn 3.183291 -4.215355 1.266666
v 45.054508 -59.661869 156.506897
vn 2.905909 -4.693203 2.008757
v 37.370720 -55.913849 164.889297
vn 2.494045 -5.002877 2.107387
v 30.087320 -60.423538 164.889297
vn 3.264126 -5.271745 -0.709945
v 37.370720 -55.913849 125.617798
vn 2.770584 -5.563670 -0.719755
v 30.087320 -60.423538 125.617798
vn -1.196754 -2.888856 -2.952438
v -25.831150 -62.361889 -0.000004
vn 1.196754 -2.888856 -2.952438
v 25.831110 -62.361889 -0.000004
vn -1.197365 -2.891101 2.977438
v -25.831150 -62.361889 182.800003
vn 1.197364 -2.891102 2.977438
v 25.831110 -62.361889 182.800003
vn -1.737385 -2.599806 -2.951908
v -37.501011 -56.124222 -0.000004
vn 1.737386 -2.599805 -2.951907
v 37.500969 -56.124222 -0.000004
vn -1.738327 -2.601760 2.975215
v -37.501011 -56.124222 182.800003
vn 1.738327 -2.601760 2.975215
v 37.500969 -56.124222 182.800003
vn -3.718858 -4.079383 2.014284
v -47.584270 -47.570160 164.889297
vn -3.903659 -3.558654 1.266666
v -55.250252 -50.367260 156.506897
vn -3.371383 -4.458741 2.109718
v -40.677860 -53.866180 164.889297
vn -4.177236 -4.582198 -0.710733
v -47.584270 -47.570160 125.617798
vn -3.745765 -4.959772 -0.720382
v -40.677860 -53.866180 125.617798
vn 3.903658 -3.558657 1.266666
v 55.250198 -50.367260 156.506897
vn 3.718856 -4.079385 2.014282
v 47.584221 -47.570160 164.889297
vn 3.371385 -4.458741 2.109717
v 40.677818 -53.866180 164.889297
vn 4.177233 -4.582200 -0.710732
v 47.584221 -47.570160 125.617798
vn 3.745766 -4.959771 -0.720382
v 40.677818 -53.866180 125.617798
vn -2.211215 -2.210821 -2.951167
v -47.729729 -47.729729 -0.000004
vn 2.211215 -2.210821 -2.951168
v 47.729679 -47.729729 -0.000004
vn -2.212471 -2.212282 2.972061
v -47.729729 -47.729729 182.800003
vn 2.212471 -2.212283 2.972062
v 47.729679 -47.729729 182.800003
vn -4.405183 -3.326635 2.021387
v -55.984241 -37.395302 164.889297
vn -4.491092 -2.780767 1.266666
v -63.564468 -39.357460 156.506897
vn -4.133474 -3.762670 2.112887
v -49.883121 -45.474468 164.889297
vn -4.948087 -3.736616 -0.711746
v -55.984241 -37.395302 125.617798
vn -4.593356 -4.186948 -0.721219
v -49.883121 -45.474468 125.617798
vn 4.491090 -2.780766 1.266665
v 63.564430 -39.357460 156.506897
vn 4.405181 -3.326638 2.021386
v 55.984200 -37.395302 164.889297
vn 4.133472 -3.762674 2.112886
v 49.883080 -45.474468 164.889297
vn 4.948084 -3.736619 -0.711745
v 55.984200 -37.395302 125.617798
vn 4.593353 -4.186952 -0.721218
v 49.883080 -45.474468 125.617798
vn -2.600025 -1.736860 -2.950213
v -56.124222 -37.501011 -0.000004
vn 2.600025 -1.736861 -2.950213
v 56.124180 -37.501011 -0.000004
vn -2.601500 -1.737581 2.967929
v -56.124222 -37.501011 182.800003
vn 2.601499 -1.737581 2.967930
v 56.124180 -37.501011 182.800003
vn -4.941505 -2.460578 2.030066
v -62.249168 -25.775030 164.889297
vn -4.925585 -1.908184 1.266666
v -69.714081 -27.007389 156.506897
vn -4.754362 -2.938568 2.116949
v -57.389679 -35.534191 164.889297
vn -5.550435 -2.763790 -0.712983
v -62.249168 -25.775030 125.617798
vn -5.284479 -3.271534 -0.722264
v -57.389679 -35.534191 125.617798
vn 4.925588 -1.908184 1.266666
v 69.714027 -27.007389 156.506897
vn 4.941497 -2.460590 2.030060
v 62.249119 -25.775030 164.889297
vn 4.754361 -2.938570 2.116949
v 57.389641 -35.534191 164.889297
vn 5.550427 -2.763804 -0.712982
v 62.249119 -25.775030 125.617798
vn 5.284478 -3.271535 -0.722263
v 57.389641 -35.534191 125.617798
vn -2.888869 -1.196153 -2.949050
v -62.361889 -25.831150 -0.000004
vn 2.888869 -1.196152 -2.949049
v 62.361851 -25.831150 -0.000004
vn -2.890351 -1.195863 2.962770
v -62.361889 -25.831150 182.800003
vn 2.890351 -1.195861 2.962768
v 62.361851 -25.831150 182.800003
vn -5.309559 -1.510678 2.040326
v -66.138161 -13.150160 164.889297
vn -5.192345 -0.970616 1.266665
v -73.489662 -13.737610 156.506897
vn -5.212972 -2.014694 2.121964
v -62.941898 -24.383841 164.889297
vn -5.963784 -1.696817 -0.714447
v -66.138161 -13.150160 125.617798
vn -5.795593 -2.244724 -0.723518
v -62.941898 -24.383841 125.617798
vn 5.192345 -0.970616 1.266666
v 73.489616 -13.737610 156.506897
vn 5.309571 -1.510655 2.040333
v 66.138123 -13.150160 164.889297
vn 5.212969 -2.014712 2.121964
v 62.941860 -24.383841 164.889297
vn 5.963793 -1.696790 -0.714447
v 66.138123 -13.150160 125.617798
vn 5.795585 -2.244743 -0.723517
v 62.941860 -24.383841 125.617798
vn -3.066644 -0.609491 -2.947673
v -66.203033 -13.168620 -0.000004
vn 3.066644 -0.609489 -2.947674
v 66.202980 -13.168620 -0.000004
vn -3.067778 -0.607952 2.956517
v -66.203033 -13.168620 182.800003
vn 3.067779 -0.607951 2.956519
v 66.202980 -13.168620 182.800003
vn -5.493835 -1.022629 2.127995
v -66.350708 -12.403110 164.889297
vn -5.587918 0.000001 2.131134
v -67.500031 -0.000023 164.889297
vn -5.282289 0.000000 1.266666
v -74.762627 -0.000023 156.506897
vn -6.109307 -1.141456 -0.724981
v -66.350708 -12.403110 125.617798
vn 5.282288 -0.000000 1.266667
v 74.762581 -0.000023 156.506897
vn 5.493838 -1.022601 2.127992
v 66.350662 -12.403110 164.889297
vn 6.109313 -1.141426 -0.724982
v 66.350662 -12.403110 125.617798
vn 6.214976 0.000000 -0.725761
v 67.499977 -0.000023 125.617798
vn -3.126576 0.000000 -2.946928
v -67.500031 -0.000023 -0.000004
vn 3.126576 0.000000 -2.946928
v 67.499977 -0.000023 -0.000004
vn -3.127140 0.000000 2.952979
v -67.500031 -0.000023 182.800003
vn 3.127140 0.000000 2.952979
v 67.499977 -0.000023 182.800003
vn -6.214976 -0.000001 -0.725761
v -67.500031 -0.000023 125.617798
vn 5.587917 -0.000001 2.131134
v 67.499977 -0.000023 164.889297
vn -5.192345 0.970616 1.266666
v -73.489662 13.737570 156.506897
vn 5.192345 0.970616 1.266665
v 73.489616 13.737570 156.506897
vn -6.109306 1.141457 -0.724982
v -66.350708 12.403070 125.617798
vn 5.493838 1.022602 2.127993
v 66.350662 12.403070 164.889297
vn -3.066644 0.609490 -2.947673
v -66.203033 13.168570 -0.000004
vn 2.600024 1.736862 -2.950213
v 56.124180 37.500969 -0.000004
vn 2.888869 1.196151 -2.949048
v 62.361851 25.831110 -0.000004
vn 3.066644 0.609489 -2.947674
v 66.202980 13.168570 -0.000004
vn 1.196753 2.888856 -2.952437
v 25.831110 62.361851 -0.000004
vn 1.737387 2.599805 -2.951907
v 37.500969 56.124180 -0.000004
vn 2.211215 2.210821 -2.951169
v 47.729679 47.729679 -0.000004
vn -0.610106 3.066856 -2.952756
v -13.168620 66.202980 -0.000004
vn 0.000000 3.126960 -2.952861
v -0.000023 67.499977 -0.000004
vn 0.610106 3.066856 -2.952757
v 13.168570 66.202980 -0.000004
vn -2.211215 2.210821 -2.951168
v -47.729729 47.729679 -0.000004
vn -1.737386 2.599805 -2.951907
v -37.501011 56.124180 -0.000004
vn -1.196753 2.888856 -2.952437
v -25.831150 62.361851 -0.000004
vn -2.888869 1.196153 -2.949050
v -62.361889 25.831110 -0.000004
vn -2.600025 1.736861 -2.950213
v -56.124222 37.500969 -0.000004
vn 6.109313 1.141426 -0.724981
v 66.350662 12.403070 125.617798
vn -5.493836 1.022629 2.127994
v -66.350708 12.403070 164.889297
vn -3.067778 0.607952 2.956517
v -66.203033 13.168570 182.800003
vn -2.212471 2.212283 2.972063
v -47.729729 47.729679 182.800003
vn -2.601500 1.737581 2.967929
v -56.124222 37.500969 182.800003
vn -2.890351 1.195862 2.962769
v -62.361889 25.831110 182.800003
vn -0.610402 3.069248 2.978758
v -13.168620 66.202980 182.800003
vn -1.197363 2.891102 2.977436
v -25.831150 62.361851 182.800003
vn -1.738327 2.601759 2.975215
v -37.501011 56.124180 182.800003
vn 1.197363 2.891102 2.977436
v 25.831110 62.361851 182.800003
vn 0.610402 3.069248 2.978759
v 13.168570 66.202980 182.800003
vn 0.000000 3.129395 2.979196
v -0.000023 67.499977 182.800003
vn 2.601499 1.737582 2.967929
v 56.124180 37.500969 182.800003
vn 2.212471 2.212283 2.972064
v 47.729679 47.729679 182.800003
vn 1.738328 2.601759 2.975215
v 37.500969 56.124180 182.800003
vn 3.067779 0.607951 2.956519
v 66.202980 13.168570 182.800003
vn 2.890351 1.195861 2.962768
v 62.361851 25.831110 182.800003
vn -5.963783 1.696819 -0.714447
v -66.138161 13.150120 125.617798
vn 5.963793 1.696792 -0.714448
v 66.138123 13.150120 125.617798
vn -5.309559 1.510679 2.040327
v -66.138161 13.150120 164.889297
vn 5.309571 1.510656 2.040333
v 66.138123 13.150120 164.889297
vn -5.795593 2.244723 -0.723519
v -62.941898 24.383789 125.617798
vn -4.925584 1.908184 1.266666
v -69.714081 27.007351 156.506897
vn 5.795586 2.244742 -0.723517
v 62.941860 24.383789 125.617798
vn 4.925587 1.908184 1.266666
v 69.714027 27.007351 156.506897
vn -5.212971 2.014693 2.121964
v -62.941898 24.383789 164.889297
vn 5.212969 2.014710 2.121964
v 62.941860 24.383789 164.889297
vn -5.550437 2.763787 -0.712984
v -62.249168 25.774981 125.617798
vn 5.550428 2.763802 -0.712982
v 62.249119 25.774981 125.617798
vn -4.941506 2.460576 2.030066
v -62.249168 25.774981 164.889297
vn 4.941497 2.460588 2.030060
v 62.249119 25.774981 164.889297
vn -5.284474 3.271541 -0.722264
v -57.389679 35.534149 125.617798
vn -4.491091 2.780767 1.266666
v -63.564468 39.357422 156.506897
vn 5.284474 3.271541 -0.722264
v 57.389641 35.534149 125.617798
vn 4.491089 2.780766 1.266665
v 63.564430 39.357422 156.506897
vn -4.754358 2.938575 2.116949
v -57.389679 35.534149 164.889297
vn 4.754358 2.938576 2.116950
v 57.389641 35.534149 164.889297
vn -4.948082 3.736620 -0.711746
v -55.984241 37.395248 125.617798
vn 4.948079 3.736624 -0.711745
v 55.984200 37.395248 125.617798
vn -4.405178 3.326638 2.021385
v -55.984241 37.395248 164.889297
vn 4.405175 3.326641 2.021384
v 55.984200 37.395248 164.889297
vn -4.593358 4.186946 -0.721219
v -49.883121 45.474430 125.617798
vn -3.903659 3.558653 1.266666
v -55.250252 50.367222 156.506897
vn 4.593355 4.186950 -0.721218
v 49.883080 45.474430 125.617798
vn 3.903658 3.558656 1.266666
v 55.250198 50.367222 156.506897
vn -4.133475 3.762669 2.112888
v -49.883121 45.474430 164.889297
vn 4.133473 3.762673 2.112887
v 49.883080 45.474430 164.889297
vn -4.177235 4.582198 -0.710734
v -47.584270 47.570122 125.617798
vn 4.177232 4.582200 -0.710733
v 47.584221 47.570122 125.617798
vn -3.718858 4.079383 2.014286
v -47.584270 47.570122 164.889297
vn 3.718855 4.079385 2.014284
v 47.584221 47.570122 164.889297
vn -3.745766 4.959771 -0.720383
v -40.677860 53.866138 125.617798
vn -3.183288 4.215355 1.266666
v -45.054562 59.661831 156.506897
vn 3.745767 4.959770 -0.720382
v 40.677818 53.866138 125.617798
vn 3.183290 4.215355 1.266666
v 45.054508 59.661831 156.506897
vn -3.371384 4.458740 2.109718
v -40.677860 53.866138 164.889297
vn 3.371386 4.458740 2.109717
v 40.677818 53.866138 164.889297
vn -3.264127 5.271744 -0.709945
v -37.370762 55.913811 125.617798
vn 3.264128 5.271744 -0.709945
v 37.370720 55.913811 125.617798
vn -2.905910 4.693203 2.008758
v -37.370762 55.913811 164.889297
vn 2.905910 4.693203 2.008757
v 37.370720 55.913811 164.889297
vn -2.770584 5.563671 -0.719754
v -30.087360 60.423500 125.617798
vn -2.354518 4.728510 1.266666
v -33.324581 66.924721 156.506897
vn 2.770584 5.563670 -0.719754
v 30.087320 60.423500 125.617798
vn 2.354519 4.728509 1.266666
v 33.324539 66.924721 156.506897
vn -2.494045 5.002878 2.107387
v -30.087360 60.423500 164.889297
vn 2.494046 5.002878 2.107386
v 30.087320 60.423500 164.889297
vn -2.239869 5.781763 -0.709382
v -25.733641 62.110142 125.617798
vn 2.239868 5.781763 -0.709382
v 25.733601 62.110142 125.617798
vn -1.994037 5.147198 2.004810
v -25.733641 62.110142 164.889297
vn 1.994036 5.147199 2.004811
v 25.733601 62.110142 164.889297
vn -1.701023 5.978077 -0.719336
v -18.472281 64.923210 125.617798
vn -1.445565 5.080638 1.266666
v -20.459780 71.908577 156.506897
vn 1.701022 5.978077 -0.719337
v 18.472231 64.923210 125.617798
vn 1.445565 5.080638 1.266666
v 20.459740 71.908577 156.506897
vn -1.531400 5.376383 2.105858
v -18.472281 64.923210 164.889297
vn 1.531399 5.376382 2.105858
v 18.472231 64.923210 164.889297
vn -1.139337 6.094890 -0.709043
v -13.116530 65.924370 125.617798
vn 1.139338 6.094890 -0.709043
v 13.116490 65.924370 125.617798
vn -1.014284 5.425924 2.002441
v -13.116530 65.924370 164.889297
vn 1.014285 5.425924 2.002441
v 13.116490 65.924370 164.889297
vn -0.573528 6.188870 -0.719125
v -6.228134 67.212051 125.617798
vn -0.487388 5.259756 1.266665
v -6.898247 74.443657 156.506897
vn 0.573528 6.188870 -0.719125
v 6.228096 67.212051 125.617798
vn 0.487388 5.259756 1.266665
v 6.898201 74.443657 156.506897
vn -0.516364 5.566408 2.105098
v -6.228134 67.212051 164.889297
vn 0.516364 5.566408 2.105098
v 6.228096 67.212051 164.889297
vn 0.000000 6.200467 -0.708930
v -0.000023 67.212051 125.617798
vn 0.000000 5.519903 2.001652
v -0.000023 67.212051 164.889297
# 228 vertices, 0 vertices normals
f 1//1 2//2 3//3
f 4//4 5//5 3//3
f 5//5 6//6 3//3
f 6//6 1//1 3//3
f 7//7 4//4 3//3
f 2//2 8//8 3//3
f 8//8 9//9 3//3
f 9//9 7//7 3//3
f 10//10 11//11 4//4
f 11//11 12//12 4//4
f 12//12 5//5 4//4
f 13//13 10//10 4//4
f 7//7 14//14 4//4
f 14//14 13//13 4//4
f 15//15 16//16 2//2
f 1//1 17//17 2//2
f 17//17 15//15 2//2
f 16//16 18//18 2//2
f 18//18 19//19 2//2
f 19//19 8//8 2//2
f 20//20 21//21 10//10
f 21//21 22//22 10//10
f 22//22 11//11 10//10
f 23//23 20//20 10//10
f 13//13 24//24 10//10
f 24//24 23//23 10//10
f 9//9 25//25 26//26
f 27//27 14//14 26//26
f 14//14 7//7 26//26
f 7//7 9//9 26//26
f 28//28 27//27 26//26
f 25//25 28//28 26//26
f 29//29 6//6 30//30
f 6//6 5//5 30//30
f 5//5 12//12 30//30
f 12//12 31//31 30//30
f 32//32 29//29 30//30
f 31//31 32//32 30//30
f 8//8 25//25 9//9
f 27//27 13//13 14//14
f 29//29 1//1 6//6
f 11//11 31//31 12//12
f 33//33 34//34 16//16
f 15//15 35//35 16//16
f 35//35 33//33 16//16
f 34//34 36//36 16//16
f 36//36 37//37 16//16
f 37//37 18//18 16//16
f 38//38 39//39 20//20
f 39//39 40//40 20//20
f 40//40 21//21 20//20
f 41//41 38//38 20//20
f 23//23 42//42 20//20
f 42//42 41//41 20//20
f 19//19 43//43 25//25
f 8//8 19//19 25//25
f 43//43 28//28 25//25
f 44//44 24//24 27//27
f 24//24 13//13 27//27
f 28//28 44//44 27//27
f 32//32 45//45 29//29
f 45//45 17//17 29//29
f 17//17 1//1 29//29
f 46//46 32//32 31//31
f 11//11 22//22 31//31
f 22//22 46//46 31//31
f 18//18 43//43 19//19
f 44//44 23//23 24//24
f 45//45 15//15 17//17
f 21//21 46//46 22//22
f 37//37 47//47 43//43
f 18//18 37//37 43//43
f 47//47 28//28 43//43
f 48//48 42//42 44//44
f 42//42 23//23 44//44
f 28//28 48//48 44//44
f 32//32 49//49 45//45
f 49//49 35//35 45//45
f 35//35 15//15 45//45
f 50//50 32//32 46//46
f 21//21 40//40 46//46
f 40//40 50//50 46//46
f 36//36 47//47 37//37
f 48//48 41//41 42//42
f 49//49 33//33 35//35
f 39//39 50//50 40//40
f 51//51 52//52 34//34
f 33//33 53//53 34//34
f 53//53 51//51 34//34
f 52//52 54//54 34//34
f 54//54 55//55 34//34
f 55//55 36//36 34//34
f 56//56 57//57 38//38
f 57//57 58//58 38//38
f 58//58 39//39 38//38
f 59//59 56//56 38//38
f 41//41 60//60 38//38
f 60//60 59//59 38//38
f 55//55 61//61 47//47
f 36//36 55//55 47//47
f 61//61 28//28 47//47
f 62//62 60//60 48//48
f 60//60 41//41 48//48
f 28//28 62//62 48//48
f 32//32 63//63 49//49
f 63//63 53//53 49//49
f 53//53 33//33 49//49
f 64//64 32//32 50//50
f 39//39 58//58 50//50
f 58//58 64//64 50//50
f 54//54 61//61 55//55
f 62//62 59//59 60//60
f 63//63 51//51 53//53
f 57//57 64//64 58//58
f 65//65 66//66 52//52
f 51//51 67//67 52//52
f 67//67 65//65 52//52
f 66//66 68//68 52//52
f 68//68 69//69 52//52
f 69//69 54//54 52//52
f 70//70 71//71 56//56
f 71//71 72//72 56//56
f 72//72 57//57 56//56
f 73//73 70//70 56//56
f 59//59 74//74 56//56
f 74//74 73//73 56//56
f 69//69 75//75 61//61
f 54//54 69//69 61//61
f 75//75 28//28 61//61
f 76//76 74//74 62//62
f 74//74 59//59 62//62
f 28//28 76//76 62//62
f 32//32 77//77 63//63
f 77//77 67//67 63//63
f 67//67 51//51 63//63
f 78//78 32//32 64//64
f 57//57 72//72 64//64
f 72//72 78//78 64//64
f 68//68 75//75 69//69
f 76//76 73//73 74//74
f 77//77 65//65 67//67
f 71//71 78//78 72//72
f 79//79 80//80 66//66
f 65//65 81//81 66//66
f 81//81 79//79 66//66
f 80//80 82//82 66//66
f 82//82 83//83 66//66
f 83//83 68//68 66//66
f 84//84 85//85 70//70
f 85//85 86//86 70//70
f 86//86 71//71 70//70
f 87//87 84//84 70//70
f 73//73 88//88 70//70
f 88//88 87//87 70//70
f 83//83 89//89 75//75
f 68//68 83//83 75//75
f 89//89 28//28 75//75
f 90//90 88//88 76//76
f 88//88 73//73 76//76
f 28//28 90//90 76//76
f 32//32 91//91 77//77
f 91//91 81//81 77//77
f 81//81 65//65 77//77
f 92//92 32//32 78//78
f 71//71 86//86 78//78
f 86//86 92//92 78//78
f 82//82 89//89 83//83
f 90//90 87//87 88//88
f 91//91 79//79 81//81
f 85//85 92//92 86//86
f 93//93 94//94 80//80
f 79//79 95//95 80//80
f 95//95 93//93 80//80
f 94//94 96//96 80//80
f 96//96 97//97 80//80
f 97//97 82//82 80//80
f 98//98 99//99 84//84
f 99//99 100//100 84//84
f 100//100 85//85 84//84
f 101//101 98//98 84//84
f 87//87 102//102 84//84
f 102//102 101//101 84//84
f 97//97 103//103 89//89
f 82//82 97//97 89//89
f 103//103 28//28 89//89
f 104//104 102//102 90//90
f 102//102 87//87 90//90
f 28//28 104//104 90//90
f 32//32 105//105 91//91
f 105//105 95//95 91//91
f 95//95 79//79 91//91
f 106//106 32//32 92//92
f 85//85 100//100 92//92
f 100//100 106//106 92//92
f 96//96 103//103 97//97
f 104//104 101//101 102//102
f 105//105 93//93 95//95
f 99//99 106//106 100//100
f 93//93 107//107 94//94
f 107//107 108//108 94//94
f 108//108 109//109 94//94
f 109//109 110//110 94//94
f 110//110 96//96 94//94
f 111//111 112//112 98//98
f 112//112 99//99 98//98
f 101//101 113//113 98//98
f 113//113 114//114 98//98
f 114//114 111//111 98//98
f 110//110 115//115 103//103
f 96//96 110//110 103//103
f 115//115 28//28 103//103
f 116//116 113//113 104//104
f 113//113 101//101 104//104
f 28//28 116//116 104//104
f 32//32 117//117 105//105
f 117//117 107//107 105//105
f 107//107 93//93 105//105
f 118//118 32//32 106//106
f 99//99 112//112 106//106
f 112//112 118//118 106//106
f 119//119 115//115 110//110
f 109//109 119//119 110//110
f 116//116 114//114 113//113
f 117//117 108//108 107//107
f 120//120 118//118 112//112
f 111//111 120//120 112//112
f 121//121 119//119 109//109
f 122//122 120//120 111//111
f 121//121 123//123 119//119
f 122//122 124//124 120//120
f 125//125 28//28 115//115
f 119//119 123//123 115//115
f 123//123 125//125 115//115
f 126//126 127//127 28//28
f 127//127 128//128 28//28
f 128//128 116//116 28//28
f 129//129 130//130 28//28
f 130//130 131//131 28//28
f 131//131 126//126 28//28
f 132//132 133//133 28//28
f 133//133 134//134 28//28
f 134//134 129//129 28//28
f 135//135 136//136 28//28
f 136//136 137//137 28//28
f 137//137 132//132 28//28
f 125//125 138//138 28//28
f 138//138 139//139 28//28
f 139//139 135//135 28//28
f 140//140 111//111 114//114
f 108//108 141//141 109//109
f 141//141 121//121 109//109
f 140//140 122//122 111//111
f 117//117 141//141 108//108
f 32//32 142//142 117//117
f 142//142 141//141 117//117
f 143//143 144//144 32//32
f 144//144 145//145 32//32
f 145//145 142//142 32//32
f 146//146 147//147 32//32
f 147//147 148//148 32//32
f 148//148 143//143 32//32
f 149//149 150//150 32//32
f 150//150 151//151 32//32
f 151//151 146//146 32//32
f 152//152 153//153 32//32
f 153//153 154//154 32//32
f 154//154 149//149 32//32
f 118//118 155//155 32//32
f 155//155 156//156 32//32
f 156//156 152//152 32//32
f 128//128 140//140 116//116
f 140//140 114//114 116//116
f 124//124 118//118 120//120
f 124//124 155//155 118//118
f 157//157 125//125 123//123
f 121//121 157//157 123//123
f 128//128 158//158 140//140
f 158//158 122//122 140//140
f 142//142 159//159 141//141
f 159//159 121//121 141//141
f 160//160 155//155 124//124
f 122//122 160//160 124//124
f 161//161 125//125 157//157
f 121//121 162//162 157//157
f 162//162 161//161 157//157
f 128//128 163//163 158//158
f 164//164 122//122 158//158
f 163//163 164//164 158//158
f 142//142 165//165 159//159
f 162//162 121//121 159//159
f 165//165 162//162 159//159
f 166//166 155//155 160//160
f 122//122 164//164 160//160
f 164//164 166//166 160//160
f 161//161 138//138 125//125
f 127//127 163//163 128//128
f 145//145 165//165 142//142
f 166//166 156//156 155//155
f 167//167 138//138 161//161
f 162//162 167//167 161//161
f 127//127 168//168 163//163
f 168//168 164//164 163//163
f 145//145 169//169 165//165
f 169//169 162//162 165//165
f 170//170 156//156 166//166
f 164//164 170//170 166//166
f 171//171 138//138 167//167
f 162//162 172//172 167//167
f 172//172 171//171 167//167
f 127//127 173//173 168//168
f 174//174 164//164 168//168
f 173//173 174//174 168//168
f 145//145 175//175 169//169
f 172//172 162//162 169//169
f 175//175 172//172 169//169
f 176//176 156//156 170//170
f 164//164 174//174 170//170
f 174//174 176//176 170//170
f 171//171 139//139 138//138
f 126//126 173//173 127//127
f 144//144 175//175 145//145
f 176//176 152//152 156//156
f 177//177 139//139 171//171
f 172//172 177//177 171//171
f 126//126 178//178 173//173
f 178//178 174//174 173//173
f 144//144 179//179 175//175
f 179//179 172//172 175//175
f 180//180 152//152 176//176
f 174//174 180//180 176//176
f 181//181 139//139 177//177
f 172//172 182//182 177//177
f 182//182 181//181 177//177
f 126//126 183//183 178//178
f 184//184 174//174 178//178
f 183//183 184//184 178//178
f 144//144 185//185 179//179
f 182//182 172//172 179//179
f 185//185 182//182 179//179
f 186//186 152//152 180//180
f 174//174 184//184 180//180
f 184//184 186//186 180//180
f 181//181 135//135 139//139
f 131//131 183//183 126//126
f 143//143 185//185 144//144
f 186//186 153//153 152//152
f 187//187 135//135 181//181
f 182//182 187//187 181//181
f 131//131 188//188 183//183
f 188//188 184//184 183//183
f 143//143 189//189 185//185
f 189//189 182//182 185//185
f 190//190 153//153 186//186
f 184//184 190//190 186//186
f 191//191 135//135 187//187
f 182//182 192//192 187//187
f 192//192 191//191 187//187
f 131//131 193//193 188//188
f 194//194 184//184 188//188
f 193//193 194//194 188//188
f 143//143 195//195 189//189
f 192//192 182//182 189//189
f 195//195 192//192 189//189
f 196//196 153//153 190//190
f 184//184 194//194 190//190
f 194//194 196//196 190//190
f 191//191 136//136 135//135
f 130//130 193//193 131//131
f 148//148 195//195 143//143
f 196//196 154//154 153//153
f 197//197 136//136 191//191
f 192//192 197//197 191//191
f 130//130 198//198 193//193
f 198//198 194//194 193//193
f 148//148 199//199 195//195
f 199//199 192//192 195//195
f 200//200 154//154 196//196
f 194//194 200//200 196//196
f 201//201 136//136 197//197
f 192//192 202//202 197//197
f 202//202 201//201 197//197
f 130//130 203//203 198//198
f 204//204 194//194 198//198
f 203//203 204//204 198//198
f 148//148 205//205 199//199
f 202//202 192//192 199//199
f 205//205 202//202 199//199
f 206//206 154//154 200//200
f 194//194 204//204 200//200
f 204//204 206//206 200//200
f 201//201 137//137 136//136
f 129//129 203//203 130//130
f 147//147 205//205 148//148
f 206//206 149//149 154//154
f 207//207 137//137 201//201
f 202//202 207//207 201//201
f 129//129 208//208 203//203
f 208//208 204//204 203//203
f 147//147 209//209 205//205
f 209//209 202//202 205//205
f 210//210 149//149 206//206
f 204//204 210//210 206//206
f 211//211 137//137 207//207
f 202//202 212//212 207//207
f 212//212 211//211 207//207
f 129//129 213//213 208//208
f 214//214 204//204 208//208
f 213//213 214//214 208//208
f 147//147 215//215 209//209
f 212//212 202//202 209//209
f 215//215 212//212 209//209
f 216//216 149//149 210//210
f 204//204 214//214 210//210
f 214//214 216//216 210//210
f 211//211 132//132 137//137
f 134//134 213//213 129//129
f 146//146 215//215 147//147
f 216//216 150//150 149//149
f 217//217 132//132 211//211
f 212//212 217//217 211//211
f 134//134 218//218 213//213
f 218//218 214//214 213//213
f 146//146 219//219 215//215
f 219//219 212//212 215//215
f 220//220 150//150 216//216
f 214//214 220//220 216//216
f 221//221 132//132 217//217
f 212//212 222//222 217//217
f 222//222 221//221 217//217
f 134//134 223//223 218//218
f 224//224 214//214 218//218
f 223//223 224//224 218//218
f 146//146 225//225 219//219
f 222//222 212//212 219//219
f 225//225 222//222 219//219
f 226//226 150//150 220//220
f 214//214 224//224 220//220
f 224//224 226//226 220//220
f 221//221 133//133 132//132
f 133//133 223//223 134//134
f 151//151 225//225 146//146
f 226//226 151//151 150//150
f 227//227 133//133 221//221
f 222//222 227//227 221//221
f 223//223 133//133 227//227
f 222//222 224//224 227//227
f 224//224 223//223 227//227
f 151//151 228//228 225//225
f 228//228 222//222 225//225
f 151//151 226//226 228//228
f 224//224 222//222 228//228
f 226//226 224//224 228//228
# 452 faces, 0 coords texture
# End of File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,840 @@
####
#
# OBJ File Generated by Meshlab
#
####
# Object th_proximal_E3M5.obj
#
# Vertices: 207
# Faces: 410
#
####
vn 3.643954 -1.325716 4.671926
v 6.562990 -2.382770 47.242680
vn 3.001478 -2.518516 4.645406
v 5.352630 -4.479270 47.242611
vn 4.878201 -1.847160 3.037849
v 8.872470 -3.223370 44.399899
vn 3.531775 0.367557 4.782580
v 6.609160 0.001130 47.535782
vn 0.953052 -1.321807 5.562101
v 2.846790 -2.376570 49.092270
vn 1.082270 0.903166 5.793338
v 2.542210 1.397900 49.166180
vn 5.532956 -1.967820 1.233064
v 10.147950 -3.687670 40.895760
vn 5.984508 1.179062 0.027198
v 10.661380 2.264480 37.154110
vn 5.829669 0.383533 1.229223
v 10.657370 -0.005540 41.136059
vn 5.970188 1.091981 0.898389
v 12.342160 2.621740 3.915270
vn 5.446340 2.683794 0.644148
v 11.527860 5.127820 3.915270
vn 6.063344 0.301712 0.824233
v 11.016340 0.001130 12.128600
vn 5.500022 2.588229 0.008108
v 9.958090 4.428920 37.154110
vn 6.119324 0.420995 -0.352597
v 10.153960 0.001130 28.836269
vn 5.153292 3.235846 1.003221
v 8.875570 6.440060 12.121890
vn 6.101694 0.451245 0.317057
v 10.192280 0.001130 20.462151
vn 5.226054 3.232666 0.320266
v 8.195270 5.945820 20.458750
vn 5.161254 3.329227 -0.228657
v 8.177800 5.933080 28.837891
vn 4.379027 4.261180 0.709538
v 8.447140 9.368060 3.915280
vn 3.606742 4.990098 0.959890
v 6.452030 8.863550 12.121880
vn 3.656401 5.032544 0.322543
v 5.957840 8.183300 20.458740
vn 3.677874 5.010916 -0.186785
v 5.945050 8.165760 28.837900
vn 4.100842 4.504420 -0.342478
v 7.297320 8.091100 37.154179
vn 3.033220 5.348574 0.921753
v 6.315350 10.916900 3.915280
vn 1.478301 5.894803 1.005845
v 3.398320 10.419530 12.121890
vn 1.876934 5.851780 0.890083
v 3.908080 11.988670 3.915270
vn 1.429651 5.970322 0.318089
v 3.138460 9.619850 20.458750
vn 1.454439 5.962157 -0.254716
v 3.131750 9.599230 28.837891
vn 1.818428 5.802749 -0.367213
v 3.377120 10.354450 37.154171
vn 0.658779 6.091357 0.923589
v 1.330670 12.536530 3.915280
vn -1.462311 5.898105 1.026773
v -3.372080 10.419530 12.121890
vn -1.429653 5.970321 0.318084
v -3.112220 9.619850 20.458750
vn -1.505360 5.943258 -0.190814
v -3.105500 9.599230 28.837891
vn -0.609878 6.097325 0.993446
v -1.304430 12.536530 3.915280
vn -0.823472 5.939095 -0.386569
v -1.124750 10.827620 37.154179
vn -1.941430 5.838144 0.866579
v -3.881840 11.988670 3.915270
vn -3.011332 5.369691 0.823297
v -6.289110 10.916900 3.915280
vn -3.685035 4.933015 0.897817
v -6.425790 8.863550 12.121890
vn -3.656404 5.032538 0.322562
v -5.931600 8.183300 20.458750
vn -3.589581 5.077980 -0.214129
v -5.918800 8.165760 28.837891
vn -3.830478 4.677118 -0.264523
v -7.271060 8.091100 37.154179
vn -4.303851 4.312155 1.003222
v -8.420900 9.368060 3.915280
vn -5.502680 2.466675 1.035770
v -11.501620 5.127820 3.915280
vn -5.104654 3.318034 0.849405
v -8.849330 6.440060 12.121890
vn -5.226431 3.232253 0.316247
v -8.169020 5.945820 20.458750
vn -5.213428 3.266400 -0.361539
v -8.151560 5.933080 28.837891
vn -5.526800 2.659152 -0.140033
v -9.931840 4.428920 37.154110
vn -6.054203 0.416023 0.996481
v -10.990100 0.001130 12.128600
vn -6.101542 0.452416 0.318453
v -10.166030 0.001130 20.462139
vn -6.111949 0.326858 -0.205592
v -10.127710 0.001130 28.836269
vn -6.104822 0.234557 0.796840
v -12.591340 0.001130 3.915350
vn -6.041976 1.029987 -0.276721
v -10.635140 2.264480 37.154110
vn 1.314831 -3.311330 4.710030
v 3.498250 -6.035270 47.242680
vn 3.976700 -3.336886 3.053528
v 7.235230 -6.058970 44.399899
vn 5.018156 0.345359 2.765740
v 9.679700 -0.008860 44.256390
vn 5.817249 0.679453 -0.653917
v 12.917130 1.094790 -0.143840
vn 4.667316 1.257236 -3.420583
v 9.762570 3.168920 -7.261750
vn 3.888805 2.809895 -3.591537
v 8.306490 6.026620 -7.261750
vn 1.987558 2.209994 -5.150820
v 4.715470 5.683280 -9.958290
vn 2.887806 3.886230 -3.651655
v 6.038590 8.294500 -7.261680
vn 2.595609 -4.495737 3.053481
v 4.727060 -8.163670 44.399830
vn 4.521967 -3.764922 1.174573
v 8.275100 -6.931470 40.895760
vn 3.507578 4.399566 -2.056722
v 7.167060 9.847770 -3.834830
vn 3.774745 4.613333 -0.410994
v 7.642450 10.502040 0.009080
vn 1.155383 4.672535 -3.581500
v 3.180890 9.750570 -7.261680
vn 1.623348 5.367931 -2.189175
v 3.774220 11.576550 -3.834830
vn 1.838933 5.756236 -0.365424
v 4.024110 12.345690 0.009140
vn -1.043022 3.246955 -4.928975
v -2.272180 7.034460 -9.958720
vn -0.936706 1.028003 -5.756390
v -3.113070 2.272450 -11.679540
vn -1.205160 4.533834 -3.741524
v -3.154720 9.750570 -7.261750
vn 0.138728 5.428807 -2.425965
v 0.013120 12.172250 -3.834830
vn 0.023505 6.046159 -0.387344
v 0.013120 12.980970 0.009140
vn -1.756137 5.404866 -2.099213
v -3.747980 11.576550 -3.834820
vn -1.895714 5.737787 -0.365416
v -3.997870 12.345690 0.009150
vn -2.616550 2.293019 -4.965265
v -5.641700 4.785560 -9.956840
vn -2.727046 3.871344 -3.781431
v -6.012420 8.294500 -7.261750
vn -3.340392 4.597616 -2.099218
v -7.140890 9.847770 -3.834900
vn -3.534833 4.905265 -0.387354
v -7.616210 10.502040 0.009080
vn -3.918776 2.806617 -3.713833
v -8.280250 6.026620 -7.261750
vn -4.597654 3.340416 -2.099232
v -9.833490 7.155130 -3.834820
vn -4.885503 3.549578 -0.318645
v -10.487770 7.630490 0.009080
vn -3.398794 0.846840 -5.017704
v -6.828840 2.313770 -9.958100
vn -4.556650 1.169886 -3.723215
v -9.736330 3.168920 -7.261750
vn -5.407255 1.614365 -2.106083
v -11.562360 3.762210 -3.834820
vn -5.759212 1.719580 -0.342069
v -12.331480 4.012120 0.009080
vn -5.742759 0.060470 -2.095505
v -12.085490 0.036930 -3.513460
vn -6.143723 -0.010659 -0.440568
v -12.759300 -0.068200 -0.007480
vn 4.653790 3.397346 1.237363
v 8.275100 6.933760 40.895760
vn 4.144734 2.947149 2.988956
v 7.235240 6.061250 44.399899
vn 3.069663 2.126919 4.598249
v 5.352630 4.481490 47.242611
vn 2.942799 5.097024 1.176954
v 5.405810 9.341440 40.895760
vn 2.595623 4.495722 3.053526
v 4.727060 8.165910 44.399830
vn 1.503087 3.209566 4.746914
v 3.498240 6.037500 47.242680
vn 0.999551 5.798584 1.174548
v 1.885970 10.622540 40.895760
vn 0.761408 4.919088 3.246846
v 1.650220 9.285770 44.399899
vn -1.165416 5.739345 1.035257
v -1.859710 10.622540 40.895760
vn -0.798080 5.050174 3.119379
v -1.623970 9.285770 44.399899
vn -0.476519 3.527350 4.617452
v -1.197230 6.865440 47.242691
vn -1.167134 1.226254 5.592976
v -2.820540 2.378840 49.092270
vn -2.758894 5.143538 0.852557
v -5.379550 9.341440 40.895771
vn -2.578671 4.257617 3.246837
v -4.700880 8.165910 44.399899
vn -4.604956 3.542779 1.224830
v -8.248850 6.933760 40.895771
vn -3.960755 3.198165 3.044097
v -7.208980 6.061250 44.399910
vn -2.840159 2.563107 4.524149
v -5.326380 4.481490 47.242619
vn -5.768543 0.992651 1.084477
v -10.610570 1.655430 40.893169
vn -4.822252 0.685035 2.821594
v -9.637320 1.118840 44.529919
vn -3.291767 0.011668 4.950877
v -6.582910 0.001130 47.535789
vn 5.520185 -2.757620 -0.214762
v 9.958090 -4.426670 37.154110
vn 6.085318 -0.997570 -0.156997
v 10.661380 -2.262270 37.154110
vn -6.038405 -1.210480 0.815370
v -12.315920 -2.619470 3.915280
vn -5.846671 -1.946126 0.971062
v -10.405250 -3.383970 12.121890
vn -5.903769 -1.983475 0.325273
v -9.605580 -3.124170 20.458750
vn -5.928283 -1.890194 -0.244552
v -9.585000 -3.117470 28.837891
vn -6.073822 -0.914910 -0.196400
v -10.635140 -2.262270 37.154110
vn -5.480734 -2.721107 0.919936
v -11.501620 -5.125570 3.915280
vn -5.488225 -2.794405 -0.260208
v -9.931840 -4.426670 37.154110
vn -4.983471 -3.620678 0.954530
v -8.849330 -6.437770 12.121890
vn -5.032602 -3.656343 0.322523
v -8.169020 -5.943570 20.458750
vn -4.982490 -3.722709 -0.177699
v -8.151560 -5.930870 28.837891
vn -4.281575 -4.371631 0.919914
v -8.420900 -9.365770 3.915280
vn -3.606762 -4.990088 0.959874
v -6.425790 -8.861270 12.121890
vn -3.656373 -5.032553 0.322545
v -5.931600 -8.181070 20.458750
vn -3.677859 -5.010942 -0.186779
v -5.918800 -8.163470 28.837891
vn -4.100821 -4.504424 -0.342461
v -7.271060 -8.088870 37.154179
vn -3.033205 -5.348558 0.921776
v -6.289110 -10.914670 3.915280
vn -1.478313 -5.894798 1.005836
v -3.372080 -10.417270 12.121890
vn -1.876897 -5.851807 0.890121
v -3.881840 -11.986370 3.915280
vn -1.429619 -5.970338 0.318101
v -3.112220 -9.617570 20.458750
vn -1.506599 -5.954899 -0.317641
v -3.105500 -9.596970 28.837891
vn -1.884631 -5.804482 -0.297898
v -3.350860 -10.352170 37.154171
vn -0.658824 -6.091347 0.923541
v -1.304430 -12.534270 3.915280
vn 0.609892 -6.097320 0.993443
v 1.330670 -12.534270 3.915280
vn 0.849994 -5.977596 -0.326686
v 1.151000 -10.825370 37.154179
vn 1.462321 -5.898098 1.026783
v 3.398320 -10.417270 12.121890
vn 1.941437 -5.838161 0.866602
v 3.908080 -11.986370 3.915280
vn 1.429620 -5.970339 0.318073
v 3.138460 -9.617570 20.458750
vn 1.549845 -5.938266 -0.231086
v 3.131750 -9.596970 28.837891
vn 3.011300 -5.369688 0.823258
v 6.315350 -10.914670 3.915280
vn 3.685051 -4.933005 0.897822
v 6.452030 -8.861270 12.121880
vn 3.656374 -5.032552 0.322553
v 5.957840 -8.181070 20.458740
vn 3.589571 -5.078000 -0.214131
v 5.945050 -8.163470 28.837900
vn 3.859977 -4.662407 -0.283751
v 7.297320 -8.088870 37.154179
vn 4.303882 -4.312141 1.003237
v 8.447140 -9.365770 3.915280
vn 5.444722 -2.756230 0.963231
v 11.527860 -5.125570 3.915270
vn 4.934412 -3.704745 0.798712
v 8.875570 -6.437770 12.121890
vn 5.032592 -3.656352 0.322541
v 8.195270 -5.943570 20.458750
vn 5.035184 -3.658355 -0.312354
v 8.177800 -5.930870 28.837891
vn 5.875986 -1.859541 0.888353
v 10.431490 -3.383970 12.121880
vn 5.904180 -1.982954 0.319829
v 9.631830 -3.124170 20.458750
vn 5.908853 -1.969770 -0.324025
v 9.611250 -3.117470 28.837900
vn 6.022304 -0.928999 0.948214
v 12.342160 -2.619470 3.915270
vn -1.380640 -0.686773 -5.731635
v -3.661930 -1.192970 -11.679540
vn -3.538029 -0.420581 -5.019475
v -6.965720 -1.299690 -9.956520
vn -4.553372 -1.124327 -3.776149
v -9.736330 -3.166670 -7.261740
vn -5.333509 -1.591913 -2.073859
v -11.642710 -3.811340 -3.895890
vn -5.772764 -1.756671 -0.365727
v -12.331480 -4.009870 0.009080
vn -2.640641 -2.027757 -5.025055
v -5.969780 -4.345670 -9.958720
vn -3.890096 -2.857974 -3.723780
v -8.280250 -6.024370 -7.261750
vn -4.577250 -3.404147 -2.081670
v -9.833490 -7.152870 -3.834820
vn -4.885487 -3.549574 -0.318641
v -10.487770 -7.628270 0.009080
vn -3.015986 -3.748705 -3.782438
v -6.012420 -8.292270 -7.261750
vn -3.381532 -4.597293 -2.064599
v -7.140890 -9.845470 -3.834900
vn -3.556869 -4.895599 -0.406118
v -7.616210 -10.499770 0.009080
vn -0.151955 -3.128865 -5.147010
v 0.013120 -7.394170 -9.958650
vn 0.040648 -4.863536 -3.679814
v 0.013120 -10.250070 -7.261750
vn -1.730245 -5.444893 -2.053631
v -3.747980 -11.574270 -3.834820
vn -1.838889 -5.756212 -0.365412
v -3.997870 -12.343470 0.009150
vn 0.000032 -5.683005 -2.099215
v 0.013120 -12.169970 -3.834830
vn -0.023508 -6.046187 -0.387347
v 0.013120 -12.978670 0.009150
vn 1.058917 -0.678058 -5.726054
v 3.139310 -2.270170 -11.679540
vn 1.251667 -3.134328 -4.990292
v 2.298420 -7.032170 -9.958730
vn 1.541835 -4.514576 -3.748779
v 3.180890 -9.748270 -7.261680
vn 1.756146 -5.404825 -2.099266
v 3.774220 -11.574270 -3.834830
vn 1.895695 -5.737748 -0.365423
v 4.024110 -12.343470 0.009150
vn 2.684484 -2.247843 -4.978163
v 5.996020 -4.345670 -9.958730
vn 2.676205 -3.822081 -3.833149
v 6.038590 -8.292270 -7.261680
vn 3.340403 -4.597663 -2.099252
v 7.167060 -9.845470 -3.834830
vn 3.534827 -4.905265 -0.387390
v 7.642450 -10.499770 0.009080
vn 3.912368 -2.842569 -3.701086
v 8.306490 -6.024370 -7.261750
vn 4.597599 -3.340463 -2.099265
v 9.859720 -7.152870 -3.834830
vn 4.885472 -3.549591 -0.318647
v 10.514010 -7.628270 0.009070
vn 3.371641 -0.837551 -4.977191
v 7.046500 -2.284170 -9.958720
vn 4.660064 -1.132651 -3.602630
v 9.762570 -3.166670 -7.261750
vn 5.392434 -1.421524 -2.141711
v 11.588600 -3.759970 -3.834830
vn 5.762479 -1.746300 -0.416116
v 12.357720 -4.009870 0.009070
vn -5.548463 -1.885387 1.197290
v -10.121700 -3.687670 40.895771
vn -4.859341 -1.803729 3.077813
v -8.846220 -3.223370 44.399910
vn -3.662211 -1.202200 4.646036
v -6.536740 -2.382770 47.242691
vn -1.532655 -0.687359 5.524807
v -3.462870 -1.264020 49.092270
vn -4.521961 -3.764937 1.174558
v -8.248850 -6.931470 40.895771
vn -3.976726 -3.336860 3.053545
v -7.208980 -6.058970 44.399910
vn -2.913390 -2.495858 4.685129
v -5.326380 -4.479270 47.242619
vn -2.942815 -5.097037 1.176923
v -5.379550 -9.339170 40.895771
vn -2.595614 -4.495668 3.053477
v -4.700880 -8.163670 44.399910
vn -1.293353 -3.186611 4.758747
v -3.471990 -6.035270 47.242691
vn -0.999537 -5.798590 1.174547
v -1.859710 -10.620270 40.895760
vn -0.761328 -4.919127 3.246870
v -1.623970 -9.283470 44.399910
vn 1.087662 -5.772390 1.122651
v 1.885970 -10.620270 40.895760
vn 0.761386 -4.919119 3.246853
v 1.650220 -9.283470 44.399899
vn 2.855849 -5.123626 0.995183
v 5.405810 -9.339170 40.895760
vn -1.479902 -2.811926 -5.296168
v -3.199070 -6.568490 -9.783740
vn -1.688262 -4.207472 -3.834387
v -3.783350 -9.397950 -7.685140
vn 4.954120 2.930639 -1.914305
v 9.975860 6.396290 -3.987940
vn 5.330820 3.000355 -0.601284
v 10.945740 6.098010 -0.287480
vn 5.653251 0.776351 -2.327995
v 11.566650 1.440500 -3.961560
vn 0.448171 3.221065 -4.710190
v 1.441900 7.540370 -9.955090
vn 3.087555 0.878535 -4.950277
v 7.229860 1.884770 -9.952640
# 207 vertices, 0 vertices normals
f 1//1 2//2 3//3
f 1//1 4//4 5//5
f 6//6 5//5 4//4
f 10//10 11//11 12//12
f 13//13 8//8 14//14
f 12//12 15//15 16//16
f 15//15 12//12 11//11
f 16//16 17//17 14//14
f 17//17 16//16 15//15
f 14//14 18//18 13//13
f 18//18 14//14 17//17
f 11//11 19//19 15//15
f 20//20 15//15 19//19
f 15//15 20//20 17//17
f 21//21 17//17 20//20
f 17//17 21//21 18//18
f 22//22 18//18 21//21
f 22//22 23//23 13//13
f 18//18 22//22 13//13
f 19//19 24//24 20//20
f 20//20 25//25 21//21
f 20//20 24//24 26//26
f 25//25 20//20 26//26
f 21//21 27//27 22//22
f 27//27 21//21 25//25
f 22//22 28//28 23//23
f 28//28 22//22 27//27
f 29//29 23//23 28//28
f 26//26 30//30 25//25
f 31//31 25//25 30//30
f 25//25 31//31 27//27
f 27//27 32//32 28//28
f 32//32 27//27 31//31
f 28//28 33//33 29//29
f 33//33 28//28 32//32
f 30//30 34//34 31//31
f 35//35 29//29 33//33
f 31//31 34//34 36//36
f 36//36 37//37 31//31
f 31//31 38//38 32//32
f 38//38 31//31 37//37
f 39//39 32//32 38//38
f 32//32 39//39 33//33
f 40//40 33//33 39//39
f 40//40 41//41 35//35
f 33//33 40//40 35//35
f 37//37 42//42 38//38
f 38//38 42//42 43//43
f 44//44 38//38 43//43
f 38//38 44//44 39//39
f 45//45 39//39 44//44
f 39//39 45//45 40//40
f 40//40 46//46 41//41
f 46//46 40//40 45//45
f 47//47 41//41 46//46
f 48//48 44//44 43//43
f 44//44 48//48 45//45
f 49//49 45//45 48//48
f 45//45 49//49 46//46
f 46//46 50//50 47//47
f 50//50 46//46 49//49
f 43//43 51//51 48//48
f 52//52 47//47 50//50
f 2//2 53//53 54//54
f 2//2 1//1 5//5
f 4//4 1//1 55//55
f 7//7 9//9 3//3
f 11//11 10//10 56//56
f 58//58 57//57 59//59
f 60//60 58//58 59//59
f 54//54 61//61 62//62
f 64//64 19//19 11//11
f 24//24 19//19 64//64
f 60//60 65//65 63//63
f 65//65 60//60 59//59
f 66//66 63//63 65//65
f 63//63 66//66 64//64
f 67//67 64//64 66//66
f 64//64 67//67 24//24
f 26//26 24//24 67//67
f 65//65 70//70 66//66
f 70//70 65//65 68//68
f 66//66 71//71 67//67
f 71//71 66//66 70//70
f 72//72 30//30 26//26
f 67//67 72//72 26//26
f 72//72 67//67 71//71
f 34//34 30//30 72//72
f 73//73 71//71 70//70
f 71//71 73//73 72//72
f 72//72 74//74 34//34
f 74//74 72//72 73//73
f 36//36 34//34 74//74
f 75//75 68//68 69//69
f 68//68 75//75 70//70
f 76//76 70//70 75//75
f 70//70 76//76 73//73
f 73//73 77//77 74//74
f 77//77 73//73 76//76
f 78//78 74//74 77//77
f 78//78 37//37 36//36
f 74//74 78//78 36//36
f 42//42 37//37 78//78
f 76//76 79//79 77//77
f 79//79 76//76 75//75
f 80//80 77//77 79//79
f 77//77 80//80 78//78
f 81//81 78//78 80//80
f 78//78 81//81 42//42
f 43//43 42//42 81//81
f 75//75 82//82 79//79
f 82//82 75//75 69//69
f 83//83 79//79 82//82
f 79//79 83//83 80//80
f 84//84 80//80 83//83
f 80//80 84//84 81//81
f 81//81 85//85 43//43
f 85//85 81//81 84//84
f 51//51 43//43 85//85
f 86//86 84//84 83//83
f 84//84 86//86 85//85
f 85//85 87//87 51//51
f 87//87 85//85 86//86
f 3//3 9//9 55//55
f 8//8 13//13 9//9
f 88//88 9//9 13//13
f 9//9 88//88 55//55
f 55//55 89//89 4//4
f 89//89 55//55 88//88
f 4//4 90//90 6//6
f 90//90 4//4 89//89
f 13//13 23//23 88//88
f 88//88 91//91 89//89
f 91//91 88//88 23//23
f 89//89 92//92 90//90
f 92//92 89//89 91//91
f 90//90 93//93 6//6
f 93//93 90//90 92//92
f 23//23 29//29 91//91
f 94//94 91//91 29//29
f 91//91 94//94 92//92
f 92//92 95//95 93//93
f 95//95 92//92 94//94
f 94//94 96//96 95//95
f 94//94 29//29 35//35
f 96//96 94//94 35//35
f 95//95 97//97 93//93
f 97//97 95//95 96//96
f 98//98 93//93 97//97
f 93//93 98//98 6//6
f 99//99 6//6 98//98
f 96//96 100//100 97//97
f 96//96 35//35 41//41
f 100//100 96//96 41//41
f 97//97 101//101 98//98
f 101//101 97//97 100//100
f 100//100 102//102 101//101
f 102//102 100//100 41//41
f 101//101 103//103 98//98
f 103//103 101//101 102//102
f 104//104 98//98 103//103
f 98//98 104//104 99//99
f 41//41 47//47 102//102
f 105//105 102//102 47//47
f 102//102 105//105 103//103
f 103//103 106//106 104//104
f 106//106 103//103 105//105
f 107//107 104//104 106//106
f 104//104 107//107 99//99
f 47//47 52//52 105//105
f 55//55 1//1 3//3
f 108//108 109//109 7//7
f 51//51 110//110 48//48
f 54//54 3//3 2//2
f 48//48 111//111 49//49
f 111//111 48//48 110//110
f 112//112 49//49 111//111
f 112//112 50//50 49//49
f 113//113 50//50 112//112
f 113//113 114//114 52//52
f 113//113 52//52 50//50
f 62//62 7//7 54//54
f 110//110 115//115 111//111
f 116//116 114//114 113//113
f 111//111 117//117 112//112
f 117//117 111//111 115//115
f 112//112 118//118 113//113
f 118//118 112//112 117//117
f 113//113 119//119 116//116
f 119//119 113//113 118//118
f 115//115 120//120 117//117
f 121//121 117//117 120//120
f 117//117 121//121 118//118
f 118//118 122//122 119//119
f 122//122 118//118 121//121
f 123//123 119//119 122//122
f 123//123 124//124 116//116
f 119//119 123//123 116//116
f 120//120 125//125 121//121
f 121//121 126//126 122//122
f 121//121 125//125 127//127
f 126//126 121//121 127//127
f 128//128 122//122 126//126
f 122//122 128//128 123//123
f 123//123 129//129 124//124
f 129//129 123//123 128//128
f 130//130 124//124 129//129
f 127//127 131//131 126//126
f 131//131 132//132 126//126
f 133//133 130//130 129//129
f 126//126 134//134 128//128
f 128//128 136//136 129//129
f 136//136 128//128 134//134
f 129//129 137//137 133//133
f 137//137 129//129 136//136
f 135//135 138//138 134//134
f 139//139 134//134 138//138
f 134//134 139//139 136//136
f 136//136 140//140 137//137
f 140//140 136//136 139//139
f 141//141 137//137 140//140
f 141//141 142//142 133//133
f 137//137 141//141 133//133
f 138//138 143//143 139//139
f 139//139 143//143 144//144
f 145//145 139//139 144//144
f 139//139 145//145 140//140
f 140//140 146//146 141//141
f 146//146 140//140 145//145
f 141//141 147//147 142//142
f 147//147 141//141 146//146
f 108//108 142//142 147//147
f 145//145 148//148 146//146
f 148//148 145//145 144//144
f 146//146 149//149 147//147
f 149//149 146//146 148//148
f 147//147 150//150 108//108
f 150//150 147//147 149//149
f 144//144 151//151 148//148
f 109//109 108//108 150//150
f 148//148 151//151 10//10
f 148//148 10//10 12//12
f 7//7 62//62 108//108
f 148//148 12//12 149//149
f 149//149 16//16 150//150
f 149//149 12//12 16//16
f 150//150 14//14 109//109
f 3//3 54//54 7//7
f 14//14 150//150 16//16
f 8//8 109//109 14//14
f 69//69 152//152 82//82
f 153//153 82//82 152//152
f 82//82 153//153 83//83
f 154//154 86//86 83//83
f 154//154 83//83 153//153
f 155//155 86//86 154//154
f 155//155 87//87 86//86
f 156//156 110//110 51//51
f 156//156 51//51 87//87
f 156//156 87//87 155//155
f 115//115 110//110 156//156
f 157//157 153//153 152//152
f 153//153 157//157 154//154
f 154//154 158//158 155//155
f 158//158 154//154 157//157
f 155//155 159//159 156//156
f 159//159 155//155 158//158
f 160//160 156//156 159//159
f 156//156 160//160 115//115
f 158//158 161//161 159//159
f 161//161 158//158 157//157
f 159//159 162//162 160//160
f 162//162 159//159 161//161
f 163//163 160//160 162//162
f 125//125 120//120 163//163
f 162//162 166//166 163//163
f 163//163 167//167 125//125
f 167//167 163//163 166//166
f 127//127 125//125 167//167
f 166//166 168//168 167//167
f 168//168 166//166 165//165
f 169//169 167//167 168//168
f 169//169 131//131 127//127
f 167//167 169//169 127//127
f 132//132 131//131 169//169
f 152//152 170//170 164//164
f 170//170 152//152 69//69
f 171//171 164//164 170//170
f 164//164 171//171 165//165
f 165//165 172//172 168//168
f 172//172 165//165 171//171
f 168//168 173//173 169//169
f 173//173 168//168 172//172
f 174//174 169//169 173//173
f 169//169 174//174 132//132
f 135//135 132//132 174//174
f 175//175 171//171 170//170
f 171//171 175//175 172//172
f 172//172 176//176 173//173
f 176//176 172//172 175//175
f 173//173 177//177 174//174
f 177//177 173//173 176//176
f 178//178 174//174 177//177
f 178//178 138//138 135//135
f 174//174 178//178 135//135
f 143//143 138//138 178//178
f 179//179 176//176 175//175
f 176//176 179//179 177//177
f 180//180 177//177 179//179
f 177//177 180//180 178//178
f 181//181 178//178 180//180
f 178//178 181//181 143//143
f 144//144 143//143 181//181
f 182//182 175//175 170//170
f 175//175 182//182 179//179
f 179//179 183//183 180//180
f 183//183 179//179 182//182
f 184//184 180//180 183//183
f 180//180 184//184 181//181
f 181//181 185//185 144//144
f 185//185 181//181 184//184
f 151//151 144//144 185//185
f 170//170 69//69 59//59
f 183//183 57//57 184//184
f 184//184 56//56 185//185
f 185//185 56//56 151//151
f 10//10 151//151 56//56
f 52//52 114//114 105//105
f 186//186 106//106 105//105
f 186//186 105//105 114//114
f 53//53 2//2 5//5
f 106//106 187//187 107//107
f 187//187 106//106 186//186
f 188//188 107//107 187//187
f 142//142 108//108 62//62
f 189//189 99//99 188//188
f 114//114 116//116 186//186
f 190//190 186//186 116//116
f 186//186 190//190 187//187
f 187//187 191//191 188//188
f 191//191 187//187 190//190
f 192//192 188//188 191//191
f 188//188 192//192 189//189
f 116//116 124//124 190//190
f 193//193 190//190 124//124
f 190//190 193//193 191//191
f 194//194 191//191 193//193
f 191//191 194//194 192//192
f 192//192 195//195 189//189
f 195//195 192//192 194//194
f 124//124 130//130 193//193
f 193//193 196//196 194//194
f 196//196 193//193 130//130
f 194//194 197//197 195//195
f 197//197 194//194 196//196
f 196//196 130//130 133//133
f 198//198 196//196 133//133
f 196//196 198//198 197//197
f 199//199 197//197 198//198
f 197//197 199//199 195//195
f 53//53 195//195 199//199
f 195//195 53//53 189//189
f 5//5 189//189 53//53
f 189//189 5//5 99//99
f 198//198 200//200 199//199
f 61//61 199//199 200//200
f 199//199 61//61 53//53
f 62//62 200//200 142//142
f 200//200 62//62 61//61
f 61//61 54//54 53//53
f 8//8 9//9 109//109
f 7//7 109//109 9//9
f 133//133 200//200 198//198
f 133//133 142//142 200//200
f 120//120 160//160 163//163
f 115//115 160//160 120//120
f 157//157 201//201 161//161
f 164//164 201//201 152//152
f 201//201 157//157 152//152
f 165//165 166//166 202//202
f 164//164 165//165 202//202
f 162//162 202//202 166//166
f 161//161 202//202 162//162
f 164//164 202//202 201//201
f 161//161 201//201 202//202
f 63//63 203//203 60//60
f 56//56 204//204 11//11
f 204//204 64//64 11//11
f 64//64 204//204 203//203
f 64//64 203//203 63//63
f 57//57 205//205 184//184
f 205//205 56//56 184//184
f 57//57 203//203 205//205
f 57//57 58//58 203//203
f 58//58 60//60 203//203
f 56//56 203//203 204//204
f 56//56 205//205 203//203
f 59//59 206//206 65//65
f 206//206 68//68 65//65
f 68//68 206//206 69//69
f 206//206 59//59 69//69
f 59//59 207//207 170//170
f 207//207 182//182 170//170
f 182//182 207//207 183//183
f 183//183 207//207 57//57
f 207//207 59//59 57//57
f 132//132 135//135 134//134
f 126//126 132//132 134//134
f 5//5 6//6 99//99
f 99//99 107//107 188//188
# 410 faces, 0 coords texture
# End of File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,327 @@
<mujoco model="right_shadow_hand">
<compiler angle="radian" meshdir="assets" autolimits="true"/>
<option impratio="10" solver="CG" iterations="8" ls_iterations="6">
<flag eulerdamp="disable"/>
</option>
<custom>
<numeric data="15" name="max_contact_points"/>
</custom>
<default>
<default class="right_hand">
<mesh scale="0.001 0.001 0.001"/>
<joint axis="1 0 0" damping="0.05" armature="0.0002" frictionloss="0.01"/>
<position forcerange="-1 1"/>
<default class="wrist">
<joint damping="0.5"/>
<default class="wrist_y">
<joint axis="0 1 0" range="-0.523599 0.174533"/>
<position kp="10" ctrlrange="-0.523599 0.174533" forcerange="-10 10"/>
</default>
<default class="wrist_x">
<joint range="-0.698132 0.488692"/>
<position kp="8" ctrlrange="-0.698132 0.488692" forcerange="-5 5"/>
</default>
</default>
<default class="thumb">
<default class="thbase">
<joint axis="0 0 -1" range="-1.0472 1.0472"/>
<position kp="0.4" ctrlrange="-1.0472 1.0472" forcerange="-3 3"/>
</default>
<default class="thproximal">
<joint range="0 1.22173"/>
<position ctrlrange="0 1.22173" forcerange="-2 2"/>
</default>
<default class="thhub">
<joint range="-0.20944 0.20944"/>
<position kp="0.5" ctrlrange="-0.20944 0.20944"/>
</default>
<default class="thmiddle">
<joint axis="0 -1 0" range="-0.698132 0.698132"/>
<position kp="1.5" ctrlrange="-0.698132 0.698132"/>
</default>
<default class="thdistal">
<joint range="-0.261799 1.5708"/>
<position ctrlrange="-0.261799 1.5708"/>
</default>
</default>
<default class="metacarpal">
<joint axis="0.573576 0 0.819152" range="0 0.785398"/>
<position ctrlrange="0 0.785398"/>
</default>
<default class="knuckle">
<joint axis="0 -1 0" range="-0.349066 0.349066"/>
<position ctrlrange="-0.349066 0.349066"/>
</default>
<default class="proximal">
<joint range="-0.261799 1.5708"/>
<position ctrlrange="-0.261799 1.5708"/>
</default>
<default class="middle_distal">
<joint range="0 1.5708"/>
<position kp="0.5" ctrlrange="0 3.1415"/>
</default>
<default class="plastic">
<geom solimp="0.5 0.99 0.0001" solref="0.005 1"/>
<default class="plastic_visual">
<geom type="mesh" material="black" contype="0" conaffinity="0" group="2"/>
</default>
<default class="plastic_collision">
<geom group="3" contype="0" conaffinity="1"/>
</default>
</default>
</default>
</default>
<asset>
<material name="black" specular="0.5" shininess="0.25" rgba="0.16355 0.16355 0.16355 1"/>
<material name="gray" specular="0.0" shininess="0.25" rgba="0.80848 0.80848 0.80848 1"/>
<material name="metallic" specular="0" shininess="0.25" rgba="0.9 0.9 0.9 1"/>
<mesh class="right_hand" file="forearm_0.obj"/>
<mesh class="right_hand" file="forearm_1.obj"/>
<mesh class="right_hand" file="forearm_collision.obj"/>
<mesh class="right_hand" file="wrist.obj"/>
<mesh class="right_hand" file="palm.obj"/>
<mesh class="right_hand" file="f_knuckle.obj"/>
<mesh class="right_hand" file="f_proximal.obj"/>
<mesh class="right_hand" file="f_middle.obj"/>
<mesh class="right_hand" file="f_distal_pst.obj"/>
<mesh class="right_hand" file="lf_metacarpal.obj"/>
<mesh class="right_hand" file="th_proximal.obj"/>
<mesh class="right_hand" file="th_middle.obj"/>
<mesh class="right_hand" file="th_distal_pst.obj"/>
</asset>
<worldbody>
<body name="rh_forearm" childclass="right_hand" quat="1 -1 1 -1">
<inertial mass="3" pos="0 0 0.09" diaginertia="0.0138 0.0138 0.00744"/>
<geom class="plastic_visual" mesh="forearm_0" material="gray"/>
<geom class="plastic_visual" mesh="forearm_1"/>
<geom class="plastic_collision" type="mesh" mesh="forearm_collision" conaffinity="0"/>
<geom class="plastic_collision" size="0.035 0.035 0.035" pos="0 -0.01 0.181" quat="0.924909 0 0.380188 0"
type="box" conaffinity="0"/>
<body name="rh_wrist" pos="0 -0.01 0.21301">
<inertial mass="0.1" pos="0 0 0.029" quat="0.5 0.5 0.5 0.5" diaginertia="6.4e-05 4.38e-05 3.5e-05"/>
<joint class="wrist_y" name="rh_WRJ2"/>
<geom class="plastic_visual" mesh="wrist" material="metallic"/>
<geom size="0.0135 0.015" quat="0.499998 0.5 0.5 -0.500002" type="cylinder" class="plastic_collision" conaffinity="0"/>
<geom size="0.011 0.005" pos="-0.026 0 0.034" quat="1 0 1 0" type="cylinder" class="plastic_collision" conaffinity="0"/>
<geom size="0.011 0.005" pos="0.031 0 0.034" quat="1 0 1 0" type="cylinder" class="plastic_collision" conaffinity="0"/>
<geom size="0.0135 0.009 0.005" pos="-0.021 0 0.011" quat="0.923879 0 0.382684 0" type="box"
class="plastic_collision" conaffinity="0"/>
<geom size="0.0135 0.009 0.005" pos="0.026 0 0.01" quat="0.923879 0 -0.382684 0" type="box"
class="plastic_collision" conaffinity="0"/>
<body name="rh_palm" pos="0 0 0.034">
<inertial mass="0.3" pos="0 0 0.035" quat="1 0 0 1" diaginertia="0.0005287 0.0003581 0.000191"/>
<joint class="wrist_x" name="rh_WRJ1"/>
<site name="grasp_site" pos="0 -.035 0.09" group="4"/>
<geom class="plastic_visual" mesh="palm"/>
<geom size="0.031 0.0035 0.049" pos="0.011 0.0085 0.038" type="box" class="plastic_collision" conaffinity="2"/>
<geom size="0.018 0.0085 0.049" pos="-0.002 -0.0035 0.038" type="box" class="plastic_collision" conaffinity="2"/>
<geom size="0.013 0.0085 0.005" pos="0.029 -0.0035 0.082" type="box" class="plastic_collision" conaffinity="2"/>
<geom size="0.013 0.007 0.009" pos="0.0265 -0.001 0.07" quat="0.987241 0.0990545 0.0124467 0.124052"
type="box" class="plastic_collision" conaffinity="2"/>
<geom size="0.0105 0.0135 0.012" pos="0.0315 -0.0085 0.001" type="box" class="plastic_collision" conaffinity="2"/>
<geom size="0.011 0.0025 0.015" pos="0.0125 -0.015 0.004" quat="0.971338 0 0 -0.237703" type="box"
class="plastic_collision" conaffinity="2"/>
<geom size="0.009 0.012 0.002" pos="0.011 0 0.089" type="box" class="plastic_collision" conaffinity="2"/>
<geom size="0.01 0.012 0.02" pos="-0.03 0 0.009" type="box" class="plastic_collision" conaffinity="2"/>
<body name="rh_ffknuckle" pos="0.033 0 0.095">
<inertial mass="0.008" pos="0 0 0" quat="0.5 0.5 -0.5 0.5" diaginertia="3.2e-07 2.6e-07 2.6e-07"/>
<joint name="rh_FFJ4" class="knuckle"/>
<geom pos="0 0 0.0005" class="plastic_visual" mesh="f_knuckle" material="metallic"/>
<geom size="0.009 0.009" quat="1 0 1 0" type="cylinder" class="plastic_collision" conaffinity="0"/>
<body name="rh_ffproximal">
<inertial mass="0.03" pos="0 0 0.0225" quat="1 0 0 1" diaginertia="1e-05 9.8e-06 1.8e-06"/>
<joint name="rh_FFJ3" class="proximal"/>
<geom class="plastic_visual" mesh="f_proximal"/>
<geom size="0.009 0.02" pos="0 0 0.025" type="capsule" class="plastic_collision" contype="1"/>
<body name="rh_ffmiddle" pos="0 0 0.045">
<inertial mass="0.017" pos="0 0 0.0125" quat="1 0 0 1" diaginertia="2.7e-06 2.6e-06 8.7e-07"/>
<joint name="rh_FFJ2" class="middle_distal"/>
<geom class="plastic_visual" mesh="f_middle"/>
<geom size="0.009 0.0125" pos="0 0 0.0125" type="capsule" class="plastic_collision" contype="1"/>
<body name="rh_ffdistal" pos="0 0 0.025">
<inertial mass="0.013" pos="0 0 0.0130769" quat="1 0 0 1"
diaginertia="1.28092e-06 1.12092e-06 5.3e-07"/>
<joint name="rh_FFJ1" class="middle_distal"/>
<geom class="plastic_visual" mesh="f_distal_pst"/>
<geom class="plastic_collision" type="mesh" mesh="f_distal_pst"/>
</body>
</body>
</body>
</body>
<body name="rh_mfknuckle" pos="0.011 0 0.099">
<inertial mass="0.008" pos="0 0 0" quat="0.5 0.5 -0.5 0.5" diaginertia="3.2e-07 2.6e-07 2.6e-07"/>
<joint name="rh_MFJ4" class="knuckle"/>
<geom pos="0 0 0.0005" class="plastic_visual" mesh="f_knuckle" material="metallic"/>
<geom size="0.009 0.009" quat="1 0 1 0" type="cylinder" class="plastic_collision" conaffinity="0"/>
<body name="rh_mfproximal">
<inertial mass="0.03" pos="0 0 0.0225" quat="1 0 0 1" diaginertia="1e-05 9.8e-06 1.8e-06"/>
<joint name="rh_MFJ3" class="proximal"/>
<geom class="plastic_visual" mesh="f_proximal"/>
<geom size="0.009 0.02" pos="0 0 0.025" type="capsule" class="plastic_collision" contype="1"/>
<body name="rh_mfmiddle" pos="0 0 0.045">
<inertial mass="0.017" pos="0 0 0.0125" quat="1 0 0 1" diaginertia="2.7e-06 2.6e-06 8.7e-07"/>
<joint name="rh_MFJ2" class="middle_distal"/>
<geom class="plastic_visual" mesh="f_middle"/>
<geom size="0.009 0.0125" pos="0 0 0.0125" type="capsule" class="plastic_collision" contype="1"/>
<body name="rh_mfdistal" pos="0 0 0.025">
<inertial mass="0.013" pos="0 0 0.0130769" quat="1 0 0 1"
diaginertia="1.28092e-06 1.12092e-06 5.3e-07"/>
<joint name="rh_MFJ1" class="middle_distal"/>
<geom class="plastic_visual" mesh="f_distal_pst"/>
<geom class="plastic_collision" type="mesh" mesh="f_distal_pst"/>
</body>
</body>
</body>
</body>
<body name="rh_rfknuckle" pos="-0.011 0 0.095">
<inertial mass="0.008" pos="0 0 0" quat="0.5 0.5 -0.5 0.5" diaginertia="3.2e-07 2.6e-07 2.6e-07"/>
<joint name="rh_RFJ4" class="knuckle" axis="0 1 0"/>
<geom pos="0 0 0.0005" class="plastic_visual" mesh="f_knuckle" material="metallic"/>
<geom size="0.009 0.009" quat="1 0 1 0" type="cylinder" class="plastic_collision" conaffinity="0"/>
<body name="rh_rfproximal">
<inertial mass="0.03" pos="0 0 0.0225" quat="1 0 0 1" diaginertia="1e-05 9.8e-06 1.8e-06"/>
<joint name="rh_RFJ3" class="proximal"/>
<geom class="plastic_visual" mesh="f_proximal"/>
<geom size="0.009 0.02" pos="0 0 0.025" type="capsule" class="plastic_collision" contype="1"/>
<body name="rh_rfmiddle" pos="0 0 0.045">
<inertial mass="0.017" pos="0 0 0.0125" quat="1 0 0 1" diaginertia="2.7e-06 2.6e-06 8.7e-07"/>
<joint name="rh_RFJ2" class="middle_distal"/>
<geom class="plastic_visual" mesh="f_middle"/>
<geom size="0.009 0.0125" pos="0 0 0.0125" type="capsule" class="plastic_collision" contype="1"/>
<body name="rh_rfdistal" pos="0 0 0.025">
<inertial mass="0.013" pos="0 0 0.0130769" quat="1 0 0 1"
diaginertia="1.28092e-06 1.12092e-06 5.3e-07"/>
<joint name="rh_RFJ1" class="middle_distal"/>
<geom class="plastic_visual" mesh="f_distal_pst"/>
<geom class="plastic_collision" type="mesh" mesh="f_distal_pst"/>
</body>
</body>
</body>
</body>
<body name="rh_lfmetacarpal" pos="-0.033 0 0.02071">
<inertial mass="0.03" pos="0 0 0.04" quat="1 0 0 1" diaginertia="1.638e-05 1.45e-05 4.272e-06"/>
<joint name="rh_LFJ5" class="metacarpal"/>
<geom class="plastic_visual" mesh="lf_metacarpal"/>
<geom size="0.011 0.012 0.025" pos="0.002 0 0.033" type="box" class="plastic_collision"/>
<body name="rh_lfknuckle" pos="0 0 0.06579">
<inertial mass="0.008" pos="0 0 0" quat="0.5 0.5 -0.5 0.5" diaginertia="3.2e-07 2.6e-07 2.6e-07"/>
<joint name="rh_LFJ4" class="knuckle" axis="0 1 0"/>
<geom pos="0 0 0.0005" class="plastic_visual" mesh="f_knuckle" material="metallic"/>
<geom size="0.009 0.009" quat="1 0 1 0" type="cylinder" class="plastic_collision" conaffinity="0"/>
<body name="rh_lfproximal">
<inertial mass="0.03" pos="0 0 0.0225" quat="1 0 0 1" diaginertia="1e-05 9.8e-06 1.8e-06"/>
<joint name="rh_LFJ3" class="proximal"/>
<geom class="plastic_visual" mesh="f_proximal"/>
<geom size="0.009 0.02" pos="0 0 0.025" type="capsule" class="plastic_collision" contype="1"/>
<body name="rh_lfmiddle" pos="0 0 0.045">
<inertial mass="0.017" pos="0 0 0.0125" quat="1 0 0 1" diaginertia="2.7e-06 2.6e-06 8.7e-07"/>
<joint name="rh_LFJ2" class="middle_distal"/>
<geom class="plastic_visual" mesh="f_middle"/>
<geom size="0.009 0.0125" pos="0 0 0.0125" type="capsule" class="plastic_collision" contype="1"/>
<body name="rh_lfdistal" pos="0 0 0.025">
<inertial mass="0.013" pos="0 0 0.0130769" quat="1 0 0 1"
diaginertia="1.28092e-06 1.12092e-06 5.3e-07"/>
<joint name="rh_LFJ1" class="middle_distal"/>
<geom class="plastic_visual" mesh="f_distal_pst"/>
<geom class="plastic_collision" type="mesh" mesh="f_distal_pst"/>
</body>
</body>
</body>
</body>
</body>
<body name="rh_thbase" pos="0.034 -0.00858 0.029" quat="0.92388 0 0.382683 0">
<inertial mass="0.01" pos="0 0 0" diaginertia="1.6e-07 1.6e-07 1.6e-07"/>
<joint name="rh_THJ5" class="thbase"/>
<geom class="plastic_collision" size="0.013"/>
<body name="rh_thproximal">
<inertial mass="0.04" pos="0 0 0.019" diaginertia="1.36e-05 1.36e-05 3.13e-06"/>
<joint name="rh_THJ4" class="thproximal"/>
<geom class="plastic_visual" mesh="th_proximal"/>
<geom class="plastic_collision" size="0.0105 0.009" pos="0 0 0.02" type="capsule" contype="1"/>
<body name="rh_thhub" pos="0 0 0.038">
<inertial mass="0.005" pos="0 0 0" diaginertia="1e-06 1e-06 3e-07"/>
<joint name="rh_THJ3" class="thhub"/>
<geom size="0.011" class="plastic_collision" contype="1"/>
<body name="rh_thmiddle">
<inertial mass="0.02" pos="0 0 0.016" diaginertia="5.1e-06 5.1e-06 1.21e-06"/>
<joint name="rh_THJ2" class="thmiddle"/>
<geom class="plastic_visual" mesh="th_middle"/>
<geom size="0.009 0.009" pos="0 0 0.012" type="capsule" class="plastic_collision" contype="1"/>
<geom size="0.01" pos="0 0 0.03" class="plastic_collision"/>
<body name="rh_thdistal" pos="0 0 0.032" quat="1 0 0 -1">
<inertial mass="0.017" pos="0 0 0.0145588" quat="1 0 0 1"
diaginertia="2.37794e-06 2.27794e-06 1e-06"/>
<joint name="rh_THJ1" class="thdistal"/>
<geom class="plastic_visual" mesh="th_distal_pst"/>
<geom class="plastic_collision" type="mesh" mesh="th_distal_pst"/>
</body>
</body>
</body>
</body>
</body>
</body>
</body>
</body>
</worldbody>
<contact>
<exclude body1="rh_wrist" body2="rh_forearm"/>
<exclude body1="rh_thproximal" body2="rh_thmiddle"/>
</contact>
<!-- <tendon>
<fixed name="rh_FFJ0">
<joint joint="rh_FFJ2" coef="1"/>
<joint joint="rh_FFJ1" coef="1"/>
</fixed>
<fixed name="rh_MFJ0">
<joint joint="rh_MFJ2" coef="1"/>
<joint joint="rh_MFJ1" coef="1"/>
</fixed>
<fixed name="rh_RFJ0">
<joint joint="rh_RFJ2" coef="1"/>
<joint joint="rh_RFJ1" coef="1"/>
</fixed>
<fixed name="rh_LFJ0">
<joint joint="rh_LFJ2" coef="1"/>
<joint joint="rh_LFJ1" coef="1"/>
</fixed>
</tendon> -->
<actuator>
<position name="rh_A_WRJ2" joint="rh_WRJ2" class="wrist_y"/>
<position name="rh_A_WRJ1" joint="rh_WRJ1" class="wrist_x"/>
<position name="rh_A_THJ5" joint="rh_THJ5" class="thbase"/>
<position name="rh_A_THJ4" joint="rh_THJ4" class="thproximal"/>
<position name="rh_A_THJ3" joint="rh_THJ3" class="thhub"/>
<position name="rh_A_THJ2" joint="rh_THJ2" class="thmiddle"/>
<position name="rh_A_THJ1" joint="rh_THJ1" class="thdistal"/>
<position name="rh_A_FFJ4" joint="rh_FFJ4" class="knuckle"/>
<position name="rh_A_FFJ3" joint="rh_FFJ3" class="proximal"/>
<position name="rh_A_FFJ2" joint="rh_FFJ2" class="middle_distal"/>
<position name="rh_A_FFJ1" joint="rh_FFJ1" class="middle_distal"/>
<position name="rh_A_MFJ4" joint="rh_MFJ4" class="knuckle"/>
<position name="rh_A_MFJ3" joint="rh_MFJ3" class="proximal"/>
<position name="rh_A_MFJ2" joint="rh_MFJ2" class="middle_distal"/>
<position name="rh_A_MFJ1" joint="rh_MFJ1" class="middle_distal"/>
<position name="rh_A_RFJ4" joint="rh_RFJ4" class="knuckle"/>
<position name="rh_A_RFJ3" joint="rh_RFJ3" class="proximal"/>
<position name="rh_A_RFJ2" joint="rh_RFJ2" class="middle_distal"/>
<position name="rh_A_RFJ1" joint="rh_RFJ1" class="middle_distal"/>
<position name="rh_A_LFJ5" joint="rh_LFJ5" class="metacarpal"/>
<position name="rh_A_LFJ4" joint="rh_LFJ4" class="knuckle"/>
<position name="rh_A_LFJ3" joint="rh_LFJ3" class="proximal"/>
<position name="rh_A_LFJ2" joint="rh_LFJ2" class="middle_distal"/>
<position name="rh_A_LFJ1" joint="rh_LFJ1" class="middle_distal"/>
</actuator>
</mujoco>
@@ -0,0 +1,31 @@
<mujoco model="right_shadow_hand scene">
<include file="right_hand.xml"/>
<statistic extent="0.3" center="0.3 0 0"/>
<visual>
<rgba haze="0.15 0.25 0.35 1"/>
<quality shadowsize="8192"/>
<global azimuth="220" elevation="-30"/>
</visual>
<asset>
<texture type="skybox" builtin="gradient" rgb1="0.3 0.5 0.7" rgb2="0 0 0" width="512" height="3072"/>
<texture type="2d" name="groundplane" builtin="checker" mark="edge" rgb1="0.2 0.3 0.4" rgb2="0.1 0.2 0.3"
markrgb="0.8 0.8 0.8" width="300" height="300"/>
<material name="groundplane" texture="groundplane" texuniform="true" texrepeat="5 5" reflectance="0.2"/>
</asset>
<worldbody>
<light pos="0 0 1"/>
<light pos="0.3 0 1.5" dir="0 0 -1" directional="true"/>
<geom name="floor" pos="0 0 -0.1" size="0 0 0.05" type="plane" material="groundplane" contype="0" conaffinity="4"/>
<body name="object" pos="0.3 0 0.1">
<freejoint/>
<geom type="sphere" size="0.03" rgba="0.5 0.7 0.5 1" condim="3" priority="1"
friction="0.5 0.01 0.003" contype="7"/>
</body>
</worldbody>
</mujoco>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

@@ -0,0 +1,91 @@
# Copyright 2023 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Tests the collision driver."""
import dataclasses
from absl.testing import absltest
from absl.testing import parameterized
import jax
import mujoco
from mujoco import mjx
from mujoco.mjx._src import test_util
# pylint: disable=g-importing-member
from mujoco.mjx._src.types import Contact
# pylint: enable=g-importing-member
import numpy as np
def _assert_attr_eq(mjx_d, mj_d, attr, name, atol):
if attr == 'efc_address':
# we do not test efc_address since it gets set in constraint logic
return
err_msg = f'mismatch: {attr} in run: {name}'
mjx_d, mj_d = getattr(mjx_d, attr), getattr(mj_d, attr)
if attr == 'frame':
mj_d = mj_d.reshape((-1, 3, 3))
if mjx_d.shape != mj_d.shape:
raise AssertionError(f'{attr} shape mismatch: {mjx_d.shape}, {mj_d.shape}')
np.testing.assert_allclose(mjx_d, mj_d, err_msg=err_msg, atol=atol)
class CollisionDriverTest(parameterized.TestCase):
@parameterized.parameters(list(range(256)))
def test_collision_driver(self, seed):
enable_contact = False if seed == 0 else True
mjcf = test_util.create_mjcf(
seed,
body_pos=(0.0, 0.0, 0.14),
disable_actuation_pct=100,
root_always_free=True,
min_trees=1,
max_trees=5,
max_tree_depth=1,
enable_contact=enable_contact,
)
m = mujoco.MjModel.from_xml_string(mjcf)
mx = mjx.device_put(m)
d = mujoco.MjData(m)
dx = mjx.device_put(d)
mujoco.mj_step(m, d)
collision_jit_fn = jax.jit(mjx.collision)
kinematics_jit_fn = jax.jit(mjx.kinematics)
dx = kinematics_jit_fn(mx, dx)
dx = collision_jit_fn(mx, dx)
if not d.contact.geom1.shape[0]:
self.assertTrue((dx.contact.dist > 0).all())
return # no contacts to test
# re-order MJX contacts to match MJ order
idx_mj = list(zip(d.contact.geom1, d.contact.geom2))
idx_mjx = list(zip(dx.contact.geom1, dx.contact.geom2))
idx_mjx = [tuple(np.array(i)) for i in idx_mjx]
self.assertSequenceEqual(set(idx_mjx), set(idx_mj))
idx = sorted(range(len(idx_mj)), key=lambda x: idx_mj.index(idx_mjx[x]))
mjx_contact = jax.tree_map(
lambda x: x.take(np.array(idx), axis=0), dx.contact
)
mjx_contact = mjx_contact.replace(dim=mjx_contact.dim[idx])
for field in dataclasses.fields(Contact):
_assert_attr_eq(mjx_contact, d.contact, field.name, seed, 1e-7)
if __name__ == '__main__':
absltest.main()

Some files were not shown because too many files have changed in this diff Show More