Add special case support for MJB and TXT files to mj_encode.
PiperOrigin-RevId: 939875974 Change-Id: I57fc753871b348a7f6342aca0a892e74acb74173
This commit is contained in:
committed by
Copybara-Service
parent
d83ef0b6b9
commit
4e1795b9a4
@@ -66,11 +66,20 @@ Parse spec from a file.
|
|||||||
|
|
||||||
.. mujoco-include:: mj_encode
|
.. mujoco-include:: mj_encode
|
||||||
|
|
||||||
Encode spec/model to a file using a registered encoder.
|
Encode :ref:`mjSpec` or :ref:`mjModel` to a file. The output format is determined by the file extension (case insensitive) or
|
||||||
|
``content_type``. Returns the number of bytes written on success, -1 on failure.
|
||||||
|
|
||||||
Returns the number of bytes written on success, -1 on failure.
|
The following formats are supported natively, without a registered encoder:
|
||||||
|
|
||||||
*Nullable:* ``m``, ``vfs``, ``error``
|
- **MJCF XML** — extension: ``.xml``, content_type: ``text/xml``. If an :ref:`mjSpec` is provided, saves via
|
||||||
|
:ref:`mj_saveXML`. Otherwise falls back to :ref:`mj_saveLastXML`, which requires a compiled :ref:`mjModel`.
|
||||||
|
- **MJB** — extension: ``.mjb``. MuJoCo binary format. Requires a compiled :ref:`mjModel`.
|
||||||
|
- **TXT** — extension: ``.txt``, content_type: ``text/plain``. Human-readable text dump via :ref:`mj_printModel`.
|
||||||
|
Requires a compiled :ref:`mjModel`.
|
||||||
|
|
||||||
|
For all other formats, a registered encoder is looked up via :ref:`mjp_findEncoder`.
|
||||||
|
|
||||||
|
*Nullable:* ``s``, ``m``, ``vfs``, ``error``
|
||||||
|
|
||||||
.. _mj_compile:
|
.. _mj_compile:
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,23 @@ the required size. XML saving automatically compiles the spec before saving.
|
|||||||
|
|
||||||
Save spec to XML file, return 0 on success, -1 otherwise. XML saving requires that the spec first be compiled.
|
Save spec to XML file, return 0 on success, -1 otherwise. XML saving requires that the spec first be compiled.
|
||||||
|
|
||||||
|
.. _mj_encode:
|
||||||
|
|
||||||
|
Encode :ref:`mjSpec` or :ref:`mjModel` to a file. The output format is determined by the file extension (case insensitive) or
|
||||||
|
``content_type``. Returns the number of bytes written on success, -1 on failure.
|
||||||
|
|
||||||
|
The following formats are supported natively, without a registered encoder:
|
||||||
|
|
||||||
|
- **MJCF XML** — extension: ``.xml``, content_type: ``text/xml``. If an :ref:`mjSpec` is provided, saves via
|
||||||
|
:ref:`mj_saveXML`. Otherwise falls back to :ref:`mj_saveLastXML`, which requires a compiled :ref:`mjModel`.
|
||||||
|
- **MJB** — extension: ``.mjb``. MuJoCo binary format. Requires a compiled :ref:`mjModel`.
|
||||||
|
- **TXT** — extension: ``.txt``, content_type: ``text/plain``. Human-readable text dump via :ref:`mj_printModel`.
|
||||||
|
Requires a compiled :ref:`mjModel`.
|
||||||
|
|
||||||
|
For all other formats, a registered encoder is looked up via :ref:`mjp_findEncoder`.
|
||||||
|
|
||||||
|
*Nullable:* ``s``, ``m``, ``vfs``, ``error``
|
||||||
|
|
||||||
.. _Mainsimulation:
|
.. _Mainsimulation:
|
||||||
|
|
||||||
These are the main entry points to the simulator. Most users will only need to call :ref:`mj_step`, which computes
|
These are the main entry points to the simulator. Most users will only need to call :ref:`mj_step`, which computes
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ General
|
|||||||
- Added Nesterov momentum extrapolation with adaptive gradient restart (O'Donoghue-Candès) to the PGS solver,
|
- Added Nesterov momentum extrapolation with adaptive gradient restart (O'Donoghue-Candès) to the PGS solver,
|
||||||
significantly improving convergence. Overall PGS now requires ~2x fewer iterations.
|
significantly improving convergence. Overall PGS now requires ~2x fewer iterations.
|
||||||
|
|
||||||
|
* :ref:`mj_encode` now supports encoding of MJB and TXT files.
|
||||||
|
|
||||||
.. admonition:: Breaking API changes
|
.. admonition:: Breaking API changes
|
||||||
:class: attention
|
:class: attention
|
||||||
|
|
||||||
|
|||||||
+86
-8
@@ -15,6 +15,7 @@
|
|||||||
#include "user/user_api.h"
|
#include "user/user_api.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@@ -145,20 +146,97 @@ mjSpec* mj_parse(const char* filename, const char* content_type,
|
|||||||
return spec;
|
return spec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encode spec or model as MJCF XML.
|
||||||
|
//
|
||||||
|
// If a spec is provided, it is saved directly to XML. This preserves the original
|
||||||
|
// structure and any user modifications in the spec.
|
||||||
|
// If spec is null, the model must be provided, and it is saved using mj_saveLastXML.
|
||||||
|
// mj_saveLastXML reconstructs the XML from the compiled model state, which may
|
||||||
|
// differ from the original XML (e.g., losing comments, reordering elements) but
|
||||||
|
// reflects the actual compiled model.
|
||||||
|
//
|
||||||
|
// Returns file size in bytes on success, -1 on failure.
|
||||||
|
static mjtSize encode_xml(const mjSpec* s, const mjModel* m, const char* filename,
|
||||||
|
char* error, int error_sz) {
|
||||||
|
if (s) {
|
||||||
|
// Save directly from the spec
|
||||||
|
if (mj_saveXML(s, filename, error, error_sz) < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!m) {
|
||||||
|
if (error) {
|
||||||
|
strncpy(error, "model is required for XML encoding when spec is null",
|
||||||
|
error_sz);
|
||||||
|
error[error_sz - 1] = '\0';
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// Reconstruct XML from the compiled model, this will copy values back
|
||||||
|
// from the mjModel into the last compiled spec and write that out
|
||||||
|
// to disk. If there was no last compiled spec, such as when loading from
|
||||||
|
// MJB, this will return fail and we return -1.
|
||||||
|
if (!mj_saveLastXML(filename, m, error, error_sz)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return static_cast<mjtSize>(std::filesystem::file_size(filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode model as MJB (MuJoCo binary format).
|
||||||
|
// Requires a compiled model; spec-only encoding is not supported.
|
||||||
|
// Returns file size in bytes on success, -1 on failure.
|
||||||
|
static mjtSize encode_mjb(const mjModel* m, const char* filename, char* error, int error_sz) {
|
||||||
|
if (!m) {
|
||||||
|
if (error) {
|
||||||
|
strncpy(error, "model is required for MJB encoding", error_sz);
|
||||||
|
error[error_sz - 1] = '\0';
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
mj_saveModel(m, filename, nullptr, 0);
|
||||||
|
return static_cast<mjtSize>(std::filesystem::file_size(filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode model as human-readable TXT (via mj_printModel).
|
||||||
|
// Requires a compiled model; spec-only encoding is not supported.
|
||||||
|
// Returns file size in bytes on success, -1 on failure.
|
||||||
|
static mjtSize encode_txt(const mjModel* m, const char* filename, char* error,
|
||||||
|
int error_sz) {
|
||||||
|
if (!m) {
|
||||||
|
if (error) {
|
||||||
|
strncpy(error, "model is required for TXT encoding", error_sz);
|
||||||
|
error[error_sz - 1] = '\0';
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
mj_printModel(m, filename);
|
||||||
|
return static_cast<mjtSize>(std::filesystem::file_size(filename));
|
||||||
|
}
|
||||||
|
|
||||||
// encode spec/model to file
|
// encode spec/model to file
|
||||||
mjtSize mj_encode(const mjSpec* s, const mjModel* m, const char* filename,
|
mjtSize mj_encode(const mjSpec* s, const mjModel* m, const char* filename,
|
||||||
const char* content_type, const mjVFS* vfs, char* error,
|
const char* content_type, const mjVFS* vfs, char* error,
|
||||||
int error_sz) {
|
int error_sz) {
|
||||||
// TODO(shaves) Move MJCF and URDF to encoders/decoders.
|
// special case handling
|
||||||
|
// TODO(shaves) write encoder/decoder paths for MJCF, TXT, MJB
|
||||||
auto filepath = mujoco::user::FilePath(filename);
|
auto filepath = mujoco::user::FilePath(filename);
|
||||||
if (filepath.Ext() == ".xml" ||
|
std::string ext = filepath.Ext();
|
||||||
(content_type && std::strcmp(content_type, "text/xml") == 0)) {
|
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||||
int result = mj_saveXML(s, filename, error, error_sz);
|
[](unsigned char c) { return std::tolower(c); });
|
||||||
if (result < 0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return static_cast<mjtSize>(std::filesystem::file_size(filename));
|
if (ext == ".xml" ||
|
||||||
|
(content_type && std::strcmp(content_type, "text/xml") == 0)) {
|
||||||
|
return encode_xml(s, m, filename, error, error_sz);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ext == ".mjb") {
|
||||||
|
return encode_mjb(m, filename, error, error_sz);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ext == ".txt" ||
|
||||||
|
(content_type && std::strcmp(content_type, "text/plain") == 0)) {
|
||||||
|
return encode_txt(m, filename, error, error_sz);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mjpEncoder* encoder = mjp_findEncoder(filename, content_type);
|
const mjpEncoder* encoder = mjp_findEncoder(filename, content_type);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -3537,5 +3538,69 @@ TEST_F(MujocoTest, CompilationWarningsClearedOnRecompile) {
|
|||||||
mj_deleteSpec(spec);
|
mj_deleteSpec(spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(MujocoTest, MjEncodeNativeFormats) {
|
||||||
|
// simple test spec
|
||||||
|
mjSpec* spec = mj_makeSpec();
|
||||||
|
mjsBody* world = mjs_findBody(spec, "world");
|
||||||
|
mjsBody* body = mjs_addBody(world, nullptr);
|
||||||
|
mjsGeom* geom = mjs_addGeom(body, nullptr);
|
||||||
|
geom->size[0] = 1.0;
|
||||||
|
geom->size[1] = 1.0;
|
||||||
|
geom->size[2] = 1.0;
|
||||||
|
|
||||||
|
mjModel* model = mj_compile(spec, nullptr);
|
||||||
|
ASSERT_THAT(model, NotNull());
|
||||||
|
|
||||||
|
std::filesystem::path tmp_dir = std::filesystem::temp_directory_path();
|
||||||
|
std::string xml_path = (tmp_dir / "test_encode.xml").string();
|
||||||
|
std::string mjb_path = (tmp_dir / "test_encode.mjb").string();
|
||||||
|
std::string txt_path = (tmp_dir / "test_encode.txt").string();
|
||||||
|
|
||||||
|
char error[1000];
|
||||||
|
|
||||||
|
// XML Encoding with spec
|
||||||
|
EXPECT_GT(mj_encode(spec, model, xml_path.c_str(), nullptr, nullptr, error,
|
||||||
|
sizeof(error)),
|
||||||
|
0);
|
||||||
|
EXPECT_TRUE(std::filesystem::exists(xml_path));
|
||||||
|
EXPECT_GT(std::filesystem::file_size(xml_path), 0);
|
||||||
|
|
||||||
|
// XML Encoding without spec (should fail because no XML loaded in global
|
||||||
|
// spec)
|
||||||
|
std::filesystem::remove(xml_path);
|
||||||
|
// free global spec
|
||||||
|
mj_freeLastXML();
|
||||||
|
EXPECT_EQ(mj_encode(nullptr, model, xml_path.c_str(), nullptr, nullptr, error,
|
||||||
|
sizeof(error)),
|
||||||
|
-1);
|
||||||
|
EXPECT_THAT(error, HasSubstr("No XML model loaded"));
|
||||||
|
EXPECT_TRUE(std::filesystem::exists(xml_path));
|
||||||
|
EXPECT_EQ(std::filesystem::file_size(xml_path), 0);
|
||||||
|
std::filesystem::remove(xml_path);
|
||||||
|
|
||||||
|
// MJB Encoding (spec can be null)
|
||||||
|
EXPECT_GT(mj_encode(nullptr, model, mjb_path.c_str(), nullptr, nullptr, error,
|
||||||
|
sizeof(error)),
|
||||||
|
0);
|
||||||
|
EXPECT_TRUE(std::filesystem::exists(mjb_path));
|
||||||
|
EXPECT_GT(std::filesystem::file_size(mjb_path), 0);
|
||||||
|
|
||||||
|
mjModel* model2 = mj_loadModel(mjb_path.c_str(), nullptr);
|
||||||
|
EXPECT_THAT(model2, NotNull());
|
||||||
|
mj_deleteModel(model2);
|
||||||
|
|
||||||
|
// TXT Encoding (spec can be null)
|
||||||
|
EXPECT_GT(mj_encode(nullptr, model, txt_path.c_str(), nullptr, nullptr, error,
|
||||||
|
sizeof(error)),
|
||||||
|
0);
|
||||||
|
EXPECT_TRUE(std::filesystem::exists(txt_path));
|
||||||
|
EXPECT_GT(std::filesystem::file_size(txt_path), 0);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
std::filesystem::remove(mjb_path);
|
||||||
|
std::filesystem::remove(txt_path);
|
||||||
|
mj_deleteModel(model);
|
||||||
|
mj_deleteSpec(spec);
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
} // namespace mujoco
|
} // namespace mujoco
|
||||||
|
|||||||
Reference in New Issue
Block a user