diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 1fda2951..9d136432 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -104,6 +104,12 @@ target_link_libraries( ) target_include_directories(fixture PRIVATE ${mujoco_SOURCE_DIR}/include gmock) +add_library(compare_model STATIC compare_model.h compare_model.cc) +target_include_directories(compare_model PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/..) +target_compile_definitions(compare_model PUBLIC MJSTATIC) +target_link_libraries(compare_model PUBLIC mujoco::mujoco) +target_include_directories(compare_model PRIVATE ${mujoco_SOURCE_DIR}/include) + mujoco_test(fixture_test) mujoco_test(header_test) diff --git a/test/compare_model.cc b/test/compare_model.cc new file mode 100644 index 00000000..47400b39 --- /dev/null +++ b/test/compare_model.cc @@ -0,0 +1,138 @@ +// Copyright 2026 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "test/compare_model.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace mujoco { +namespace { + +template +auto Compare(T val1, T val2); + +auto Compare(char val1, char val2) { + return val1 != val2; +} + +auto Compare(unsigned char val1, unsigned char val2) { + return val1 != val2; +} + +auto Compare(bool val1, bool val2) { + return val1 != val2; +} + +auto Compare(mjtSize val1, mjtSize val2) { + return val1 > val2 ? val1 - val2 : val2 - val1; +} + +// The maximum spacing between a normalised floating point number x and an +// adjacent normalised number is 2 epsilon |x|; a factor 10 is added accounting +// for losses during non-idempotent operations such as vector normalizations. +template +auto Compare(T val1, T val2) { + using ReturnType = + std::conditional_t, float, double>; + ReturnType error; + if (std::abs(val1) <= 1 || std::abs(val2) <= 1) { + // Absolute precision for small numbers + error = std::abs(val1-val2); + } else { + // Relative precision for larger numbers + ReturnType magnitude = std::abs(val1) + std::abs(val2); + error = std::abs(val1/magnitude - val2/magnitude) / magnitude; + } + ReturnType safety_factor = 200; + return error < safety_factor * std::numeric_limits::epsilon() + ? 0 + : error; +} + +} // namespace + +mjtNum CompareModel(const mjModel* m1, const mjModel* m2, + std::string& field) { + mjtNum dif, maxdif = 0.0; + + // define symbols corresponding to number of columns + // (needed in MJMODEL_POINTERS) + MJMODEL_POINTERS_PREAMBLE(m1); + +// compare ints, exclude nbuffer because it hides the actual difference +#define X(name) \ + if constexpr (std::string_view(#name) != "nbuffer") { \ + if (m1->name != m2->name) { \ + maxdif = std::abs((long)m1->name - (long)m2->name); \ + field = #name; \ + } \ + } + MJMODEL_SIZES +#undef X + if (maxdif > 0) return maxdif; + + // compare arrays, apart from bvh-related ones (which includes flex_vert0), as + // those are sensitive to numerical differences when meshes are perfectly + // symmetric. Also skip flex fields derived from node local positions and + // cell geometry that are not fully serialized to XML. +#define X(type, name, nr, nc) \ + if (strncmp(#name, "bvh_", 4) && \ + strncmp(#name, "flex_vert", 9) && \ + strncmp(#name, "mesh_poly", 9) && \ + strcmp(#name, "flex_centered") && \ + strcmp(#name, "flex_size") && \ + strcmp(#name, "flexedge_length0") && \ + strcmp(#name, "flexedge_invweight0") && \ + strncmp(#name, "flex_node", 9)) { \ + for (int r = 0; r < m1->nr; r++) { \ + for (int c = 0; c < nc; c++) { \ + dif = Compare(m1->name[r * nc + c], m2->name[r * nc + c]); \ + if (dif > maxdif) { \ + maxdif = dif; \ + field = #name; \ + field += " row: " + std::to_string(r); \ + field += " col: " + std::to_string(c); \ + } \ + } \ + } \ + } // NOLINT + MJMODEL_POINTERS +#undef X + + // compare fields in mjOption + #define X(type, name, n) \ + dif = Compare(m1->opt.name, m2->opt.name); \ + if (dif > maxdif) {maxdif = dif; field = #name;} + #define XVEC(type, name, n) \ + for (int c=0; c < n; c++) { \ + dif = Compare(m1->opt.name[c], m2->opt.name[c]); \ + if (dif > maxdif) {maxdif = dif; field = #name;} } + MJOPTION_FIELDS + #undef XVEC + #undef X + + // Return largest difference and field name + return maxdif; +} + +} // namespace mujoco diff --git a/test/compare_model.h b/test/compare_model.h new file mode 100644 index 00000000..1d69b88b --- /dev/null +++ b/test/compare_model.h @@ -0,0 +1,30 @@ +// Copyright 2026 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MUJOCO_TEST_COMPARE_MODEL_H_ +#define MUJOCO_TEST_COMPARE_MODEL_H_ + +#include + +#include + +namespace mujoco { + +// Compares all fields of two mjModels. +// Returns the name of the different field and the max difference. +mjtNum CompareModel(const mjModel* m1, const mjModel* m2, std::string& field); + +} // namespace mujoco + +#endif // MUJOCO_TEST_COMPARE_MODEL_H_ diff --git a/test/fixture.cc b/test/fixture.cc index 33c3d8ac..b66de8e9 100644 --- a/test/fixture.cc +++ b/test/fixture.cc @@ -192,110 +192,6 @@ std::vector GetCtrlNoise(const mjModel* m, int nsteps, return ctrl; } -template -auto Compare(T val1, T val2); - -auto Compare(char val1, char val2) { - return val1 != val2; -} - -auto Compare(unsigned char val1, unsigned char val2) { - return val1 != val2; -} - -auto Compare(bool val1, bool val2) { - return val1 != val2; -} - -auto Compare(mjtSize val1, mjtSize val2) { - return val1 > val2 ? val1 - val2 : val2 - val1; -} - -// The maximum spacing between a normalised floating point number x and an -// adjacent normalised number is 2 epsilon |x|; a factor 10 is added accounting -// for losses during non-idempotent operations such as vector normalizations. -template -auto Compare(T val1, T val2) { - using ReturnType = - std::conditional_t, float, double>; - ReturnType error; - if (std::abs(val1) <= 1 || std::abs(val2) <= 1) { - // Absolute precision for small numbers - error = std::abs(val1-val2); - } else { - // Relative precision for larger numbers - ReturnType magnitude = std::abs(val1) + std::abs(val2); - error = std::abs(val1/magnitude - val2/magnitude) / magnitude; - } - ReturnType safety_factor = 200; - return error < safety_factor * std::numeric_limits::epsilon() - ? 0 - : error; -} - -mjtNum CompareModel(const mjModel* m1, const mjModel* m2, - std::string& field) { - mjtNum dif, maxdif = 0.0; - - // define symbols corresponding to number of columns - // (needed in MJMODEL_POINTERS) - MJMODEL_POINTERS_PREAMBLE(m1); - -// compare ints, exclude nbuffer because it hides the actual difference -#define X(name) \ - if constexpr (std::string_view(#name) != "nbuffer") { \ - if (m1->name != m2->name) { \ - maxdif = std::abs((long)m1->name - (long)m2->name); \ - field = #name; \ - } \ - } - MJMODEL_SIZES -#undef X - if (maxdif > 0) return maxdif; - - // compare arrays, apart from bvh-related ones (which includes flex_vert0), as - // those are sensitive to numerical differences when meshes are perfectly - // symmetric. Also skip flex fields derived from node local positions and - // cell geometry that are not fully serialized to XML. -#define X(type, name, nr, nc) \ - if (strncmp(#name, "bvh_", 4) && \ - strncmp(#name, "flex_vert", 9) && \ - strncmp(#name, "mesh_poly", 9) && \ - strcmp(#name, "flex_centered") && \ - strcmp(#name, "flex_size") && \ - strcmp(#name, "flexedge_length0") && \ - strcmp(#name, "flexedge_invweight0") && \ - strncmp(#name, "flex_node", 9)) { \ - for (int r = 0; r < m1->nr; r++) { \ - for (int c = 0; c < nc; c++) { \ - dif = Compare(m1->name[r * nc + c], m2->name[r * nc + c]); \ - if (dif > maxdif) { \ - maxdif = dif; \ - field = #name; \ - field += " row: " + std::to_string(r); \ - field += " col: " + std::to_string(c); \ - } \ - } \ - } \ - } // NOLINT - MJMODEL_POINTERS -#undef X - - // compare fields in mjOption - #define X(type, name, n) \ - dif = Compare(m1->opt.name, m2->opt.name); \ - if (dif > maxdif) {maxdif = dif; field = #name;} - #define XVEC(type, name, n) \ - for (int c=0; c < n; c++) { \ - dif = Compare(m1->opt.name[c], m2->opt.name[c]); \ - if (dif > maxdif) {maxdif = dif; field = #name;} } - MJOPTION_FIELDS - #undef XVEC - #undef X - - // Return largest difference and field name - return maxdif; -} MockFilesystem::MockFilesystem(std::string unit_test_name) { prefix_ = absl::StrCat("MjMock.", unit_test_name); diff --git a/test/fixture.h b/test/fixture.h index 8ed78750..bcf4c0de 100644 --- a/test/fixture.h +++ b/test/fixture.h @@ -182,9 +182,6 @@ std::string SaveAndReadXml(const mjSpec* spec); std::vector GetCtrlNoise(const mjModel* m, int nsteps, mjtNum ctrlnoise = 0.01); -// Compares all fields of two mjModels. -// Returns the name of the different field and the max difference. -mjtNum CompareModel(const mjModel* m1, const mjModel* m2, std::string& field); // Returns a vector containing the elements of the array. template diff --git a/test/user/CMakeLists.txt b/test/user/CMakeLists.txt index bc07f262..3b073c3d 100644 --- a/test/user/CMakeLists.txt +++ b/test/user/CMakeLists.txt @@ -14,13 +14,14 @@ mujoco_test( user_model_test - ADDITIONAL_LINK_LIBRARIES absl::str_format + ADDITIONAL_LINK_LIBRARIES absl::str_format compare_model ) mujoco_test(user_objects_test) mujoco_test( user_api_test + ADDITIONAL_LINK_LIBRARIES compare_model PROPERTIES ENVIRONMENT "MUJOCO_PLUGIN_DIR=$" @@ -28,6 +29,7 @@ mujoco_test( mujoco_test( user_recompile_test + ADDITIONAL_LINK_LIBRARIES compare_model PROPERTIES ENVIRONMENT "MUJOCO_PLUGIN_DIR=$" diff --git a/test/user/user_api_test.cc b/test/user/user_api_test.cc index b65b273f..701381bb 100644 --- a/test/user/user_api_test.cc +++ b/test/user/user_api_test.cc @@ -31,6 +31,7 @@ #include #include #include "src/xml/xml_api.h" +#include "test/compare_model.h" #include "test/fixture.h" namespace mujoco { diff --git a/test/user/user_model_test.cc b/test/user/user_model_test.cc index 529a73f9..831d570b 100644 --- a/test/user/user_model_test.cc +++ b/test/user/user_model_test.cc @@ -26,6 +26,7 @@ #include #include #include +#include "test/compare_model.h" #include "test/fixture.h" namespace mujoco { diff --git a/test/user/user_recompile_test.cc b/test/user/user_recompile_test.cc index 2945c170..f01c306e 100644 --- a/test/user/user_recompile_test.cc +++ b/test/user/user_recompile_test.cc @@ -29,6 +29,7 @@ #include #include "src/xml/xml_api.h" #include "src/xml/xml_numeric_format.h" +#include "test/compare_model.h" #include "test/fixture.h" namespace mujoco { diff --git a/test/xml/CMakeLists.txt b/test/xml/CMakeLists.txt index 920e6933..8aac751e 100644 --- a/test/xml/CMakeLists.txt +++ b/test/xml/CMakeLists.txt @@ -16,6 +16,7 @@ mujoco_test(xml_api_test) mujoco_test( xml_native_reader_test + ADDITIONAL_LINK_LIBRARIES compare_model ) mujoco_test(xml_utils_test) @@ -33,6 +34,7 @@ mujoco_test( xml_write_read_test ADDITIONAL_LINK_LIBRARIES absl::flat_hash_set + compare_model PROPERTIES ENVIRONMENT "MUJOCO_PLUGIN_DIR=$" diff --git a/test/xml/mjz/CMakeLists.txt b/test/xml/mjz/CMakeLists.txt index 718cf9df..c50e8185 100644 --- a/test/xml/mjz/CMakeLists.txt +++ b/test/xml/mjz/CMakeLists.txt @@ -20,4 +20,5 @@ mujoco_test( mujoco_test( mjz_encoder_test + ADDITIONAL_LINK_LIBRARIES compare_model ) diff --git a/test/xml/mjz/mjz_encoder_test.cc b/test/xml/mjz/mjz_encoder_test.cc index c9cd1ff1..21786bfc 100644 --- a/test/xml/mjz/mjz_encoder_test.cc +++ b/test/xml/mjz/mjz_encoder_test.cc @@ -31,6 +31,7 @@ #include #include #include "src/xml/xml_numeric_format.h" +#include "test/compare_model.h" #include "test/fixture.h" namespace mujoco { diff --git a/test/xml/xml_native_reader_test.cc b/test/xml/xml_native_reader_test.cc index 3897282a..ee36ca6c 100644 --- a/test/xml/xml_native_reader_test.cc +++ b/test/xml/xml_native_reader_test.cc @@ -30,6 +30,7 @@ #include "src/cc/array_safety.h" #include "src/engine/engine_util_errmem.h" #include "src/xml/xml_api.h" +#include "test/compare_model.h" #include "test/fixture.h" namespace mujoco { diff --git a/test/xml/xml_write_read_test.cc b/test/xml/xml_write_read_test.cc index 1a1e1255..0bc10d27 100644 --- a/test/xml/xml_write_read_test.cc +++ b/test/xml/xml_write_read_test.cc @@ -28,6 +28,7 @@ #include #include #include "src/xml/xml_numeric_format.h" +#include "test/compare_model.h" #include "test/fixture.h" namespace mujoco {