Modify Python simulate so that the Reload button works.

Also allow a `Callable[[], [MjModel, MjData]]` to be provided as an argument to `launch` instead of a fixed MjModel/MjData instance. This allows users to customise the behaviour of the Reload button.

Context: https://github.com/deepmind/mujoco/discussions/576#discussioncomment-4161429
PiperOrigin-RevId: 492296268
Change-Id: Ic4b797d2ea8a6967f7e3e7316f2184ea94a86d8b
This commit is contained in:
Saran Tunyasuvunakool
2022-12-01 14:11:49 -08:00
committed by Copybara-Service
parent 89185b4af2
commit 9fcf3b0c2e
2 changed files with 86 additions and 49 deletions
+3 -4
View File
@@ -17,9 +17,7 @@
#include <string> #include <string>
#include <simulate.h> #include <simulate.h>
#include "raw.h"
#include "structs.h" #include "structs.h"
#include <pybind11/detail/common.h>
#include <pybind11/pybind11.h> #include <pybind11/pybind11.h>
namespace mujoco::python { namespace mujoco::python {
@@ -47,8 +45,9 @@ PYBIND11_MODULE(_simulate, pymodule) {
py::call_guard<py::gil_scoped_release>()) py::call_guard<py::gil_scoped_release>())
.def( .def(
"load", "load",
[](mujoco::Simulate& simulate, MjModelWrapper& m, MjDataWrapper& d) { [](mujoco::Simulate& simulate, const std::string& path,
simulate.load("", m.get(), d.get()); MjModelWrapper& m, MjDataWrapper& d) {
simulate.load(path.c_str(), m.get(), d.get());
}, },
py::call_guard<py::gil_scoped_release>()) py::call_guard<py::gil_scoped_release>())
.def("applyposepertubations", &mujoco::Simulate::applyposepertubations, .def("applyposepertubations", &mujoco::Simulate::applyposepertubations,
+83 -45
View File
@@ -21,7 +21,7 @@ import math
import os import os
import threading import threading
import time import time
from typing import Callable, Optional from typing import Callable, Optional, Union
import glfw import glfw
import mujoco import mujoco
@@ -48,35 +48,32 @@ MAX_SYNC_MISALIGN = 0.1
SIM_REFRESH_FRACTION = 0.7 SIM_REFRESH_FRACTION = 0.7
CallbackType = Callable[[mujoco.MjModel, mujoco.MjData], None] CallbackType = Callable[[mujoco.MjModel, mujoco.MjData], None]
LoaderType = Callable[[], tuple[mujoco.MjModel, mujoco.MjData]]
# Loader function that also returns a file path for the GUI to display.
_LoaderWithPathType = Callable[[], tuple[mujoco.MjModel, mujoco.MjData, str]]
_InternalLoaderType = Union[LoaderType, _LoaderWithPathType]
Simulate = _simulate.Simulate Simulate = _simulate.Simulate
def _reload_from_file(simulate: Simulate, filename: str): def _file_loader(path: str) -> _LoaderWithPathType:
"""Loads an MJCF model into the Simulate GUI.""" """Loads an MJCF model from file path."""
try:
m = mujoco.MjModel.from_xml_path(filename)
except mujoco.FatalError as e:
m = None
simulate.load_error = str(e)
if m is not None: def load(path=path) -> tuple[mujoco.MjModel, mujoco.MjData, str]:
m = mujoco.MjModel.from_xml_path(path)
d = mujoco.MjData(m) d = mujoco.MjData(m)
simulate.load(m, d) return m, d, path
mujoco.mj_forward(m, d)
else:
d = None
return m, d return load
def _physics_loop(simulate: Simulate, def _physics_loop(simulate: Simulate, loader: Optional[_InternalLoaderType]):
m: Optional[mujoco.MjModel],
d: Optional[mujoco.MjData]):
"""Physics loop for the Simulate GUI, to be run in a separate thread.""" """Physics loop for the Simulate GUI, to be run in a separate thread."""
ctrlnoise = None m: mujoco.MjModel = None
if m is not None: d: mujoco.MjData = None
ctrlnoise = np.zeros((m.nu,)) ctrlnoise = np.array([])
reload = True
# CPU-sim synchronization point. # CPU-sim synchronization point.
synccpu = 0.0 synccpu = 0.0
@@ -86,20 +83,40 @@ def _physics_loop(simulate: Simulate,
while not simulate.exitrequest: while not simulate.exitrequest:
if simulate.droploadrequest: if simulate.droploadrequest:
simulate.droploadrequest = 0 simulate.droploadrequest = 0
new_m, new_d = _reload_from_file(simulate, simulate.dropfilename) loader = _file_loader(simulate.dropfilename)
if new_m is not None: reload = True
m = new_m
d = new_d
ctrlnoise = np.zeros((m.nu,))
if simulate.uiloadrequest: if simulate.uiloadrequest:
simulate.uiloadrequest_decrement() simulate.uiloadrequest_decrement()
new_m, new_d = _reload_from_file(simulate, simulate.dropfilename) reload = True
if new_m is not None:
m = new_m if reload and loader is not None:
d = new_d try:
load_tuple = loader()
except Exception as e: # pylint: disable=broad-except
simulate.load_error = str(e)
else:
# Do not assign to m and d until simulate.load is done!
# This is because simulate.load needs to clean up mjvScene and
# mjrContext. This cleanup logic requires access to the old m and d.
new_m, new_d = load_tuple[:2]
# If the loader does not raise an exception then we assume that it
# successfully created mjModel and mjData. This is specified in the type
# annotation, but we perform a runtime assertion here as well to prevent
# possible segmentation faults.
assert new_m is not None and new_d is not None
path = load_tuple[2] if len(load_tuple) == 3 else ''
simulate.load(path, new_m, new_d)
# We can now allow the old m and d to be deleted.
m, d = new_m, new_d
mujoco.mj_forward(m, d)
ctrlnoise = np.zeros((m.nu,)) ctrlnoise = np.zeros((m.nu,))
reload = False
# Sleep for 1 ms or yield, to let main thread run. # Sleep for 1 ms or yield, to let main thread run.
if simulate.run != 0 and simulate.busywait != 0: if simulate.run != 0 and simulate.busywait != 0:
time.sleep(0) time.sleep(0)
@@ -186,15 +203,28 @@ def _physics_loop(simulate: Simulate,
mujoco.mj_forward(m, d) mujoco.mj_forward(m, d)
def launch(model: Optional[mujoco.MjModel] = None, def _launch_internal(model: Optional[mujoco.MjModel] = None,
data: Optional[mujoco.MjData] = None, data: Optional[mujoco.MjData] = None,
*, *,
run_physics_thread: bool = True) -> None: run_physics_thread: bool = True,
"""Launches the Simulate GUI.""" loader: Optional[_InternalLoaderType] = None) -> None:
"""Internal API, so that the public API has more readable type annotations."""
if model is None and data is not None: if model is None and data is not None:
raise ValueError('mjData is specified but mjModel is not') raise ValueError('mjData is specified but mjModel is not')
elif model is not None and data is None: elif callable(model) and data is not None:
data = mujoco.MjData(model) raise ValueError(
'mjData should not be specified when an mjModel loader is used')
elif loader is not None and model is not None:
raise ValueError('model and loader are both specified')
if loader is None and model is not None:
def _loader(m=model, d=data) -> tuple[mujoco.MjModel, mujoco.MjData]:
if d is None:
d = mujoco.MjData(m)
return m, d
loader = _loader
# The simulate object encapsulates the UI. # The simulate object encapsulates the UI.
simulate = Simulate() simulate = Simulate()
@@ -207,21 +237,30 @@ def launch(model: Optional[mujoco.MjModel] = None,
if run_physics_thread: if run_physics_thread:
physics_thread = threading.Thread( physics_thread = threading.Thread(
target=_physics_loop, args=(simulate, model, data)) target=_physics_loop, args=(simulate, loader))
physics_thread.start() physics_thread.start()
# Load the initial model, if one is given.
if model is not None:
t = threading.Thread(target=simulate.load, args=(model, data))
t.start()
del t
simulate.renderloop() simulate.renderloop()
if run_physics_thread: if run_physics_thread:
physics_thread.join() physics_thread.join()
def launch(model: Optional[mujoco.MjModel] = None,
data: Optional[mujoco.MjData] = None,
*,
run_physics_thread: bool = True,
loader: Optional[LoaderType] = None) -> None:
"""Launches the Simulate GUI."""
_launch_internal(
model, data, run_physics_thread=run_physics_thread, loader=loader)
def launch_from_path(path: str) -> None:
"""Launches the Simulate GUI from file path."""
_launch_internal(loader=_file_loader(path))
def launch_repl(model: mujoco.MjModel, data: mujoco.MjData) -> None: def launch_repl(model: mujoco.MjModel, data: mujoco.MjData) -> None:
"""EXPERIMENTAL FEATURE: Launches the Simulate GUI in REPL mode.""" """EXPERIMENTAL FEATURE: Launches the Simulate GUI in REPL mode."""
try: try:
@@ -253,8 +292,7 @@ if __name__ == '__main__':
def main(argv) -> None: def main(argv) -> None:
del argv del argv
if _MJCF_PATH.value is not None: if _MJCF_PATH.value is not None:
model = mujoco.MjModel.from_xml_path(os.path.expanduser(_MJCF_PATH.value)) launch_from_path(os.path.expanduser(_MJCF_PATH.value))
launch(model)
else: else:
launch() launch()