From e5615d404db171a3d68cf4b5fb2f084c2f66d485 Mon Sep 17 00:00:00 2001 From: RKJ Date: Sat, 17 Sep 2022 00:08:31 +0530 Subject: [PATCH 01/19] actuator force rendering via sites --- examples/actuator_force_rendering.py | 75 ++++++++++++++++++++++++++++ mujoco_viewer/mujoco_viewer.py | 62 +++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 examples/actuator_force_rendering.py diff --git a/examples/actuator_force_rendering.py b/examples/actuator_force_rendering.py new file mode 100644 index 0000000..5f1469a --- /dev/null +++ b/examples/actuator_force_rendering.py @@ -0,0 +1,75 @@ +import mujoco +import mujoco_viewer + +MODEL_XML = """ + + + +""" + +model = mujoco.MjModel.from_xml_string(MODEL_XML) +data = mujoco.MjData(model) + +# create the viewer object +viewer = mujoco_viewer.MujocoViewer(model, data) + +for _ in range(10000): + # Render forces + viewer.show_actuator_forces( + site_list=["site_1", "site_2", "site_3"], + actuator_list=["pos_servo_1", "pos_servo_2", "pos_servo_3"], + rgba_list=[1, 0.5, 1, 0.5], + force_scale=0.05, + arrow_radius=0.05, + show_force_labels=True, + ) + + # step and render + mujoco.mj_step(model, data) + viewer.render() + +# close +viewer.close() diff --git a/mujoco_viewer/mujoco_viewer.py b/mujoco_viewer/mujoco_viewer.py index 7cc0cc7..07ec8e6 100644 --- a/mujoco_viewer/mujoco_viewer.py +++ b/mujoco_viewer/mujoco_viewer.py @@ -117,6 +117,68 @@ def __init__( # overlay, markers self._overlay = {} self._markers = [] + + def show_actuator_forces( + self, + site_list, + actuator_list, + rgba_list=[1, 0, 1, 1], + force_scale=0.05, + arrow_radius=0.03, + show_force_labels=False, + ) -> None: + if show_force_labels is False: + for i in range(0, len(site_list)): + self.add_marker( + pos=self.data.site(i).xpos, + mat=self.data.site(i).xmat, + size=[ + arrow_radius, + arrow_radius, + self.data.actuator_force[ + mujoco.mj_name2id( + self.model, + mujoco.mjtObj.mjOBJ_ACTUATOR, + actuator_list[i], + ) + ] + * force_scale, + ], + rgba=rgba_list, + type=mujoco.mjtGeom.mjGEOM_ARROW, + label="", + ) + else: + for i in range(0, len(site_list)): + self.add_marker( + pos=self.data.site(i).xpos, + mat=self.data.site(i).xmat, + size=[ + arrow_radius, + arrow_radius, + self.data.actuator_force[ + mujoco.mj_name2id( + self.model, + mujoco.mjtObj.mjOBJ_ACTUATOR, + actuator_list[i], + ) + ] + * force_scale, + ], + rgba=rgba_list, + type=mujoco.mjtGeom.mjGEOM_ARROW, + label=str(actuator_list[i]) + + ":" + + str( + self.data.actuator_force[ + mujoco.mj_name2id( + self.model, + mujoco.mjtObj.mjOBJ_ACTUATOR, + actuator_list[i], + ) + ] + ), + ) def add_marker(self, **marker_params): self._markers.append(marker_params) From 921f14be22f888e817a9888c7cff3b26bf8bdb19 Mon Sep 17 00:00:00 2001 From: RKJ Date: Sat, 17 Sep 2022 01:12:01 +0530 Subject: [PATCH 02/19] Added v1 graphing and rendering utilities --- .vscode/settings.json | 3 + examples/actuator_force_rendering.py | 3 + examples/graphs.py | 100 +++++++++++++++++ mujoco_viewer/callbacks.py | 4 + mujoco_viewer/mujoco_viewer.py | 156 +++++++++++++++++++++++++++ 5 files changed, 266 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 examples/graphs.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..74a53f5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.formatOnSaveMode": "modifications" +} \ No newline at end of file diff --git a/examples/actuator_force_rendering.py b/examples/actuator_force_rendering.py index 5f1469a..392a267 100644 --- a/examples/actuator_force_rendering.py +++ b/examples/actuator_force_rendering.py @@ -1,5 +1,6 @@ import mujoco import mujoco_viewer +import math MODEL_XML = """ @@ -70,6 +71,8 @@ # step and render mujoco.mj_step(model, data) viewer.render() + if not viewer.is_alive: + break # close viewer.close() diff --git a/examples/graphs.py b/examples/graphs.py new file mode 100644 index 0000000..7eca3bd --- /dev/null +++ b/examples/graphs.py @@ -0,0 +1,100 @@ +import mujoco +import mujoco_viewer +import math + +MODEL_XML = """ + + + +""" + +model = mujoco.MjModel.from_xml_string(MODEL_XML) +data = mujoco.MjData(model) + +# create the viewer object +viewer = mujoco_viewer.MujocoViewer(model, data) + + +# Initialize the graph lines +viewer.add_graph_line(line_name="line_1") +viewer.add_graph_line(line_name="downscaled_force_sensor") +viewer.add_graph_line(line_name="position_sensor") + +# Initialize the Legend +viewer.show_graph_legend(show_legend=True) + +# For a time-based graph, +# x_axis_time is the total time that you want your viewing window to see +# It needs to be set to grater than [model.opt.timestep*50] +# If you want to over ride that, change the "override" parameter to True +viewer.set_grid_divisions(x_div=5, + y_div=4, + x_axis_time=model.opt.timestep * 1000, + override=True) + +for i in range(10000): + # Render forces + viewer.show_actuator_forces( + site_list=["site_1", "site_2", "site_3"], + actuator_list=["pos_servo_1", "pos_servo_2", "pos_servo_3"], + rgba_list=[1, 0.5, 1, 0.5], + force_scale=0.05, + arrow_radius=0.05, + show_force_labels=True, + ) + + viewer.update_graph_line(line_name="line_1", line_data=math.sin(i / 10.0)) + viewer.update_graph_line(line_name="downscaled_force_sensor", line_data=data.actuator_force[0]/100.0) + viewer.update_graph_line(line_name="position_sensor", line_data=data.sensordata[0]) + + # step and render + mujoco.mj_step(model, data) + viewer.render() + if not viewer.is_alive: + break + +# close +viewer.close() diff --git a/mujoco_viewer/callbacks.py b/mujoco_viewer/callbacks.py index 92c63b8..30637b7 100644 --- a/mujoco_viewer/callbacks.py +++ b/mujoco_viewer/callbacks.py @@ -18,6 +18,7 @@ def __init__(self, hide_menus): self._last_mouse_x = 0 self._last_mouse_y = 0 self._paused = False + self._hide_graph = False self._transparent = False self._contacts = False self._joints = False @@ -97,6 +98,9 @@ def _key_callback(self, window, key, scancode, action, mods): self.model.geom_rgba[:, 3] /= 5.0 else: self.model.geom_rgba[:, 3] *= 5.0 + # Toggle Graph overlay + elif key == glfw.KEY_G: + self._hide_graph = not self._hide_graph # Display inertia elif key == glfw.KEY_I: self._inertias = not self._inertias diff --git a/mujoco_viewer/mujoco_viewer.py b/mujoco_viewer/mujoco_viewer.py index 07ec8e6..d80e677 100644 --- a/mujoco_viewer/mujoco_viewer.py +++ b/mujoco_viewer/mujoco_viewer.py @@ -70,8 +70,36 @@ def __init__( self.cam = mujoco.MjvCamera() self.scn = mujoco.MjvScene(self.model, maxgeom=10000) self.pert = mujoco.MjvPerturb() + self.fig = mujoco.MjvFigure() + mujoco.mjv_defaultFigure(self.fig) + + # Points for sampling of sensors... dictates smoothness of graph + self._num_pnts = 100 + self._data_graph_line_names = [] + self._line_datas = [] + + for n in range(0, len(self.model.sensor_adr) * 3): + for i in range(0, 300): + self.fig.linedata[n][2 * i] = float(-i) + self.ctx = mujoco.MjrContext( self.model, mujoco.mjtFontScale.mjFONTSCALE_150.value) + + # Adjust placement and size of graph + width, height = glfw.get_framebuffer_size(self.window) + width_adjustment = width % 4 + self.graph_viewport = mujoco.MjrRect( + int(3 * width / 4) + width_adjustment, + 0, + int(width / 4), + int(height / 4), + ) + mujoco.mjr_figure(self.graph_viewport, self.fig, self.ctx) + self.fig.flg_extend = 1 + self.fig.flg_symmetric = 0 + + # Makes the graph to be in autorange + self.axis_autorange() # load camera from configuration (if available) pathlib.Path( @@ -117,7 +145,110 @@ def __init__( # overlay, markers self._overlay = {} self._markers = [] + + def set_grid_divisions(self, x_div: int, y_div: int, x_axis_time: float = 0.0, override=False): + if override is False: + assert x_axis_time >= self.model.opt.timestep * 50, "Set [x_axis_time] >= [self.model.opt.timestep * 50], inorder to get a suitable sampling rate" + self.fig.gridsize[0] = x_div + 1 + self.fig.gridsize[1] = y_div + 1 + if x_axis_time != 0.0: + self._num_pnts = x_axis_time / self.model.opt.timestep + print("self._num_pnts: ", self._num_pnts) + if self._num_pnts > 300: + self._num_pnts = 300 + new_x_axis_time = self.model.opt.timestep * self._num_pnts + print( + f"Minimum x_axis_time is: {new_x_axis_time}" + + " reduce the x_axis_time" + f" OR Maximum time_step is: " + + f"{self.model.opt.timestep*self._num_pnts}" + + " increase the timestep" + ) + # assert x_axis_time == + assert 1 <= self._num_pnts <= 300, ( + "num_pnts should be [10,300], it is currently:", + f"{self._num_pnts}", + ) + # self._num_pnts = num_pnts + self._time_per_div = (self.model.opt.timestep * self._num_pnts) / ( + x_div + ) + self.set_x_label( + xname=f"time/div: {self._time_per_div}s" + + f" total: {self.model.opt.timestep * self._num_pnts}" + ) + + def axis_autorange(self): + """ + Call this function to auto-range the graph + """ + self.fig.range[0][0] = 1.0 + self.fig.range[0][1] = -1.0 + self.fig.range[1][0] = 1.0 + self.fig.range[1][1] = -1.0 + + def set_graph_name(self, name: str): + assert type(name) == str, "name is not a string" + self.fig.title = name + + def show_graph_legend(self, show_legend: bool = True): + if show_legend is True: + for i in range(0, len(self._data_graph_line_names)): + self.fig.linename[i] = self._data_graph_line_names[i] + self.fig.flg_legend = True + + def set_x_label(self, xname: str): + assert type(xname) == str, "xname is not a string" + self.fig.xlabel = xname + + def add_graph_line(self, line_name, line_data=0.0): + assert ( + type(line_name) == str + ), f"Line_name is not a string: {type(line_name)}" + if line_name in self._data_graph_line_names: + print("line name already exists") + else: + self._data_graph_line_names.append(line_name) + self._line_datas.append(line_data) + + def update_graph_line(self, line_name, line_data): + if line_name in self._data_graph_line_names: + idx = self._data_graph_line_names.index(line_name) + self._line_datas[idx] = line_data + else: + raise NameError( + "line name is not valid, add it to list before calling update" + ) + + def sensorupdate(self): + pnt = int(mujoco.mju_min(self._num_pnts, self.fig.linepnt[0] + 1)) + for n in range(0, len(self._line_datas)): + for i in range(pnt - 1, 0, -1): + self.fig.linedata[n][2 * i + 1] = self.fig.linedata[n][ + 2 * i - 1 + ] + self.fig.linepnt[n] = pnt + self.fig.linedata[n][1] = self._line_datas[n] + + def update_graph_size(self, size_div_x=None, size_div_y=None): + if size_div_x is None and size_div_y is None: + width, height = glfw.get_framebuffer_size(self.window) + width_adjustment = width % 3 + self.graph_viewport.left = int(2 * width / 3) + width_adjustment + self.graph_viewport.width = int(width / 3) + self.graph_viewport.height = int(height / 3) + + else: + assert size_div_x is not None and size_div_y is None, "" + width, height = glfw.get_framebuffer_size(self.window) + width_adjustment = width % size_div_x + self.graph_viewport.left = ( + int((size_div_x - 1) * width / size_div_x) + width_adjustment + ) + self.graph_viewport.width = int(width / size_div_x) + self.graph_viewport.height = int(height / size_div_x) + def show_actuator_forces( self, site_list, @@ -267,6 +398,10 @@ def add_overlay(gridpos, text1, text2): topleft, "[J]oints", "On" if self._joints else "Off") + add_overlay( + topleft, + "[G]raph Viewer", + "Off" if self._hide_graph else "On") add_overlay( topleft, "[I]nertia", @@ -411,6 +546,27 @@ def update(): t1, t2, self.ctx) + + # Handle graph and pausing interactions + if ( + not self._paused + and not self._hide_graph + ): + self.sensorupdate() + self.update_graph_size() + mujoco.mjr_figure( + self.graph_viewport, self.fig, self.ctx + ) + elif self._hide_graph and self._paused: + self.update_graph_size() + elif not self._hide_graph and self._paused: + mujoco.mjr_figure( + self.graph_viewport, self.fig, self.ctx + ) + elif self._hide_graph and not self._paused: + self.sensorupdate() + self.update_graph_size() + glfw.swap_buffers(self.window) glfw.poll_events() self._time_per_render = 0.9 * self._time_per_render + \ From 19eb1e354308caa93f53bb8b652f4e72583f29ed Mon Sep 17 00:00:00 2001 From: RKJ Date: Sat, 17 Sep 2022 01:14:48 +0530 Subject: [PATCH 03/19] modified gitignore --- .gitignore | 161 ++++++++++++++++++++++++++++++++++++++++++ .vscode/settings.json | 3 - 2 files changed, 161 insertions(+), 3 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 4606f98..b120dbb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,164 @@ dist/ *.egg-info mujoco_viewer/__pycache__ mujoco_viewer/MUJOCO_LOG.TXT + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 74a53f5..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "editor.formatOnSaveMode": "modifications" -} \ No newline at end of file From a66038c4ee1d03bb51731f47597093cf499ea4e3 Mon Sep 17 00:00:00 2001 From: RKJ Date: Sat, 17 Sep 2022 14:15:15 +0530 Subject: [PATCH 04/19] used named access to get actuator force --- mujoco_viewer/mujoco_viewer.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/mujoco_viewer/mujoco_viewer.py b/mujoco_viewer/mujoco_viewer.py index d80e677..0a460a8 100644 --- a/mujoco_viewer/mujoco_viewer.py +++ b/mujoco_viewer/mujoco_viewer.py @@ -266,14 +266,7 @@ def show_actuator_forces( size=[ arrow_radius, arrow_radius, - self.data.actuator_force[ - mujoco.mj_name2id( - self.model, - mujoco.mjtObj.mjOBJ_ACTUATOR, - actuator_list[i], - ) - ] - * force_scale, + self.data.actuator(actuator_list[i]).force* force_scale, ], rgba=rgba_list, type=mujoco.mjtGeom.mjGEOM_ARROW, @@ -287,14 +280,7 @@ def show_actuator_forces( size=[ arrow_radius, arrow_radius, - self.data.actuator_force[ - mujoco.mj_name2id( - self.model, - mujoco.mjtObj.mjOBJ_ACTUATOR, - actuator_list[i], - ) - ] - * force_scale, + self.data.actuator(actuator_list[i]).force* force_scale, ], rgba=rgba_list, type=mujoco.mjtGeom.mjGEOM_ARROW, From 70837593949e0b1eeaee982a12e753292d269a0b Mon Sep 17 00:00:00 2001 From: RKJ Date: Sat, 17 Sep 2022 14:21:27 +0530 Subject: [PATCH 05/19] Now hides graph if hide_menus=True at init --- examples/actuator_force_rendering.py | 2 +- mujoco_viewer/mujoco_viewer.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/actuator_force_rendering.py b/examples/actuator_force_rendering.py index 392a267..16df30e 100644 --- a/examples/actuator_force_rendering.py +++ b/examples/actuator_force_rendering.py @@ -55,7 +55,7 @@ data = mujoco.MjData(model) # create the viewer object -viewer = mujoco_viewer.MujocoViewer(model, data) +viewer = mujoco_viewer.MujocoViewer(model, data, hide_menus=True) for _ in range(10000): # Render forces diff --git a/mujoco_viewer/mujoco_viewer.py b/mujoco_viewer/mujoco_viewer.py index 0a460a8..725c8bd 100644 --- a/mujoco_viewer/mujoco_viewer.py +++ b/mujoco_viewer/mujoco_viewer.py @@ -18,6 +18,8 @@ def __init__( height=None, hide_menus=False): super().__init__(hide_menus) + if hide_menus is True: + self._hide_graph = True self.model = model self.data = data From f35cf84526a1b24f5898d0bdb1ef09f9ff828f27 Mon Sep 17 00:00:00 2001 From: RKJ Date: Wed, 21 Sep 2022 00:15:15 +0530 Subject: [PATCH 06/19] force rendering w/o site -- tested only for hinge --- examples/actuator_force_rendering.py | 13 +++---------- mujoco_viewer/mujoco_viewer.py | 28 +++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/examples/actuator_force_rendering.py b/examples/actuator_force_rendering.py index 16df30e..dad84b6 100644 --- a/examples/actuator_force_rendering.py +++ b/examples/actuator_force_rendering.py @@ -6,10 +6,6 @@