diff --git a/Metaworld/zarr_path: data/metaworld_disassemble_expert.zarr/data/full_state/6.0 b/Metaworld/zarr_path: data/metaworld_disassemble_expert.zarr/data/full_state/6.0 new file mode 100644 index 0000000000000000000000000000000000000000..90d1daed66206a9c9d142058782c8743329e4c87 Binary files /dev/null and b/Metaworld/zarr_path: data/metaworld_disassemble_expert.zarr/data/full_state/6.0 differ diff --git a/Metaworld/zarr_path: data/metaworld_door-close_expert.zarr/.zgroup b/Metaworld/zarr_path: data/metaworld_door-close_expert.zarr/.zgroup new file mode 100644 index 0000000000000000000000000000000000000000..3b7daf227c1687f28bc23b69f183e27ce9a475c1 --- /dev/null +++ b/Metaworld/zarr_path: data/metaworld_door-close_expert.zarr/.zgroup @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file diff --git a/Metaworld/zarr_path: data/metaworld_door-close_expert.zarr/data/action/11.0 b/Metaworld/zarr_path: data/metaworld_door-close_expert.zarr/data/action/11.0 new file mode 100644 index 0000000000000000000000000000000000000000..8ff27326640da4c1ad1a482a492a8d2f5d26490f Binary files /dev/null and b/Metaworld/zarr_path: data/metaworld_door-close_expert.zarr/data/action/11.0 differ diff --git a/Metaworld/zarr_path: data/metaworld_door-close_expert.zarr/data/action/14.0 b/Metaworld/zarr_path: data/metaworld_door-close_expert.zarr/data/action/14.0 new file mode 100644 index 0000000000000000000000000000000000000000..74160752dbabf9bdb42f25f7f878902efdd7dfad Binary files /dev/null and b/Metaworld/zarr_path: data/metaworld_door-close_expert.zarr/data/action/14.0 differ diff --git a/Metaworld/zarr_path: data/metaworld_door-close_expert.zarr/data/action/16.0 b/Metaworld/zarr_path: data/metaworld_door-close_expert.zarr/data/action/16.0 new file mode 100644 index 0000000000000000000000000000000000000000..19843afb41c28986197bd02d8c326205cf459ec6 Binary files /dev/null and b/Metaworld/zarr_path: data/metaworld_door-close_expert.zarr/data/action/16.0 differ diff --git a/Metaworld/zarr_path: data/metaworld_door-close_expert.zarr/data/action/18.0 b/Metaworld/zarr_path: data/metaworld_door-close_expert.zarr/data/action/18.0 new file mode 100644 index 0000000000000000000000000000000000000000..3ef95a3c5cc34393aa5566128f4c5653f82350c7 Binary files /dev/null and b/Metaworld/zarr_path: data/metaworld_door-close_expert.zarr/data/action/18.0 differ diff --git a/Metaworld/zarr_path: data/metaworld_door-close_expert.zarr/data/depth/.zarray b/Metaworld/zarr_path: data/metaworld_door-close_expert.zarr/data/depth/.zarray new file mode 100644 index 0000000000000000000000000000000000000000..0a63fa64cd39147af12642b1b70c03062384d8ea --- /dev/null +++ b/Metaworld/zarr_path: data/metaworld_door-close_expert.zarr/data/depth/.zarray @@ -0,0 +1,24 @@ +{ + "chunks": [ + 100, + 128, + 128 + ], + "compressor": { + "blocksize": 0, + "clevel": 3, + "cname": "zstd", + "id": "blosc", + "shuffle": 1 + }, + "dtype": "' + """ + ) + +import math +import numpy as np + +RAD2DEG = 57.29577951308232 + + +def get_display(spec): + """Convert a display specification (such as :0) into an actual Display + object. + + Pyglet only supports multiple Displays on Linux. + """ + if spec is None: + return pyglet.canvas.get_display() + # returns already available pyglet_display, + # if there is no pyglet display available then it creates one + elif isinstance(spec, str): + return pyglet.canvas.Display(spec) + else: + raise error.Error( + "Invalid display specification: {}. (Must be a string like :0 or None.)".format( + spec + ) + ) + + +def get_window(width, height, display, **kwargs): + """ + Will create a pyglet window from the display specification provided. + """ + screen = display.get_screens() # available screens + config = screen[0].get_best_config() # selecting the first screen + context = config.create_context(None) # create GL context + + return pyglet.window.Window( + width=width, + height=height, + display=display, + config=config, + context=context, + **kwargs + ) + + +class Viewer(object): + def __init__(self, width, height, display=None): + display = get_display(display) + + self.width = width + self.height = height + self.window = get_window(width=width, height=height, display=display) + self.window.on_close = self.window_closed_by_user + self.isopen = True + self.geoms = [] + self.onetime_geoms = [] + self.transform = Transform() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + def close(self): + if self.isopen and sys.meta_path: + # ^^^ check sys.meta_path to avoid 'ImportError: sys.meta_path is None, Python is likely shutting down' + self.window.close() + self.isopen = False + + def window_closed_by_user(self): + self.isopen = False + + def set_bounds(self, left, right, bottom, top): + assert right > left and top > bottom + scalex = self.width / (right - left) + scaley = self.height / (top - bottom) + self.transform = Transform( + translation=(-left * scalex, -bottom * scaley), scale=(scalex, scaley) + ) + + def add_geom(self, geom): + self.geoms.append(geom) + + def add_onetime(self, geom): + self.onetime_geoms.append(geom) + + def render(self, return_rgb_array=False): + glClearColor(1, 1, 1, 1) + self.window.clear() + self.window.switch_to() + self.window.dispatch_events() + self.transform.enable() + for geom in self.geoms: + geom.render() + for geom in self.onetime_geoms: + geom.render() + self.transform.disable() + arr = None + if return_rgb_array: + buffer = pyglet.image.get_buffer_manager().get_color_buffer() + image_data = buffer.get_image_data() + arr = np.frombuffer(image_data.get_data(), dtype=np.uint8) + # In https://github.com/openai/gym-http-api/issues/2, we + # discovered that someone using Xmonad on Arch was having + # a window of size 598 x 398, though a 600 x 400 window + # was requested. (Guess Xmonad was preserving a pixel for + # the boundary.) So we use the buffer height/width rather + # than the requested one. + arr = arr.reshape(buffer.height, buffer.width, 4) + arr = arr[::-1, :, 0:3] + self.window.flip() + self.onetime_geoms = [] + return arr if return_rgb_array else self.isopen + + # Convenience + def draw_circle(self, radius=10, res=30, filled=True, **attrs): + geom = make_circle(radius=radius, res=res, filled=filled) + _add_attrs(geom, attrs) + self.add_onetime(geom) + return geom + + def draw_polygon(self, v, filled=True, **attrs): + geom = make_polygon(v=v, filled=filled) + _add_attrs(geom, attrs) + self.add_onetime(geom) + return geom + + def draw_polyline(self, v, **attrs): + geom = make_polyline(v=v) + _add_attrs(geom, attrs) + self.add_onetime(geom) + return geom + + def draw_line(self, start, end, **attrs): + geom = Line(start, end) + _add_attrs(geom, attrs) + self.add_onetime(geom) + return geom + + def get_array(self): + self.window.flip() + image_data = ( + pyglet.image.get_buffer_manager().get_color_buffer().get_image_data() + ) + self.window.flip() + arr = np.fromstring(image_data.get_data(), dtype=np.uint8, sep="") + arr = arr.reshape(self.height, self.width, 4) + return arr[::-1, :, 0:3] + + def __del__(self): + self.close() + + +def _add_attrs(geom, attrs): + if "color" in attrs: + geom.set_color(*attrs["color"]) + if "linewidth" in attrs: + geom.set_linewidth(attrs["linewidth"]) + + +class Geom(object): + def __init__(self): + self._color = Color((0, 0, 0, 1.0)) + self.attrs = [self._color] + + def render(self): + for attr in reversed(self.attrs): + attr.enable() + self.render1() + for attr in self.attrs: + attr.disable() + + def render1(self): + raise NotImplementedError + + def add_attr(self, attr): + self.attrs.append(attr) + + def set_color(self, r, g, b): + self._color.vec4 = (r, g, b, 1) + + +class Attr(object): + def enable(self): + raise NotImplementedError + + def disable(self): + pass + + +class Transform(Attr): + def __init__(self, translation=(0.0, 0.0), rotation=0.0, scale=(1, 1)): + self.set_translation(*translation) + self.set_rotation(rotation) + self.set_scale(*scale) + + def enable(self): + glPushMatrix() + glTranslatef( + self.translation[0], self.translation[1], 0 + ) # translate to GL loc ppint + glRotatef(RAD2DEG * self.rotation, 0, 0, 1.0) + glScalef(self.scale[0], self.scale[1], 1) + + def disable(self): + glPopMatrix() + + def set_translation(self, newx, newy): + self.translation = (float(newx), float(newy)) + + def set_rotation(self, new): + self.rotation = float(new) + + def set_scale(self, newx, newy): + self.scale = (float(newx), float(newy)) + + +class Color(Attr): + def __init__(self, vec4): + self.vec4 = vec4 + + def enable(self): + glColor4f(*self.vec4) + + +class LineStyle(Attr): + def __init__(self, style): + self.style = style + + def enable(self): + glEnable(GL_LINE_STIPPLE) + glLineStipple(1, self.style) + + def disable(self): + glDisable(GL_LINE_STIPPLE) + + +class LineWidth(Attr): + def __init__(self, stroke): + self.stroke = stroke + + def enable(self): + glLineWidth(self.stroke) + + +class Point(Geom): + def __init__(self): + Geom.__init__(self) + + def render1(self): + glBegin(GL_POINTS) # draw point + glVertex3f(0.0, 0.0, 0.0) + glEnd() + + +class FilledPolygon(Geom): + def __init__(self, v): + Geom.__init__(self) + self.v = v + + def render1(self): + if len(self.v) == 4: + glBegin(GL_QUADS) + elif len(self.v) > 4: + glBegin(GL_POLYGON) + else: + glBegin(GL_TRIANGLES) + for p in self.v: + glVertex3f(p[0], p[1], 0) # draw each vertex + glEnd() + + +def make_circle(radius=10, res=30, filled=True): + points = [] + for i in range(res): + ang = 2 * math.pi * i / res + points.append((math.cos(ang) * radius, math.sin(ang) * radius)) + if filled: + return FilledPolygon(points) + else: + return PolyLine(points, True) + + +def make_polygon(v, filled=True): + if filled: + return FilledPolygon(v) + else: + return PolyLine(v, True) + + +def make_polyline(v): + return PolyLine(v, False) + + +def make_capsule(length, width): + l, r, t, b = 0, length, width / 2, -width / 2 + box = make_polygon([(l, b), (l, t), (r, t), (r, b)]) + circ0 = make_circle(width / 2) + circ1 = make_circle(width / 2) + circ1.add_attr(Transform(translation=(length, 0))) + geom = Compound([box, circ0, circ1]) + return geom + + +class Compound(Geom): + def __init__(self, gs): + Geom.__init__(self) + self.gs = gs + for g in self.gs: + g.attrs = [a for a in g.attrs if not isinstance(a, Color)] + + def render1(self): + for g in self.gs: + g.render() + + +class PolyLine(Geom): + def __init__(self, v, close): + Geom.__init__(self) + self.v = v + self.close = close + self.linewidth = LineWidth(1) + self.add_attr(self.linewidth) + + def render1(self): + glBegin(GL_LINE_LOOP if self.close else GL_LINE_STRIP) + for p in self.v: + glVertex3f(p[0], p[1], 0) # draw each vertex + glEnd() + + def set_linewidth(self, x): + self.linewidth.stroke = x + + +class Line(Geom): + def __init__(self, start=(0.0, 0.0), end=(0.0, 0.0)): + Geom.__init__(self) + self.start = start + self.end = end + self.linewidth = LineWidth(1) + self.add_attr(self.linewidth) + + def render1(self): + glBegin(GL_LINES) + glVertex2f(*self.start) + glVertex2f(*self.end) + glEnd() + + +class Image(Geom): + def __init__(self, fname, width, height): + Geom.__init__(self) + self.set_color(1.0, 1.0, 1.0) + self.width = width + self.height = height + img = pyglet.image.load(fname) + self.img = img + self.flip = False + + def render1(self): + self.img.blit( + -self.width / 2, -self.height / 2, width=self.width, height=self.height + ) + + +# ================================================================ + + +class SimpleImageViewer(object): + def __init__(self, display=None, maxwidth=500): + self.window = None + self.isopen = False + self.display = get_display(display) + self.maxwidth = maxwidth + + def imshow(self, arr): + if self.window is None: + height, width, _channels = arr.shape + if width > self.maxwidth: + scale = self.maxwidth / width + width = int(scale * width) + height = int(scale * height) + self.window = get_window( + width=width, + height=height, + display=self.display, + vsync=False, + resizable=True, + ) + self.width = width + self.height = height + self.isopen = True + + @self.window.event + def on_resize(width, height): + self.width = width + self.height = height + + @self.window.event + def on_close(): + self.isopen = False + + assert len(arr.shape) == 3, "You passed in an image with the wrong number shape" + image = pyglet.image.ImageData( + arr.shape[1], arr.shape[0], "RGB", arr.tobytes(), pitch=arr.shape[1] * -3 + ) + texture = image.get_texture() + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_NEAREST) + texture.width = self.width + texture.height = self.height + self.window.clear() + self.window.switch_to() + self.window.dispatch_events() + texture.blit(0, 0) # draw + self.window.flip() + + def close(self): + if self.isopen and sys.meta_path: + # ^^^ check sys.meta_path to avoid 'ImportError: sys.meta_path is None, Python is likely shutting down' + self.window.close() + self.isopen = False + + def __del__(self): + self.close() diff --git a/gym-0.21.0/gym/envs/robotics/assets/fetch/robot.xml b/gym-0.21.0/gym/envs/robotics/assets/fetch/robot.xml new file mode 100644 index 0000000000000000000000000000000000000000..b627d4925d52565feb41ce9bb53d745ec5eb2e6c --- /dev/null +++ b/gym-0.21.0/gym/envs/robotics/assets/fetch/robot.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gym-0.21.0/gym/envs/robotics/assets/fetch/shared.xml b/gym-0.21.0/gym/envs/robotics/assets/fetch/shared.xml new file mode 100644 index 0000000000000000000000000000000000000000..5d61fef70dd10b018a216b73c023f958abaf8234 --- /dev/null +++ b/gym-0.21.0/gym/envs/robotics/assets/fetch/shared.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gym-0.21.0/gym/envs/robotics/assets/hand/manipulate_block.xml b/gym-0.21.0/gym/envs/robotics/assets/hand/manipulate_block.xml new file mode 100644 index 0000000000000000000000000000000000000000..83a6517e6cf6ecc618060346e2fb8da66b6e76ae --- /dev/null +++ b/gym-0.21.0/gym/envs/robotics/assets/hand/manipulate_block.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gym-0.21.0/gym/envs/robotics/assets/stls/fetch/bellows_link_collision.stl b/gym-0.21.0/gym/envs/robotics/assets/stls/fetch/bellows_link_collision.stl new file mode 100644 index 0000000000000000000000000000000000000000..a7e5ab75ca42d597fc49d7913446ab7e6c2388f3 Binary files /dev/null and b/gym-0.21.0/gym/envs/robotics/assets/stls/fetch/bellows_link_collision.stl differ diff --git a/gym-0.21.0/gym/envs/robotics/assets/textures/block.png b/gym-0.21.0/gym/envs/robotics/assets/textures/block.png new file mode 100644 index 0000000000000000000000000000000000000000..0243b8f33123749b1811b3ae03eeb202d0279db6 Binary files /dev/null and b/gym-0.21.0/gym/envs/robotics/assets/textures/block.png differ diff --git a/gym-0.21.0/gym/envs/robotics/fetch/push.py b/gym-0.21.0/gym/envs/robotics/fetch/push.py new file mode 100644 index 0000000000000000000000000000000000000000..3227374e8ccba1fff4dcbf8ee2684d781f7970da --- /dev/null +++ b/gym-0.21.0/gym/envs/robotics/fetch/push.py @@ -0,0 +1,33 @@ +import os +from gym import utils +from gym.envs.robotics import fetch_env + + +# Ensure we get the path separator correct on windows +MODEL_XML_PATH = os.path.join("fetch", "push.xml") + + +class FetchPushEnv(fetch_env.FetchEnv, utils.EzPickle): + def __init__(self, reward_type="sparse"): + initial_qpos = { + "robot0:slide0": 0.405, + "robot0:slide1": 0.48, + "robot0:slide2": 0.0, + "object0:joint": [1.25, 0.53, 0.4, 1.0, 0.0, 0.0, 0.0], + } + fetch_env.FetchEnv.__init__( + self, + MODEL_XML_PATH, + has_object=True, + block_gripper=True, + n_substeps=20, + gripper_extra_height=0.0, + target_in_the_air=False, + target_offset=0.0, + obj_range=0.15, + target_range=0.15, + distance_threshold=0.05, + initial_qpos=initial_qpos, + reward_type=reward_type, + ) + utils.EzPickle.__init__(self, reward_type=reward_type) diff --git a/gym-0.21.0/gym/envs/robotics/fetch/slide.py b/gym-0.21.0/gym/envs/robotics/fetch/slide.py new file mode 100644 index 0000000000000000000000000000000000000000..ab47db7ff52780c10066f6e78482c7ab7a00a676 --- /dev/null +++ b/gym-0.21.0/gym/envs/robotics/fetch/slide.py @@ -0,0 +1,35 @@ +import os +import numpy as np + +from gym import utils +from gym.envs.robotics import fetch_env + + +# Ensure we get the path separator correct on windows +MODEL_XML_PATH = os.path.join("fetch", "slide.xml") + + +class FetchSlideEnv(fetch_env.FetchEnv, utils.EzPickle): + def __init__(self, reward_type="sparse"): + initial_qpos = { + "robot0:slide0": 0.05, + "robot0:slide1": 0.48, + "robot0:slide2": 0.0, + "object0:joint": [1.7, 1.1, 0.41, 1.0, 0.0, 0.0, 0.0], + } + fetch_env.FetchEnv.__init__( + self, + MODEL_XML_PATH, + has_object=True, + block_gripper=True, + n_substeps=20, + gripper_extra_height=-0.02, + target_in_the_air=False, + target_offset=np.array([0.4, 0.0, 0.0]), + obj_range=0.1, + target_range=0.3, + distance_threshold=0.05, + initial_qpos=initial_qpos, + reward_type=reward_type, + ) + utils.EzPickle.__init__(self, reward_type=reward_type) diff --git a/gym-0.21.0/gym/envs/robotics/robot_env.py b/gym-0.21.0/gym/envs/robotics/robot_env.py new file mode 100644 index 0000000000000000000000000000000000000000..1fe6ca653e8d37d42abd01ecae6fc46548b3381e --- /dev/null +++ b/gym-0.21.0/gym/envs/robotics/robot_env.py @@ -0,0 +1,179 @@ +import os +import copy +import numpy as np + +import gym +from gym import error, spaces +from gym.utils import seeding + +try: + import 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 + ) + ) + +DEFAULT_SIZE = 500 + + +class RobotEnv(gym.GoalEnv): + def __init__(self, model_path, initial_qpos, n_actions, n_substeps): + if model_path.startswith("/"): + fullpath = model_path + else: + fullpath = os.path.join(os.path.dirname(__file__), "assets", model_path) + if not os.path.exists(fullpath): + raise IOError("File {} does not exist".format(fullpath)) + + model = mujoco_py.load_model_from_path(fullpath) + self.sim = mujoco_py.MjSim(model, nsubsteps=n_substeps) + self.viewer = None + self._viewers = {} + + self.metadata = { + "render.modes": ["human", "rgb_array"], + "video.frames_per_second": int(np.round(1.0 / self.dt)), + } + + self.seed() + self._env_setup(initial_qpos=initial_qpos) + self.initial_state = copy.deepcopy(self.sim.get_state()) + + self.goal = self._sample_goal() + obs = self._get_obs() + self.action_space = spaces.Box(-1.0, 1.0, shape=(n_actions,), dtype="float32") + self.observation_space = spaces.Dict( + dict( + desired_goal=spaces.Box( + -np.inf, np.inf, shape=obs["achieved_goal"].shape, dtype="float32" + ), + achieved_goal=spaces.Box( + -np.inf, np.inf, shape=obs["achieved_goal"].shape, dtype="float32" + ), + observation=spaces.Box( + -np.inf, np.inf, shape=obs["observation"].shape, dtype="float32" + ), + ) + ) + + @property + def dt(self): + return self.sim.model.opt.timestep * self.sim.nsubsteps + + # Env methods + # ---------------------------- + + def seed(self, seed=None): + self.np_random, seed = seeding.np_random(seed) + return [seed] + + def step(self, action): + action = np.clip(action, self.action_space.low, self.action_space.high) + self._set_action(action) + self.sim.step() + self._step_callback() + obs = self._get_obs() + + done = False + info = { + "is_success": self._is_success(obs["achieved_goal"], self.goal), + } + reward = self.compute_reward(obs["achieved_goal"], self.goal, info) + return obs, reward, done, info + + def reset(self): + # Attempt to reset the simulator. Since we randomize initial conditions, it + # is possible to get into a state with numerical issues (e.g. due to penetration or + # Gimbel lock) or we may not achieve an initial condition (e.g. an object is within the hand). + # In this case, we just keep randomizing until we eventually achieve a valid initial + # configuration. + super(RobotEnv, self).reset() + did_reset_sim = False + while not did_reset_sim: + did_reset_sim = self._reset_sim() + self.goal = self._sample_goal().copy() + obs = self._get_obs() + return obs + + def close(self): + if self.viewer is not None: + # self.viewer.finish() + self.viewer = None + self._viewers = {} + + def render(self, mode="human", width=DEFAULT_SIZE, height=DEFAULT_SIZE): + self._render_callback() + if mode == "rgb_array": + self._get_viewer(mode).render(width, height) + # window size used for old mujoco-py: + data = self._get_viewer(mode).read_pixels(width, height, depth=False) + # original image is upside-down, so flip it + return data[::-1, :, :] + elif mode == "human": + self._get_viewer(mode).render() + + def _get_viewer(self, mode): + self.viewer = self._viewers.get(mode) + if self.viewer is None: + if mode == "human": + self.viewer = mujoco_py.MjViewer(self.sim) + elif mode == "rgb_array": + self.viewer = mujoco_py.MjRenderContextOffscreen(self.sim, device_id=-1) + self._viewer_setup() + self._viewers[mode] = self.viewer + return self.viewer + + # Extension methods + # ---------------------------- + + def _reset_sim(self): + """Resets a simulation and indicates whether or not it was successful. + If a reset was unsuccessful (e.g. if a randomized state caused an error in the + simulation), this method should indicate such a failure by returning False. + In such a case, this method will be called again to attempt a the reset again. + """ + self.sim.set_state(self.initial_state) + self.sim.forward() + return True + + def _get_obs(self): + """Returns the observation.""" + raise NotImplementedError() + + def _set_action(self, action): + """Applies the given action to the simulation.""" + raise NotImplementedError() + + def _is_success(self, achieved_goal, desired_goal): + """Indicates whether or not the achieved goal successfully achieved the desired goal.""" + raise NotImplementedError() + + def _sample_goal(self): + """Samples a new goal and returns it.""" + raise NotImplementedError() + + def _env_setup(self, initial_qpos): + """Initial configuration of the environment. Can be used to configure initial state + and extract information from the simulation. + """ + pass + + def _viewer_setup(self): + """Initial configuration of the viewer. Can be used to set the camera position, + for example. + """ + pass + + def _render_callback(self): + """A custom callback that is called before rendering. Can be used + to implement custom visualizations. + """ + pass + + def _step_callback(self): + """A custom callback that is called after stepping the simulation. Can be used + to enforce additional constraints on the simulation state. + """ + pass