// Copyright 2022 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_PYTHON_STRUCTS_H_ #define MUJOCO_PYTHON_STRUCTS_H_ #include #include #include #include #include #include #include #include #include #include #include #include "indexers.h" #include "mjdata_meta.h" #include "raw.h" #include #include namespace mujoco::python { namespace _impl { template class WrapperBase { public: WrapperBase(WrapperBase&& other) = default; T* get() { return ptr_; } const T* get() const { return ptr_; } const pybind11::handle owner() const { return owner_; } protected: static void DefaultCapsuleDestructor(PyObject* pyobj) { T* ptr = static_cast(PyCapsule_GetPointer(pyobj, nullptr)); if (ptr) { delete ptr; } } // Takes ownership of ptr. explicit WrapperBase(T* ptr, void (*destructor)(PyObject*) = DefaultCapsuleDestructor) : ptr_(ptr), owner_(pybind11::capsule(ptr_, /* name = */ nullptr, destructor)) {} // `ptr` is owned by `owner`. WrapperBase(T* ptr, pybind11::handle owner) : ptr_(ptr), owner_(pybind11::reinterpret_borrow(owner)) {} T* ptr_; pybind11::object owner_; }; template struct py_array_or_tuple { using type = pybind11::tuple; }; template struct py_array_or_tuple>> { using type = pybind11::array_t; }; // A type that resolves to a NumPy array if the dtype is numeric, and // a Python tuple otherwise. template using py_array_or_tuple_t = typename py_array_or_tuple::type; template struct enable_if_mj_struct {}; template class MjWrapper {}; template class StructListBase { public: StructListBase(T* ptr, int num, pybind11::handle owner, bool lazy = false) : ptr_(ptr), num_(num), owner_(owner) { if (!lazy) { PopulateUpTo(size()); } } StructListBase(const StructListBase& other) = delete; StructListBase(StructListBase&& other) = default; virtual ~StructListBase() = default; MjWrapper& operator[](int i) { if (i < 0 || i >= size()) { throw pybind11::index_error(); } PopulateUpTo(i); return *wrappers_[i]; } virtual int size() const { return num_; } T* get() const { return ptr_; } pybind11::handle owner() const { return owner_; } protected: void PopulateUpTo(int n) { while (wrappers_.size() <= n) { wrappers_.push_back( std::make_shared>(&ptr_[wrappers_.size()], owner_)); } } // Slicing StructListBase(StructListBase& other, pybind11::slice slice) : owner_(other.owner_) { pybind11::size_t start, stop, step, slicelength; if (!slice.compute(other.size(), &start, &stop, &step, &slicelength)) { throw pybind11::index_error(); } other.PopulateUpTo(stop); ptr_ = &other.ptr_[start]; for (int i = start; i < stop; i += step) { wrappers_.push_back(other.wrappers_[i]); } num_ = wrappers_.size(); } T* ptr_; int num_; pybind11::handle owner_; // Using shared_ptr here so that we get identical Python objects when slicing. std::vector>> wrappers_; }; template struct is_mj_struct_list { static constexpr bool value = false; }; template class MjStructList {}; // ==================== MJOPTION =============================================== template <> class MjWrapper : public WrapperBase { public: MjWrapper(); MjWrapper(const MjWrapper&); MjWrapper(MjWrapper&&) = default; MjWrapper(raw::MjOption* ptr, pybind11::handle owner); ~MjWrapper() = default; #define X(var, dim) py_array_or_tuple_t var; MJOPTION_VECTORS #undef X }; using MjOptionWrapper = MjWrapper; template <> struct enable_if_mj_struct { using type = void; }; // ==================== MJVISUAL =============================================== template <> class MjWrapper : public WrapperBase { public: MjWrapper(); MjWrapper(const MjWrapper& other); MjWrapper(MjWrapper&&) = default; MjWrapper(raw::MjVisualHeadlight* ptr, pybind11::handle owner); ~MjWrapper() = default; #define X(var) py_array_or_tuple_t var X(ambient); X(diffuse); X(specular); #undef X }; using MjVisualHeadlightWrapper = MjWrapper; template <> struct enable_if_mj_struct { using type = void; }; template <> class MjWrapper : public WrapperBase { public: MjWrapper(); MjWrapper(const MjWrapper& other); MjWrapper(MjWrapper&&) = default; MjWrapper(raw::MjVisualRgba* ptr, pybind11::handle owner); ~MjWrapper() = default; #define X(var) py_array_or_tuple_t var X(fog); X(haze); X(force); X(inertia); X(joint); X(actuator); X(actuatornegative); X(actuatorpositive); X(com); X(camera); X(light); X(selectpoint); X(connect); X(contactpoint); X(contactforce); X(contactfriction); X(contacttorque); X(contactgap); X(rangefinder); X(constraint); X(slidercrank); X(crankbroken); #undef X }; using MjVisualRgbaWrapper = MjWrapper; template <> struct enable_if_mj_struct { using type = void; }; template <> class MjWrapper : public WrapperBase { public: MjWrapper(); MjWrapper(const MjWrapper& other); MjWrapper(MjWrapper&&) = default; MjWrapper(raw::MjVisual* ptr, pybind11::handle owner); ~MjWrapper() = default; MjVisualHeadlightWrapper headlight; MjVisualRgbaWrapper rgba; }; using MjVisualWrapper = MjWrapper; template <> struct enable_if_mj_struct { using type = void; }; // ==================== MJSTATISTIC ============================================ template <> class MjWrapper : public WrapperBase { public: MjWrapper(); MjWrapper(const MjWrapper&); MjWrapper(MjWrapper&&) = default; MjWrapper(raw::MjStatistic* ptr, pybind11::handle owner); ~MjWrapper() = default; #define X(var) py_array_or_tuple_t var X(center); #undef X }; using MjStatisticWrapper = MjWrapper; template <> struct enable_if_mj_struct { using type = void; }; // ==================== MJWARNINGSTAT ========================================== template <> class MjWrapper : public WrapperBase { public: MjWrapper(); MjWrapper(const MjWrapper&); MjWrapper(MjWrapper&&) = default; MjWrapper(raw::MjWarningStat* ptr, pybind11::handle owner); ~MjWrapper() = default; }; using MjWarningStatWrapper = MjWrapper; template <> struct enable_if_mj_struct { using type = void; }; template <> class MjStructList : public StructListBase { public: MjStructList(raw::MjWarningStat* ptr, int num, pybind11::handle owner); MjStructList(MjStructList&&) = default; ~MjStructList() override = default; using StructListBase::operator[]; using StructListBase::size; MjStructList Slice(pybind11::slice slice) { return MjStructList(*this, slice); } #define X(type, var) pybind11::array_t var X(int, lastinfo); X(int, number); #undef X protected: MjStructList(MjStructList& other, pybind11::slice slice); }; using MjWarningStatList = MjStructList; template <> struct py_array_or_tuple { using type = MjWarningStatList; }; template <> struct is_mj_struct_list { static constexpr bool value = true; }; // ==================== MJTIMERSTAT ============================================ template <> class MjWrapper : public WrapperBase { public: MjWrapper(); MjWrapper(const MjWrapper&); MjWrapper(MjWrapper&&) = default; MjWrapper(raw::MjTimerStat* ptr, pybind11::handle owner); ~MjWrapper() = default; }; using MjTimerStatWrapper = MjWrapper; template <> struct enable_if_mj_struct { using type = void; }; template <> class MjStructList : public StructListBase { public: MjStructList(raw::MjTimerStat* ptr, int num, pybind11::handle owner); MjStructList(MjStructList&&) = default; ~MjStructList() override = default; using StructListBase::operator[]; using StructListBase::size; MjStructList Slice(pybind11::slice slice) { return MjStructList(*this, slice); } #define X(type, var) pybind11::array_t var X(mjtNum, duration); X(int, number); #undef X protected: MjStructList(MjStructList& other, pybind11::slice slice); }; using MjTimerStatList = MjStructList; template <> struct py_array_or_tuple { using type = MjTimerStatList; }; template <> struct is_mj_struct_list { static constexpr bool value = true; }; // ==================== MJSOLVERSTAT =========================================== template <> class MjWrapper : public WrapperBase { public: MjWrapper(); MjWrapper(const MjWrapper&); MjWrapper(MjWrapper&&) = default; MjWrapper(raw::MjSolverStat* ptr, pybind11::handle owner); ~MjWrapper() = default; }; using MjSolverStatWrapper = MjWrapper; template <> struct enable_if_mj_struct { using type = void; }; template <> class MjStructList : public StructListBase { public: MjStructList(raw::MjSolverStat* ptr, int num, pybind11::handle owner); MjStructList(MjStructList&&) = default; ~MjStructList() override = default; using StructListBase::operator[]; using StructListBase::size; MjStructList Slice(pybind11::slice slice) { return MjStructList(*this, slice); } #define X(type, var) pybind11::array_t var X(mjtNum, improvement); X(mjtNum, gradient); X(mjtNum, lineslope); X(int, nactive); X(int, nchange); X(int, neval); X(int, nupdate); #undef X protected: MjStructList(MjStructList& other, pybind11::slice slice); }; using MjSolverStatList = MjStructList; template <> struct py_array_or_tuple { using type = MjSolverStatList; }; template <> struct is_mj_struct_list { static constexpr bool value = true; }; // ==================== MJMODEL ================================================ template <> class MjWrapper : public WrapperBase { public: MjWrapper(const MjWrapper&); MjWrapper(MjWrapper&&); ~MjWrapper(); MjModelIndexer& indexer() { return indexer_; } void Serialize(std::ostream& output) const; static MjWrapper Deserialize(std::istream& input); static MjWrapper LoadXMLFile( const std::string& filename, const std::optional< std::unordered_map>& assets); static MjWrapper LoadBinaryFile( const std::string& filename, const std::optional< std::unordered_map>& assets); static MjWrapper LoadXML( const std::string& xml, const std::optional< std::unordered_map>& assets); static constexpr char kFromRawPointer[] = "__MUJOCO_STRUCTS_MJMODELWRAPPER_LOOKUP"; static MjWrapper* FromRawPointer(raw::MjModel* m) noexcept; MjOptionWrapper opt; MjVisualWrapper vis; MjStatisticWrapper stat; #define X(dtype, var, dim0, dim1) py_array_or_tuple_t var; MJMODEL_POINTERS #undef X // TODO(nimrod): Exclude text_data and names from the MJMODEL_POINTERS macro. pybind11::bytes text_data_bytes; pybind11::bytes names_bytes; protected: explicit MjWrapper(raw::MjModel* ptr); MjModelIndexer indexer_; }; using MjModelWrapper = MjWrapper; template <> struct enable_if_mj_struct { using type = void; }; // ==================== MJCONTACT ============================================== template <> class MjWrapper : public WrapperBase { public: MjWrapper(); MjWrapper(const MjWrapper&); MjWrapper(MjWrapper&&) = default; MjWrapper(raw::MjContact* ptr, pybind11::handle owner); ~MjWrapper() = default; #define X(var) py_array_or_tuple_t var X(pos); X(frame); X(friction); X(solref); X(solimp); X(H); #undef X }; using MjContactWrapper = MjWrapper; template <> struct enable_if_mj_struct { using type = void; }; template <> class MjStructList : public StructListBase { public: MjStructList(raw::MjContact* ptr, int nconmax, int* ncon, pybind11::handle owner); MjStructList(MjStructList&&) = default; ~MjStructList() override = default; using StructListBase::operator[]; int size() const override { if (ncon_) { return *ncon_; } else { return StructListBase::size(); } } MjStructList Slice(pybind11::slice slice) { return MjStructList(*this, slice); } protected: MjStructList(MjStructList& other, pybind11::slice slice); int* ncon_ = nullptr; }; using MjContactList = MjStructList; template <> struct py_array_or_tuple { using type = MjContactList; }; template <> struct is_mj_struct_list { static constexpr bool value = true; }; // ==================== MJDATA ================================================= template <> class MjWrapper: public WrapperBase { public: explicit MjWrapper(const MjModelWrapper& model); MjWrapper(const MjWrapper& other); MjWrapper(MjWrapper&&); ~MjWrapper(); const MjDataMetadata& metadata() const { return metadata_; } MjDataIndexer& indexer() { return indexer_; } void Serialize(std::ostream& output) const; static MjWrapper Deserialize(std::istream& input); static constexpr char kFromRawPointer[] = "__MUJOCO_STRUCTS_MJDATAWRAPPER_LOOKUP"; static MjWrapper* FromRawPointer(raw::MjData* m) noexcept; #define X(dtype, var, dim0, dim1) py_array_or_tuple_t var; MJDATA_POINTERS #undef X py_array_or_tuple_t contact; py_array_or_tuple_t warning; py_array_or_tuple_t timer; py_array_or_tuple_t solver; py_array_or_tuple_t solver_fwdinv; py_array_or_tuple_t energy; protected: // Internal constructor which takes ownership of given mjData pointer. // Used for deserialization. explicit MjWrapper(MjDataMetadata&& metadata, raw::MjData* d); raw::MjData* Copy() const; MjDataMetadata metadata_; MjDataIndexer indexer_; }; using MjDataWrapper = MjWrapper; template <> struct enable_if_mj_struct { using type = void; }; // ==================== MJVPERTURB ============================================= template <> class MjWrapper : public WrapperBase { public: MjWrapper(); MjWrapper(const MjWrapper&); MjWrapper(MjWrapper&&) = default; ~MjWrapper() = default; #define X(var) py_array_or_tuple_t var X(refpos); X(refquat); X(localpos); #undef X }; using MjvPerturbWrapper = MjWrapper; template <> struct enable_if_mj_struct { using type = void; }; // ==================== MJVCAMERA ============================================== template <> class MjWrapper : public WrapperBase { public: MjWrapper(); MjWrapper(const MjWrapper&); MjWrapper(MjWrapper&&) = default; ~MjWrapper() = default; #define X(var) py_array_or_tuple_t var X(lookat); #undef X }; using MjvCameraWrapper = MjWrapper; template <> struct enable_if_mj_struct { using type = void; }; // ==================== MJVGLCAMERA ============================================ template <> class MjWrapper : public WrapperBase { public: MjWrapper(); MjWrapper(const MjWrapper&); MjWrapper(MjWrapper&&) = default; MjWrapper(raw::MjvGLCamera* ptr, pybind11::handle owner); explicit MjWrapper(raw::MjvGLCamera&& other); ~MjWrapper() = default; #define X(var) py_array_or_tuple_t var X(pos); X(forward); X(up); #undef X }; using MjvGLCameraWrapper = MjWrapper; template <> struct enable_if_mj_struct { using type = void; }; // ==================== MJVGEOM ================================================ template <> class MjWrapper : public WrapperBase { public: MjWrapper(); MjWrapper(const MjWrapper&); MjWrapper(MjWrapper&&) = default; MjWrapper(raw::MjvGeom* ptr, pybind11::handle owner); ~MjWrapper() = default; #define X(var) py_array_or_tuple_t var X(texrepeat); X(size); X(pos); X(mat); X(rgba); #undef X }; using MjvGeomWrapper = MjWrapper; template <> struct enable_if_mj_struct { using type = void; }; // ==================== MJVLIGHT =============================================== template <> class MjWrapper : public WrapperBase { public: MjWrapper(); MjWrapper(const MjWrapper&); MjWrapper(MjWrapper&&) = default; MjWrapper(raw::MjvLight* ptr, pybind11::handle owner); ~MjWrapper() = default; #define X(var) py_array_or_tuple_t var X(pos); X(dir); X(attenuation); X(ambient); X(diffuse); X(specular); #undef X }; using MjvLightWrapper = MjWrapper; template <> struct enable_if_mj_struct { using type = void; }; // ==================== MJVOPTION ============================================== template <> class MjWrapper : public WrapperBase { public: MjWrapper(); MjWrapper(const MjWrapper&); MjWrapper(MjWrapper&&) = default; ~MjWrapper() = default; #define X(var) py_array_or_tuple_t var X(geomgroup); X(sitegroup); X(jointgroup); X(tendongroup); X(actuatorgroup); X(skingroup); X(flags); #undef X }; using MjvOptionWrapper = MjWrapper; template <> struct enable_if_mj_struct { using type = void; }; // ==================== MJVSCENE =============================================== template <> class MjWrapper : public WrapperBase { public: MjWrapper(); MjWrapper(const MjWrapper&); MjWrapper(MjWrapper&&) = default; MjWrapper(const MjModelWrapper& model, int maxgeom); ~MjWrapper() = default; int nskinvert; #define X(dtype, var) py_array_or_tuple_t var X(mjvGeom, geoms); X(int, geomorder); X(int, skinfacenum); X(int, skinvertadr); X(int, skinvertnum); X(float, skinvert); X(float, skinnormal); #undef X #define X(dtype, var) py_array_or_tuple_t var X(raw::MjvLight, lights); X(raw::MjvGLCamera, camera); X(float, translate); X(float, rotate); X(mjtByte, flags); X(float, framergb); #undef X }; using MjvSceneWrapper = MjWrapper; template <> struct enable_if_mj_struct { using type = void; }; // ==================== MJVFIGURE ============================================== template <> class MjWrapper : public WrapperBase { public: MjWrapper(); MjWrapper(const MjWrapper&); MjWrapper(MjWrapper&&) = default; ~MjWrapper() = default; #define X(dtype, var) py_array_or_tuple_t var X(int, flg_ticklabel); X(int, gridsize); X(float, gridrgb); X(float, figurergba); X(float, panergba); X(float, legendrgba); X(float, textrgb); X(float, linergb); X(float, range); X(int, highlight); X(int, linepnt); X(float, linedata); X(int, xaxispixel); X(int, yaxispixel); X(float, xaxisdata); X(float, yaxisdata); #undef X pybind11::array linename; }; using MjvFigureWrapper = MjWrapper; template <> struct enable_if_mj_struct { using type = void; }; #ifdef MEMORY_SANITIZER template class ScopedMsanDisabler { public: ScopedMsanDisabler(T* mj) : mj_(mj), shadow_(std::malloc(mj_->nbuffer)) { __msan_copy_shadow(shadow_, mj_->buffer, mj_->nbuffer); __msan_unpoison(mj_->buffer, mj_->nbuffer); } ~ScopedMsanDisabler() { __msan_copy_shadow(mj_->buffer, shadow_, mj_->nbuffer); std::free(shadow_); } private: T* mj_; void* shadow_; }; #endif } // namespace _impl template using MjWrapper = typename std::conditional_t< std::is_const_v, const _impl::MjWrapper>, _impl::MjWrapper>; template using enable_if_mj_struct_t = typename _impl::enable_if_mj_struct>::type; using _impl::MjOptionWrapper; using _impl::MjVisualHeadlightWrapper; using _impl::MjVisualRgbaWrapper; using _impl::MjVisualWrapper; using _impl::MjStatisticWrapper; using _impl::MjWarningStatWrapper; using _impl::MjTimerStatWrapper; using _impl::MjSolverStatWrapper; using _impl::MjModelWrapper; using _impl::MjDataWrapper; using _impl::MjContactWrapper; using _impl::MjvPerturbWrapper; using _impl::MjvCameraWrapper; using _impl::MjvGLCameraWrapper; using _impl::MjvGeomWrapper; using _impl::MjvLightWrapper; using _impl::MjvOptionWrapper; using _impl::MjvSceneWrapper; using _impl::MjvFigureWrapper; template using MjStructList = typename std::conditional_t< std::is_const_v, const _impl::MjStructList>, _impl::MjStructList>; template static constexpr bool is_mj_struct_list_v = _impl::is_mj_struct_list>::value; using _impl::MjContactList; using _impl::MjWarningStatList; using _impl::MjTimerStatList; using _impl::MjSolverStatList; // ==================== HELPER FUNCTIONS FOR BINDING ARRAYS ==================== // Python array initialization. // If T is an arithmetic (i.e. numeric) type, returns a NumPy array that wraps // around a preexisting data buffer. template std::enable_if_t, pybind11::array_t> static InitPyArray(Shape&& shape, T* buf, pybind11::handle owner) { int size = 1; for (const auto& i : shape) { size *= i; } if (shape.empty() || size == 0) { return pybind11::array_t(shape); } else { return pybind11::array_t(shape, buf, owner); } } // Same as above, but where we can determine array dimensions through the // C array type directly. template std::enable_if_t<(N > 0) && std::is_arithmetic_v, pybind11::array_t> static InitPyArray(T (&buf)[N], pybind11::handle owner) { return pybind11::array_t({N}, &buf[0], owner); } template std::enable_if_t<(N1*N2 > 0) && std::is_arithmetic_v, pybind11::array_t> static InitPyArray(T (&buf)[N1][N2], pybind11::handle owner) { return pybind11::array_t( {N1, N2}, &buf[0][0], owner); } template std::enable_if_t, MjStructList> static InitPyArray(Shape&& shape, T* buf, pybind11::handle owner) { return MjStructList(buf, shape[0], owner); } // For arrays of non-arithmetic type, we create tuple of tuples of MjWrapper. template std::enable_if_t && !is_mj_struct_list_v, pybind11::tuple> static InitPyArray(Shape&& shape, T* buf, pybind11::handle owner) { int size = 1; for (const auto& i : shape) { size *= i; } if (shape.empty() || !size) { return pybind11::tuple(); } pybind11::list out; const auto n = shape[0]; if (shape.size() == 1) { for (int i = 0; i < n; ++i) { out.append(MjWrapper(&buf[i], owner)); } } else { auto block_shape = absl::MakeConstSpan(shape).subspan(1); auto block_size = std::accumulate( block_shape.begin(), block_shape.end(), 1, std::multiplies()); for (int i = 0; i < n; ++i) { out.append(InitPyArray(block_shape, &buf[i * block_size], owner)); } } return std::move(out); } // Same as above, but where we can determine array dimensions through the // C array type directly. template std::enable_if_t<(N > 0) && !std::is_arithmetic_v && !std::is_array_v && !is_mj_struct_list_v, pybind11::tuple> static InitPyArray(T (&buf)[N], pybind11::handle owner) { return InitPyArray(std::array{N}, buf, owner); } template std::enable_if_t<(N1*N2 > 0) && !std::is_arithmetic_v && !std::is_array_v && !is_mj_struct_list_v, pybind11::tuple> static InitPyArray(T (&buf)[N1][N2], pybind11::handle owner) { return InitPyArray(std::array{N1, N2}, buf, owner); } template std::enable_if_t, MjStructList> static InitPyArray(T (&buf)[N], pybind11::handle owner) { return MjStructList(buf, N, owner); } // Helpers for defining array/tuple properties in pybind11 classes. // // Defines a NumPy array property of a Python class that supports assignments. // Specifically, we implement the setter such that `obj.arr = val` is the same // as `obj.arr[:] = val`. // // Use `DefinePyArray(c, "somearray", &MjStructHolder::somearray)` // as a drop-in replacement for // `c.def_readonly("somearray", &MjStructHolder::somearray)`. template static void DefinePyArray(pybind11::class_ c, const char* name, pybind11::array_t C::* arr) { namespace py = pybind11; c.def_property( name, [arr](const C& wrapper) { return wrapper.*arr; }, [arr](const C& wrapper, py::handle rhs) -> void { (wrapper.*arr)[py::slice(py::none(), py::none(), py::none())] = rhs; } ); } // For array of non-arithmetic type, we bind to tuples rather than NumPy array. // These can't be assigned to directly so we just use def_readonly. template static void DefinePyArray(pybind11::class_ c, const char* name, T C::* arr) { c.def_property_readonly( name, [arr](const C& wrapper) -> auto& { return wrapper.*arr; }); } template static void DefinePyStr(pybind11::class_ c, const char* name, char (Raw::* arr)[N]) { c.def_property( name, [arr](const C& c) { return pybind11::str(c.get()->*arr); }, [name = std::string(name), arr](C& c, std::string_view rhs) { constexpr int kMaxLen = sizeof(c.get()->*arr); const int actual_len = rhs.size(); if (actual_len >= kMaxLen) { std::ostringstream msg; msg << "len(" << name << ") cannot exceed " << kMaxLen - 1 << ": got length " << actual_len; throw pybind11::value_error(msg.str()); } rhs.copy(c.get()->*arr, actual_len); (c.get()->*arr)[actual_len] = '\0'; }); } // An equals operator that works for comparing numpy arrays too. // Unlike other objects, the equality operator for numpy arrays returns a // numpy array. inline bool FieldsEqual(pybind11::handle lhs, pybind11::handle rhs, pybind11::handle array_equal) { // np.array_equal handles non-arrays. return PyObject_IsTrue(array_equal(lhs, rhs).ptr()); } // Returns an iterable object for iterating over attributes of T. template pybind11::object Dir() { pybind11::object type = pybind11::type::of(); auto dir = pybind11::reinterpret_steal(PyObject_Dir(type.ptr())); if (PyErr_Occurred()) { throw pybind11::error_already_set(); } return dir; } // Returns true if all public fields in lhs and rhs are equal. template bool StructsEqual(pybind11::object lhs, pybind11::object rhs) { // Equivalent to the following python code: // if type(lhs) != type(rhs): // return False // for field in dir(lhs): // if field.startswith("_"): // continue // # equal() handles equality of numpy arrays // if not equal(getattr(lhs, field, None), getattr(rhs, field, None)): // return False // // return True auto np = pybind11::module::import("numpy"); auto array_equal = np.attr("array_equal"); const pybind11::handle lhs_t = pybind11::type::handle_of(lhs); const pybind11::handle rhs_t = pybind11::type::handle_of(rhs); if (!lhs_t.is(rhs_t)) { return false; } for (pybind11::handle f : Dir()) { auto name = f.cast(); if (name.empty() || name[0] == '_') { continue; } pybind11::object l = pybind11::getattr(lhs, f, pybind11::none()); pybind11::object r = pybind11::getattr(rhs, f, pybind11::none()); if (!FieldsEqual(l, r, array_equal)) { return false; } } return true; } // Returns a string representation of a struct like object. template std::string StructRepr(pybind11::object self) { std::ostringstream result; result << "<" << self.attr("__class__").attr("__name__").cast(); for (pybind11::handle f : Dir()) { auto name = f.cast(); if (name.empty() || name[0] == '_') { continue; } result << "\n " << name << ": " << self.attr(f).attr("__repr__")().cast(); } result << "\n>"; return result.str(); } template std::string MjModelStructRepr(pybind11::object self) { #ifdef MEMORY_SANITIZER _impl::ScopedMsanDisabler msan_disabler( pybind11::cast(self).ptr()); #endif return StructRepr(self); } template std::string MjDataStructRepr(pybind11::object self) { #ifdef MEMORY_SANITIZER _impl::ScopedMsanDisabler msan_disabler( pybind11::cast(self).ptr()); #endif return StructRepr(self); } template void DefineStructFunctions(pybind11::class_ c) { c.def("__eq__", StructsEqual); c.def("__repr__", StructRepr); } } // namespace mujoco::python #endif // MUJOCO_PYTHON_STRUCTS_H_