2022-03-31 12:50:38 -07:00
|
|
|
from collections import OrderedDict
|
|
|
|
from os import path
|
2021-12-08 22:14:15 +01:00
|
|
|
from typing import Optional
|
2019-06-07 14:43:05 -07:00
|
|
|
|
2016-04-27 08:00:58 -07:00
|
|
|
import numpy as np
|
2022-03-31 12:50:38 -07:00
|
|
|
|
2016-04-27 08:00:58 -07:00
|
|
|
import gym
|
2022-05-24 08:47:51 -04:00
|
|
|
from gym import error, logger, spaces
|
2022-06-08 00:20:56 +02:00
|
|
|
from gym.utils.renderer import Renderer
|
2016-04-27 08:00:58 -07:00
|
|
|
|
2022-05-24 08:47:51 -04:00
|
|
|
DEFAULT_SIZE = 480
|
2018-05-30 01:39:49 -07:00
|
|
|
|
2019-06-07 14:43:05 -07:00
|
|
|
|
|
|
|
def convert_observation_to_space(observation):
|
|
|
|
if isinstance(observation, dict):
|
2021-07-29 15:39:42 -04:00
|
|
|
space = spaces.Dict(
|
|
|
|
OrderedDict(
|
|
|
|
[
|
|
|
|
(key, convert_observation_to_space(value))
|
|
|
|
for key, value in observation.items()
|
|
|
|
]
|
|
|
|
)
|
|
|
|
)
|
2019-06-07 14:43:05 -07:00
|
|
|
elif isinstance(observation, np.ndarray):
|
2021-07-29 02:26:34 +02:00
|
|
|
low = np.full(observation.shape, -float("inf"), dtype=np.float32)
|
|
|
|
high = np.full(observation.shape, float("inf"), dtype=np.float32)
|
2019-06-07 14:43:05 -07:00
|
|
|
space = spaces.Box(low, high, dtype=observation.dtype)
|
|
|
|
else:
|
|
|
|
raise NotImplementedError(type(observation), observation)
|
|
|
|
|
|
|
|
return space
|
|
|
|
|
|
|
|
|
2016-04-27 08:00:58 -07:00
|
|
|
class MujocoEnv(gym.Env):
|
2021-07-29 02:26:34 +02:00
|
|
|
"""Superclass for all MuJoCo environments."""
|
2016-04-30 22:47:51 -07:00
|
|
|
|
2022-06-08 00:20:56 +02:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
model_path,
|
|
|
|
frame_skip,
|
|
|
|
render_mode: Optional[str] = None,
|
|
|
|
mujoco_bindings="mujoco",
|
|
|
|
):
|
2016-04-27 08:00:58 -07:00
|
|
|
if model_path.startswith("/"):
|
|
|
|
fullpath = model_path
|
|
|
|
else:
|
2022-05-24 08:47:51 -04:00
|
|
|
fullpath = path.join(path.dirname(__file__), "assets", model_path)
|
2016-04-30 22:47:51 -07:00
|
|
|
if not path.exists(fullpath):
|
2021-11-14 01:53:32 +01:00
|
|
|
raise OSError(f"File {fullpath} does not exist")
|
2022-05-24 08:47:51 -04:00
|
|
|
|
|
|
|
if mujoco_bindings == "mujoco_py":
|
|
|
|
logger.warn(
|
|
|
|
"This version of the mujoco environments depends "
|
|
|
|
"on the mujoco-py bindings, which are no longer maintained "
|
|
|
|
"and may stop working. Please upgrade to the v4 versions of "
|
|
|
|
"the environments (which depend on the mujoco python bindings instead), unless "
|
|
|
|
"you are trying to precisely replicate previous works)."
|
|
|
|
)
|
|
|
|
try:
|
|
|
|
import mujoco_py
|
|
|
|
|
|
|
|
self._mujoco_bindings = mujoco_py
|
|
|
|
except ImportError as e:
|
|
|
|
raise error.DependencyNotInstalled(
|
|
|
|
"{}. (HINT: you need to install mujoco_py, and also perform the setup instructions here: https://github.com/openai/mujoco-py/.)".format(
|
|
|
|
e
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
self.model = self._mujoco_bindings.load_model_from_path(fullpath)
|
|
|
|
self.sim = self._mujoco_bindings.MjSim(self.model)
|
|
|
|
self.data = self.sim.data
|
|
|
|
|
|
|
|
elif mujoco_bindings == "mujoco":
|
|
|
|
try:
|
|
|
|
import mujoco
|
|
|
|
|
|
|
|
self._mujoco_bindings = mujoco
|
|
|
|
|
|
|
|
except ImportError as e:
|
|
|
|
raise error.DependencyNotInstalled(
|
|
|
|
f"{e}. (HINT: you need to install mujoco)"
|
|
|
|
)
|
|
|
|
self.model = self._mujoco_bindings.MjModel.from_xml_path(fullpath)
|
|
|
|
self.data = self._mujoco_bindings.MjData(self.model)
|
|
|
|
|
|
|
|
self.init_qpos = self.data.qpos.ravel().copy()
|
|
|
|
self.init_qvel = self.data.qvel.ravel().copy()
|
|
|
|
self._viewers = {}
|
|
|
|
|
2017-02-22 17:24:27 -08:00
|
|
|
self.frame_skip = frame_skip
|
2022-05-24 08:47:51 -04:00
|
|
|
|
2016-04-27 08:00:58 -07:00
|
|
|
self.viewer = None
|
|
|
|
|
|
|
|
self.metadata = {
|
2022-06-08 00:20:56 +02:00
|
|
|
"render_modes": [
|
|
|
|
"human",
|
|
|
|
"rgb_array",
|
|
|
|
"depth_array",
|
|
|
|
"single_rgb_array",
|
|
|
|
"single_depth_array",
|
|
|
|
],
|
2022-02-28 15:54:03 -05:00
|
|
|
"render_fps": int(np.round(1.0 / self.dt)),
|
2016-04-27 08:00:58 -07:00
|
|
|
}
|
|
|
|
|
2019-06-07 14:43:05 -07:00
|
|
|
self._set_action_space()
|
|
|
|
|
2022-06-08 00:20:56 +02:00
|
|
|
self.render_mode = render_mode
|
|
|
|
self.renderer = Renderer(self.render_mode, self._render)
|
|
|
|
|
2019-06-07 14:43:05 -07:00
|
|
|
action = self.action_space.sample()
|
|
|
|
observation, _reward, done, _info = self.step(action)
|
2016-04-30 22:47:51 -07:00
|
|
|
assert not done
|
|
|
|
|
2019-06-07 14:43:05 -07:00
|
|
|
self._set_observation_space(observation)
|
|
|
|
|
|
|
|
def _set_action_space(self):
|
2020-02-29 01:11:29 +01:00
|
|
|
bounds = self.model.actuator_ctrlrange.copy().astype(np.float32)
|
2019-06-07 14:43:05 -07:00
|
|
|
low, high = bounds.T
|
2018-09-17 10:27:31 -07:00
|
|
|
self.action_space = spaces.Box(low=low, high=high, dtype=np.float32)
|
2019-06-07 14:43:05 -07:00
|
|
|
return self.action_space
|
2016-04-30 22:47:51 -07:00
|
|
|
|
2019-06-07 14:43:05 -07:00
|
|
|
def _set_observation_space(self, observation):
|
|
|
|
self.observation_space = convert_observation_to_space(observation)
|
|
|
|
return self.observation_space
|
2016-05-30 18:07:59 -07:00
|
|
|
|
2016-04-30 22:47:51 -07:00
|
|
|
# methods to override:
|
|
|
|
# ----------------------------
|
|
|
|
|
|
|
|
def reset_model(self):
|
|
|
|
"""
|
|
|
|
Reset the robot degrees of freedom (qpos and qvel).
|
|
|
|
Implement this in each subclass.
|
|
|
|
"""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def viewer_setup(self):
|
|
|
|
"""
|
2018-09-24 14:53:23 -07:00
|
|
|
This method is called when the viewer is initialized.
|
2022-05-25 14:46:41 +01:00
|
|
|
Optionally implement this method, if you need to tinker with camera position and so forth.
|
2016-04-30 22:47:51 -07:00
|
|
|
"""
|
|
|
|
|
|
|
|
# -----------------------------
|
|
|
|
|
2022-02-06 17:28:27 -06:00
|
|
|
def reset(
|
|
|
|
self,
|
|
|
|
*,
|
|
|
|
seed: Optional[int] = None,
|
|
|
|
return_info: bool = False,
|
|
|
|
options: Optional[dict] = None,
|
|
|
|
):
|
2021-12-08 22:14:15 +01:00
|
|
|
super().reset(seed=seed)
|
2022-05-24 08:47:51 -04:00
|
|
|
|
|
|
|
if self._mujoco_bindings.__name__ == "mujoco_py":
|
|
|
|
self.sim.reset()
|
|
|
|
else:
|
|
|
|
self._mujoco_bindings.mj_resetData(self.model, self.data)
|
|
|
|
|
2016-04-30 22:47:51 -07:00
|
|
|
ob = self.reset_model()
|
2022-06-08 00:20:56 +02:00
|
|
|
self.renderer.reset()
|
|
|
|
self.renderer.render_step()
|
2022-02-06 17:28:27 -06:00
|
|
|
if not return_info:
|
|
|
|
return ob
|
|
|
|
else:
|
|
|
|
return ob, {}
|
2016-04-30 22:47:51 -07:00
|
|
|
|
|
|
|
def set_state(self, qpos, qvel):
|
|
|
|
assert qpos.shape == (self.model.nq,) and qvel.shape == (self.model.nv,)
|
2022-05-24 08:47:51 -04:00
|
|
|
if self._mujoco_bindings.__name__ == "mujoco_py":
|
|
|
|
state = self.sim.get_state()
|
|
|
|
state = self._mujoco_bindings.MjSimState(
|
|
|
|
state.time, qpos, qvel, state.act, state.udd_state
|
|
|
|
)
|
|
|
|
self.sim.set_state(state)
|
|
|
|
self.sim.forward()
|
|
|
|
else:
|
|
|
|
self.data.qpos[:] = np.copy(qpos)
|
|
|
|
self.data.qvel[:] = np.copy(qvel)
|
|
|
|
if self.model.na == 0:
|
|
|
|
self.data.act[:] = None
|
|
|
|
self._mujoco_bindings.mj_forward(self.model, self.data)
|
2016-04-30 22:47:51 -07:00
|
|
|
|
2016-04-27 08:00:58 -07:00
|
|
|
@property
|
|
|
|
def dt(self):
|
|
|
|
return self.model.opt.timestep * self.frame_skip
|
|
|
|
|
|
|
|
def do_simulation(self, ctrl, n_frames):
|
2021-11-18 07:11:40 +08:00
|
|
|
if np.array(ctrl).shape != self.action_space.shape:
|
|
|
|
raise ValueError("Action dimension mismatch")
|
|
|
|
|
2022-05-24 08:47:51 -04:00
|
|
|
if self._mujoco_bindings.__name__ == "mujoco_py":
|
|
|
|
self.sim.data.ctrl[:] = ctrl
|
|
|
|
else:
|
|
|
|
self.data.ctrl[:] = ctrl
|
2016-04-27 08:00:58 -07:00
|
|
|
for _ in range(n_frames):
|
2022-05-24 08:47:51 -04:00
|
|
|
if self._mujoco_bindings.__name__ == "mujoco_py":
|
|
|
|
self.sim.step()
|
|
|
|
else:
|
|
|
|
self._mujoco_bindings.mj_step(self.model, self.data)
|
|
|
|
|
|
|
|
# As of MuJoCo 2.0, force-related quantities like cacc are not computed
|
|
|
|
# unless there's a force sensor in the model.
|
|
|
|
# See https://github.com/openai/gym/issues/1541
|
|
|
|
if self._mujoco_bindings.__name__ != "mujoco_py":
|
|
|
|
self._mujoco_bindings.mj_rnePostConstraint(self.model, self.data)
|
2016-04-27 08:00:58 -07:00
|
|
|
|
2021-07-29 02:26:34 +02:00
|
|
|
def render(
|
|
|
|
self,
|
|
|
|
mode="human",
|
|
|
|
width=DEFAULT_SIZE,
|
|
|
|
height=DEFAULT_SIZE,
|
|
|
|
camera_id=None,
|
|
|
|
camera_name=None,
|
|
|
|
):
|
2022-06-08 00:20:56 +02:00
|
|
|
if self.render_mode is not None:
|
|
|
|
return self.renderer.get_renders()
|
|
|
|
else:
|
|
|
|
return self._render(
|
|
|
|
mode=mode,
|
|
|
|
width=width,
|
|
|
|
height=height,
|
|
|
|
camera_id=camera_id,
|
|
|
|
camera_name=camera_name,
|
|
|
|
)
|
|
|
|
|
|
|
|
def _render(
|
|
|
|
self,
|
|
|
|
mode="human",
|
|
|
|
width=DEFAULT_SIZE,
|
|
|
|
height=DEFAULT_SIZE,
|
|
|
|
camera_id=None,
|
|
|
|
camera_name=None,
|
|
|
|
):
|
|
|
|
assert mode in self.metadata["render_modes"]
|
|
|
|
|
|
|
|
if mode in {
|
|
|
|
"rgb_array",
|
|
|
|
"single_rgb_array",
|
|
|
|
"depth_array",
|
|
|
|
"single_depth_array",
|
|
|
|
}:
|
2019-08-23 15:02:33 -07:00
|
|
|
if camera_id is not None and camera_name is not None:
|
2021-07-29 15:39:42 -04:00
|
|
|
raise ValueError(
|
|
|
|
"Both `camera_id` and `camera_name` cannot be"
|
|
|
|
" specified at the same time."
|
|
|
|
)
|
2019-08-23 15:02:33 -07:00
|
|
|
|
|
|
|
no_camera_specified = camera_name is None and camera_id is None
|
|
|
|
if no_camera_specified:
|
2021-07-29 02:26:34 +02:00
|
|
|
camera_name = "track"
|
2019-08-23 15:02:33 -07:00
|
|
|
|
2022-05-24 08:47:51 -04:00
|
|
|
if camera_id is None:
|
|
|
|
if self._mujoco_bindings.__name__ == "mujoco_py":
|
|
|
|
if camera_name in self.model._camera_name2id:
|
|
|
|
camera_id = self.model.camera_name2id(camera_name)
|
|
|
|
else:
|
|
|
|
camera_id = self._mujoco_bindings.mj_name2id(
|
|
|
|
self.model,
|
|
|
|
self._mujoco_bindings.mjtObj.mjOBJ_CAMERA,
|
|
|
|
camera_name,
|
|
|
|
)
|
2019-08-23 15:02:33 -07:00
|
|
|
|
2022-05-24 08:47:51 -04:00
|
|
|
self._get_viewer(mode).render(width, height, camera_id=camera_id)
|
2020-06-20 06:42:26 +09:00
|
|
|
|
2022-06-08 00:20:56 +02:00
|
|
|
if mode in {"rgb_array", "single_rgb_array"}:
|
2018-05-30 01:39:49 -07:00
|
|
|
data = self._get_viewer(mode).read_pixels(width, height, depth=False)
|
2018-02-26 01:48:44 -08:00
|
|
|
# original image is upside-down, so flip it
|
|
|
|
return data[::-1, :, :]
|
2022-06-08 00:20:56 +02:00
|
|
|
elif mode in {"depth_array", "single_depth_array"}:
|
2018-09-21 01:51:58 +01:00
|
|
|
self._get_viewer(mode).render(width, height)
|
|
|
|
# Extract depth part of the read_pixels() tuple
|
|
|
|
data = self._get_viewer(mode).read_pixels(width, height, depth=True)[1]
|
|
|
|
# original image is upside-down, so flip it
|
|
|
|
return data[::-1, :]
|
2021-07-29 02:26:34 +02:00
|
|
|
elif mode == "human":
|
2018-05-30 01:39:49 -07:00
|
|
|
self._get_viewer(mode).render()
|
2016-04-27 08:00:58 -07:00
|
|
|
|
Cleanup, removal of unmaintained code (#836)
* add dtype to Box
* remove board_game, debugging, safety, parameter_tuning environments
* massive set of breaking changes
- remove python logging module
- _step, _reset, _seed, _close => non underscored method
- remove benchmark and scoring folder
* Improve render("human"), now resizable, closable window.
* get rid of default step and reset in wrappers, so it doesn’t silently fail for people with underscore methods
* CubeCrash unit test environment
* followup fixes
* MemorizeDigits unit test envrionment
* refactored spaces a bit
fixed indentation
disabled test_env_semantics
* fix unit tests
* fixes
* CubeCrash, MemorizeDigits tested
* gym backwards compatibility patch
* gym backwards compatibility, followup fixes
* changelist, add spaces to main namespaces
* undo_logger_setup for backwards compat
* remove configuration.py
2018-01-25 18:20:14 -08:00
|
|
|
def close(self):
|
|
|
|
if self.viewer is not None:
|
2022-06-04 11:28:31 -07:00
|
|
|
if self._mujoco_bindings.__name__ == "mujoco":
|
|
|
|
self.viewer.close()
|
Cleanup, removal of unmaintained code (#836)
* add dtype to Box
* remove board_game, debugging, safety, parameter_tuning environments
* massive set of breaking changes
- remove python logging module
- _step, _reset, _seed, _close => non underscored method
- remove benchmark and scoring folder
* Improve render("human"), now resizable, closable window.
* get rid of default step and reset in wrappers, so it doesn’t silently fail for people with underscore methods
* CubeCrash unit test environment
* followup fixes
* MemorizeDigits unit test envrionment
* refactored spaces a bit
fixed indentation
disabled test_env_semantics
* fix unit tests
* fixes
* CubeCrash, MemorizeDigits tested
* gym backwards compatibility patch
* gym backwards compatibility, followup fixes
* changelist, add spaces to main namespaces
* undo_logger_setup for backwards compat
* remove configuration.py
2018-01-25 18:20:14 -08:00
|
|
|
self.viewer = None
|
2018-05-30 01:39:49 -07:00
|
|
|
self._viewers = {}
|
Cleanup, removal of unmaintained code (#836)
* add dtype to Box
* remove board_game, debugging, safety, parameter_tuning environments
* massive set of breaking changes
- remove python logging module
- _step, _reset, _seed, _close => non underscored method
- remove benchmark and scoring folder
* Improve render("human"), now resizable, closable window.
* get rid of default step and reset in wrappers, so it doesn’t silently fail for people with underscore methods
* CubeCrash unit test environment
* followup fixes
* MemorizeDigits unit test envrionment
* refactored spaces a bit
fixed indentation
disabled test_env_semantics
* fix unit tests
* fixes
* CubeCrash, MemorizeDigits tested
* gym backwards compatibility patch
* gym backwards compatibility, followup fixes
* changelist, add spaces to main namespaces
* undo_logger_setup for backwards compat
* remove configuration.py
2018-01-25 18:20:14 -08:00
|
|
|
|
2022-05-24 08:47:51 -04:00
|
|
|
def _get_viewer(self, mode, width=DEFAULT_SIZE, height=DEFAULT_SIZE):
|
2018-05-30 01:39:49 -07:00
|
|
|
self.viewer = self._viewers.get(mode)
|
2016-04-27 08:00:58 -07:00
|
|
|
if self.viewer is None:
|
2021-07-29 02:26:34 +02:00
|
|
|
if mode == "human":
|
2022-05-24 08:47:51 -04:00
|
|
|
if self._mujoco_bindings.__name__ == "mujoco_py":
|
|
|
|
self.viewer = self._mujoco_bindings.MjViewer(self.sim)
|
|
|
|
else:
|
|
|
|
from gym.envs.mujoco.mujoco_rendering import Viewer
|
|
|
|
|
|
|
|
self.viewer = Viewer(self.model, self.data)
|
2022-06-08 00:20:56 +02:00
|
|
|
elif mode in {
|
|
|
|
"rgb_array",
|
|
|
|
"depth_array",
|
|
|
|
"single_rgb_array",
|
|
|
|
"single_depth_array",
|
|
|
|
}:
|
2022-05-24 08:47:51 -04:00
|
|
|
if self._mujoco_bindings.__name__ == "mujoco_py":
|
|
|
|
self.viewer = self._mujoco_bindings.MjRenderContextOffscreen(
|
|
|
|
self.sim, -1
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
from gym.envs.mujoco.mujoco_rendering import RenderContextOffscreen
|
|
|
|
|
|
|
|
self.viewer = RenderContextOffscreen(
|
|
|
|
width, height, self.model, self.data
|
|
|
|
)
|
2019-06-07 14:43:05 -07:00
|
|
|
|
2016-04-27 08:00:58 -07:00
|
|
|
self.viewer_setup()
|
2018-05-30 01:39:49 -07:00
|
|
|
self._viewers[mode] = self.viewer
|
2016-04-27 08:00:58 -07:00
|
|
|
return self.viewer
|
|
|
|
|
|
|
|
def get_body_com(self, body_name):
|
2022-05-24 08:47:51 -04:00
|
|
|
if self._mujoco_bindings.__name__ == "mujoco_py":
|
|
|
|
return self.data.get_body_xpos(body_name)
|
|
|
|
else:
|
|
|
|
return self.data.body(body_name).xpos
|
2016-04-27 08:00:58 -07:00
|
|
|
|
2016-04-30 22:47:51 -07:00
|
|
|
def state_vector(self):
|
2022-05-24 08:47:51 -04:00
|
|
|
return np.concatenate([self.data.qpos.flat, self.data.qvel.flat])
|