mujoco-toolbox 0.1.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mujoco_toolbox/Controller.py +131 -0
- mujoco_toolbox/Utils.py +53 -0
- mujoco_toolbox/Wrapper.py +741 -0
- mujoco_toolbox/__init__.py +67 -0
- mujoco_toolbox-0.1.8.dist-info/LICENSE +21 -0
- mujoco_toolbox-0.1.8.dist-info/METADATA +186 -0
- mujoco_toolbox-0.1.8.dist-info/RECORD +8 -0
- mujoco_toolbox-0.1.8.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from numpy import cos, pi, random, sin
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _apply_control(model, data, value, joint=None, axis=None, delay=0):
|
|
5
|
+
"""Common helper function for controller logic to reduce redundancy.
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
model: MuJoCo model.
|
|
9
|
+
data: MuJoCo data.
|
|
10
|
+
value: Control value to apply.
|
|
11
|
+
joint (list[int], optional): Joints to apply control to.
|
|
12
|
+
axis (int, optional): Axis to apply control to.
|
|
13
|
+
delay (float, optional): Delay before applying control.
|
|
14
|
+
"""
|
|
15
|
+
# Don't apply control until after delay
|
|
16
|
+
if data.time < delay:
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
# Determine targets if not explicitly provided
|
|
20
|
+
if joint is None and axis is None:
|
|
21
|
+
joint = range(model.nu)
|
|
22
|
+
|
|
23
|
+
# Apply control to joints or specific axis
|
|
24
|
+
if joint is not None and model.nu > 0:
|
|
25
|
+
for j in joint:
|
|
26
|
+
data.ctrl[j] = value
|
|
27
|
+
elif axis is not None:
|
|
28
|
+
data.qpos[axis] = value
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def stepController(model, data, **kwargs):
|
|
32
|
+
"""A step controller for the simulation.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
amplitude (float): The amplitude of the step signal (default=1).
|
|
36
|
+
joint (list[int]): The joints to apply the step signal to (default=all).
|
|
37
|
+
axis (int): The axis to apply the step signal to (default=None).
|
|
38
|
+
delay (float): The delay before applying the step signal (default=0).
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
None
|
|
42
|
+
"""
|
|
43
|
+
amplitude = kwargs.get("amplitude", 1)
|
|
44
|
+
joint = kwargs.get("joint", None)
|
|
45
|
+
axis = kwargs.get("axis", None)
|
|
46
|
+
delay = kwargs.get("delay", 0)
|
|
47
|
+
|
|
48
|
+
if delay < 0:
|
|
49
|
+
raise ValueError("Delay must be non-negative.")
|
|
50
|
+
if joint is not None and axis is not None:
|
|
51
|
+
raise ValueError("Cannot specify both 'joint' and 'axis'.")
|
|
52
|
+
|
|
53
|
+
_apply_control(model, data, amplitude, joint=joint, axis=axis, delay=delay)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def sineController(model, data, **kwargs):
|
|
57
|
+
"""A simple sine wave controller for the simulation.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
amplitude (float): The amplitude of the sine wave (default=1).
|
|
61
|
+
frequency (float): The frequency of the sine wave (default=1).
|
|
62
|
+
phase (float): The phase shift of the sine wave (default=0).
|
|
63
|
+
joint (list[int]): The joint to apply the sine wave to (default=all).
|
|
64
|
+
delay (float): The delay before applying the sine wave (default=0).
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
None
|
|
68
|
+
"""
|
|
69
|
+
amplitude = kwargs.get("amplitude", 1)
|
|
70
|
+
frequency = kwargs.get("frequency", 1)
|
|
71
|
+
phase = kwargs.get("phase", 0)
|
|
72
|
+
joint = kwargs.get("joint", None)
|
|
73
|
+
delay = kwargs.get("delay", 0)
|
|
74
|
+
|
|
75
|
+
if delay < 0:
|
|
76
|
+
raise ValueError("Delay must be non-negative.")
|
|
77
|
+
|
|
78
|
+
value = amplitude * sin(2 * pi * frequency * data.time + phase)
|
|
79
|
+
_apply_control(model, data, value, joint=joint, delay=delay)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def cosineController(model, data, **kwargs):
|
|
83
|
+
"""A simple cosine wave controller for the simulation.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
amplitude (float): The amplitude of the cosine wave (default=1).
|
|
87
|
+
frequency (float): The frequency of the cosine wave (default=1).
|
|
88
|
+
phase (float): The phase shift of the cosine wave (default=0).
|
|
89
|
+
joint (list[int]): The joint to apply the cosine wave to (default=all).
|
|
90
|
+
delay (float): The delay before applying the cosine wave (default=0).
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
None
|
|
94
|
+
"""
|
|
95
|
+
amplitude = kwargs.get("amplitude", 1)
|
|
96
|
+
frequency = kwargs.get("frequency", 1)
|
|
97
|
+
phase = kwargs.get("phase", 0)
|
|
98
|
+
joint = kwargs.get("joint", None)
|
|
99
|
+
delay = kwargs.get("delay", 0)
|
|
100
|
+
|
|
101
|
+
if delay < 0:
|
|
102
|
+
raise ValueError("Delay must be non-negative.")
|
|
103
|
+
|
|
104
|
+
value = amplitude * cos(2 * pi * frequency * data.time + phase)
|
|
105
|
+
_apply_control(model, data, value, joint=joint, delay=delay)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def randomController(model, data, **kwargs):
|
|
109
|
+
"""A random controller for the simulation.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
amplitude (float): The maximum amplitude of the random signal (default=1).
|
|
113
|
+
joint (list[int]): The joints to apply the random signal to (default=all).
|
|
114
|
+
axis (int): The axis to apply the random signal to (default=None).
|
|
115
|
+
delay (float): The delay before applying the random signal (default=0).
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
None
|
|
119
|
+
"""
|
|
120
|
+
amplitude = kwargs.get("amplitude", 1)
|
|
121
|
+
joint = kwargs.get("joint", None)
|
|
122
|
+
axis = kwargs.get("axis", None)
|
|
123
|
+
delay = kwargs.get("delay", 0)
|
|
124
|
+
|
|
125
|
+
if delay < 0:
|
|
126
|
+
raise ValueError("Delay must be non-negative.")
|
|
127
|
+
if joint is not None and axis is not None:
|
|
128
|
+
raise ValueError("Cannot specify both 'joint' and 'axis'.")
|
|
129
|
+
|
|
130
|
+
value = amplitude * random.rand()
|
|
131
|
+
_apply_control(model, data, value, joint=joint, axis=axis, delay=delay)
|
mujoco_toolbox/Utils.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
from sys import platform
|
|
3
|
+
from time import time
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def timer(func):
|
|
7
|
+
@wraps(func)
|
|
8
|
+
def time_wrapper(*args, **kwargs):
|
|
9
|
+
start_time = time()
|
|
10
|
+
result = func(*args, **kwargs)
|
|
11
|
+
end_time = time()
|
|
12
|
+
total_time = end_time - start_time
|
|
13
|
+
|
|
14
|
+
from . import VERBOSITY
|
|
15
|
+
|
|
16
|
+
if VERBOSITY:
|
|
17
|
+
print(f"Function '{func.__name__}' took {total_time:.4f} seconds")
|
|
18
|
+
return result
|
|
19
|
+
|
|
20
|
+
return time_wrapper
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def print_warning(*args) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Prints the first argument as an orange warning message and prints the other arguments in regular text, all on the same line.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
*args: First argument is printed as an orange warning, other arguments are printed normally.
|
|
29
|
+
"""
|
|
30
|
+
if args:
|
|
31
|
+
# Make sure the first argument is printed with the 'WARNING:' prefix and in orange
|
|
32
|
+
message = f"WARNING: {args[0]}"
|
|
33
|
+
|
|
34
|
+
# Check if we're on Windows or a Unix-like system for color support
|
|
35
|
+
if platform.startswith("win"):
|
|
36
|
+
try:
|
|
37
|
+
from colorama import Fore, Style, init
|
|
38
|
+
|
|
39
|
+
init() # Initialize colorama
|
|
40
|
+
print(
|
|
41
|
+
f"{Fore.YELLOW}{message}{Style.RESET_ALL}", end=" "
|
|
42
|
+
) # Yellow color for warning
|
|
43
|
+
print(
|
|
44
|
+
*args[1:], end=""
|
|
45
|
+
) # No newline, print all args separated by spaces
|
|
46
|
+
except ImportError:
|
|
47
|
+
# If colorama is not available, print without color
|
|
48
|
+
print(message, end=" ")
|
|
49
|
+
print(*args[1:], end="")
|
|
50
|
+
else:
|
|
51
|
+
# ANSI escape code for orange (for Unix-like systems)
|
|
52
|
+
print(f"\033[38;5;214m{message}\033[0m", end=" ") # Orange ANSI escape code
|
|
53
|
+
print(*args[1:], end="") # No newline, print all args separated by spaces
|
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import xml.etree.ElementTree as ET
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
import mediapy as media
|
|
10
|
+
import mujoco
|
|
11
|
+
import numpy as np
|
|
12
|
+
import trimesh
|
|
13
|
+
import yaml
|
|
14
|
+
from screeninfo import get_monitors
|
|
15
|
+
from tqdm import tqdm as barTerminal
|
|
16
|
+
from tqdm.notebook import tqdm as barNotebook
|
|
17
|
+
from screeninfo import get_monitors
|
|
18
|
+
import trimesh
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import Dict, List, Tuple, Union, Optional, Callable, Any, TypeAlias
|
|
23
|
+
|
|
24
|
+
from .Utils import print_warning, timer
|
|
25
|
+
|
|
26
|
+
assert sys.version_info >= (3, 10), "This code requires Python 3.10.0 or later."
|
|
27
|
+
assert mujoco.__version__ >= "2.0.0", "This code requires MuJoCo 2.0.0 or later."
|
|
28
|
+
|
|
29
|
+
mjModel: TypeAlias = mujoco.MjModel
|
|
30
|
+
mjData: TypeAlias = mujoco.MjData
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Wrapper(object):
|
|
34
|
+
"""A class to handle MuJoCo simulations and data capture."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, xml:str, duration:int=10, fps:int=30, resolution:Tuple[int,int]=(400,300), initialConditions:Dict[str, List]={}, controller:Optional[Callable[[mjModel, mjData, Any], None]]=None, *args, **kwargs):
|
|
37
|
+
self._load_model(xml, **kwargs)
|
|
38
|
+
|
|
39
|
+
self.duration = duration
|
|
40
|
+
self.fps = fps
|
|
41
|
+
self.resolution = resolution # recursively sets width and height
|
|
42
|
+
self.init_conditions = initialConditions
|
|
43
|
+
self.controller = controller
|
|
44
|
+
|
|
45
|
+
# Predefined simulation parameters but can be overridden
|
|
46
|
+
# TODO: Currently Causing Bugs when occluded from XML Code
|
|
47
|
+
self.ts = kwargs.get("ts", self._model.opt.timestep)
|
|
48
|
+
self.gravity = kwargs.get("gravity", self._model.opt.gravity)
|
|
49
|
+
|
|
50
|
+
self._data = mujoco.MjData(self._model)
|
|
51
|
+
|
|
52
|
+
# Auto-Populate the names of bodies, joints, and actuators
|
|
53
|
+
self._body_names = [self._model.body(i).name for i in range(self._model.nbody)]
|
|
54
|
+
self._geom_names = [self._model.geom(i).name for i in range(self._model.ngeom)]
|
|
55
|
+
self._joint_names = [self._model.joint(i).name for i in range(self._model.njnt)]
|
|
56
|
+
self._actuator_names = [
|
|
57
|
+
self._model.actuator(i).name for i in range(self._model.nu)
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
def _load_model(self, xml: str, **kwargs: Any) -> None:
|
|
61
|
+
"""Load a MuJoCo model from an XML file or a string."""
|
|
62
|
+
try:
|
|
63
|
+
# Convert the XML path to an absolute path
|
|
64
|
+
xml_path = os.path.abspath(xml)
|
|
65
|
+
|
|
66
|
+
# Check if the path exists
|
|
67
|
+
if os.path.exists(xml_path):
|
|
68
|
+
# Extract and validate file extension
|
|
69
|
+
extension = os.path.splitext(xml_path)[1].lower()[1:]
|
|
70
|
+
|
|
71
|
+
if extension == "xml":
|
|
72
|
+
self._load_xml_file(xml_path)
|
|
73
|
+
elif extension == "urdf":
|
|
74
|
+
self._load_urdf_file(xml_path, **kwargs)
|
|
75
|
+
else:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"Unsupported file extension: '{extension}'. Please provide an XML or URDF file."
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
# If the file doesn't exist, assume it's a string and attempt to load it as XML
|
|
81
|
+
if "<mujoco>" in xml or "<robot>" in xml:
|
|
82
|
+
self._load_xml_string(xml)
|
|
83
|
+
else:
|
|
84
|
+
raise FileNotFoundError(
|
|
85
|
+
f"Model file not found: {xml_path}. Ensure the file path is correct and accessible."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
except FileNotFoundError as e:
|
|
89
|
+
raise FileNotFoundError(f"Failed to load the MuJoCo model: {e}") from e
|
|
90
|
+
|
|
91
|
+
except ValueError as e:
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"Invalid value encountered while loading the model: {e}"
|
|
94
|
+
) from e
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
raise Exception(
|
|
98
|
+
f"Unexpected error while loading the MuJoCo model: {e}"
|
|
99
|
+
) from e
|
|
100
|
+
|
|
101
|
+
def _load_xml_file(self, xml: str, **kwargs) -> None:
|
|
102
|
+
"""Load a MuJoCo model from an XML file."""
|
|
103
|
+
self._model = mujoco.MjModel.from_xml_path(xml)
|
|
104
|
+
with open(xml, "r") as f:
|
|
105
|
+
self.xml = f.read()
|
|
106
|
+
template = kwargs.get('template', None)
|
|
107
|
+
if template:
|
|
108
|
+
try:
|
|
109
|
+
self.xml = self.xml.format(**template)
|
|
110
|
+
except KeyError as e:
|
|
111
|
+
raise ValueError(f"Template key error. Ensure the template keys match the placeholders in the XML.") from e
|
|
112
|
+
except Exception as e:
|
|
113
|
+
raise ValueError(f"Error formatting XML with template") from e
|
|
114
|
+
self._model = mujoco.MjModel.from_xml_string(self.xml)
|
|
115
|
+
|
|
116
|
+
def _load_urdf_file(self, urdf_path: str, **kwargs: Any) -> None:
|
|
117
|
+
"""Process and load a URDF file for use with MuJoCo."""
|
|
118
|
+
|
|
119
|
+
def convert_dae_to_stl(meshdir: str) -> None:
|
|
120
|
+
"""Convert all DAE files in a directory (including subdirectories) to STL."""
|
|
121
|
+
if not os.path.exists(meshdir):
|
|
122
|
+
raise FileNotFoundError(f"Directory not found: {meshdir}")
|
|
123
|
+
for filename in os.listdir(meshdir):
|
|
124
|
+
if filename.lower().endswith(".dae"):
|
|
125
|
+
dae_path = os.path.join(meshdir, filename)
|
|
126
|
+
stl_path = os.path.splitext(dae_path)[0] + ".stl"
|
|
127
|
+
try:
|
|
128
|
+
trimesh.load_mesh(dae_path).export(stl_path)
|
|
129
|
+
print(f"Converted: {os.path.basename(dae_path)} -> {os.path.basename(stl_path)}")
|
|
130
|
+
except Exception as e:
|
|
131
|
+
raise ValueError(f"Error converting {filename}") from e
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
from . import VERBOSITY
|
|
135
|
+
robot = ET.parse(urdf_path).getroot()
|
|
136
|
+
mujoco_tag = ET.Element("mujoco")
|
|
137
|
+
|
|
138
|
+
# Get main meshdir (parent directory for meshes)
|
|
139
|
+
meshdir = kwargs.get("meshdir", "meshes/") # Default as tuple
|
|
140
|
+
if not os.path.isabs(meshdir):
|
|
141
|
+
urdf_dir = os.path.dirname(os.path.abspath(urdf_path))
|
|
142
|
+
meshdir = os.path.join(urdf_dir, meshdir)
|
|
143
|
+
|
|
144
|
+
# Ensure meshdir exists
|
|
145
|
+
if not os.path.exists(meshdir):
|
|
146
|
+
raise FileNotFoundError(f"Mesh directory not found: {meshdir}")
|
|
147
|
+
|
|
148
|
+
# If no explicit subdirs are provided, auto-detect subdirectories with STL files
|
|
149
|
+
subdirs = kwargs.get('meshdir_sub', None)
|
|
150
|
+
if not subdirs:
|
|
151
|
+
subdirs = [os.path.relpath(root, meshdir) for root, _, files in os.walk(meshdir) if any(f.endswith('.stl') for f in files)]
|
|
152
|
+
if VERBOSITY:
|
|
153
|
+
print(f"Auto-detected subdirectories: {subdirs}")
|
|
154
|
+
|
|
155
|
+
# Convert relative subdir paths to absolute based on meshdir
|
|
156
|
+
full_meshdirs = [
|
|
157
|
+
os.path.join(meshdir, subdir) if not os.path.isabs(subdir) else subdir
|
|
158
|
+
for subdir in subdirs
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
# Add <compiler> tag with the main meshdir (required for MuJoCo)
|
|
162
|
+
compiler_attrs = {
|
|
163
|
+
"meshdir": meshdir, # Use the main meshdir
|
|
164
|
+
"balanceinertia": kwargs.get("balanceinertia", "true"),
|
|
165
|
+
"discardvisual": "false",
|
|
166
|
+
}
|
|
167
|
+
ET.SubElement(mujoco_tag, "compiler", **compiler_attrs)
|
|
168
|
+
|
|
169
|
+
# Create <asset> tag (required for MuJoCo)
|
|
170
|
+
asset_tag = ET.SubElement(mujoco_tag, "asset")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# Process all valid mesh directories
|
|
174
|
+
for full_meshdir in full_meshdirs:
|
|
175
|
+
if os.path.exists(full_meshdir):
|
|
176
|
+
convert_dae_to_stl(full_meshdir) # Convert DAE to STL in the origin directory
|
|
177
|
+
|
|
178
|
+
# Walk through all files in the directory
|
|
179
|
+
for root, _, files in os.walk(full_meshdir):
|
|
180
|
+
for filename in files:
|
|
181
|
+
if filename.lower().endswith('.stl'):
|
|
182
|
+
relative_dir = os.path.relpath(root, meshdir) # Extract subdir name
|
|
183
|
+
mesh_name = f"{os.path.splitext(filename)[0]}_{relative_dir.replace(os.sep, '_')}" # Format: base_visual
|
|
184
|
+
mesh_file_relative = os.path.join(relative_dir, filename)
|
|
185
|
+
ET.SubElement(asset_tag, "mesh", name=mesh_name, file=mesh_file_relative)
|
|
186
|
+
|
|
187
|
+
robot.insert(0, mujoco_tag)
|
|
188
|
+
self.xml = ET.tostring(robot, encoding="unicode").replace('.dae', '.stl')
|
|
189
|
+
self._model = mujoco.MjModel.from_xml_string(self.xml)
|
|
190
|
+
except Exception as e:
|
|
191
|
+
raise ValueError(f"Failed to process URDF file: {e}")
|
|
192
|
+
|
|
193
|
+
def _load_xml_string(self, xml: str) -> None:
|
|
194
|
+
"""Load a MuJoCo model from an XML string."""
|
|
195
|
+
try:
|
|
196
|
+
self._model = mujoco.MjModel.from_xml_string(xml)
|
|
197
|
+
self.xml = xml
|
|
198
|
+
except Exception as e:
|
|
199
|
+
raise ValueError(f"Failed to load the MuJoCo model from XML string: {e}") from e
|
|
200
|
+
|
|
201
|
+
def __str__(self):
|
|
202
|
+
return self._model.__str__()
|
|
203
|
+
|
|
204
|
+
def __repr__(self):
|
|
205
|
+
return (
|
|
206
|
+
f"{self.__class__.__name__}(\n"
|
|
207
|
+
f" Duration: {self.duration} [fps={self.fps}, ts={self.ts:.0e}]\n"
|
|
208
|
+
f" Gravity: {self.gravity},\n"
|
|
209
|
+
f" Resolution: {self._width}x{self._height}\n"
|
|
210
|
+
f" Bodies ({self.model.nbody}): {', '.join(self._body_names[:5])}{' ...' if len(self._body_names) > 5 else ''}\n"
|
|
211
|
+
f" Joints ({self.model.njnt}): {', '.join(self._joint_names[:5])}{' ...' if len(self._joint_names) > 5 else ''}\n"
|
|
212
|
+
f" Actuators ({self.model.nu}): {', '.join(self._actuator_names[:5])}{' ...' if len(self._actuator_names) > 5 else ''}\n"
|
|
213
|
+
f" Controller: {self.controller.__name__ if self.controller else None}\n" # Returns str name of the function
|
|
214
|
+
f")"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def __enter__(self):
|
|
218
|
+
return self
|
|
219
|
+
|
|
220
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
221
|
+
mujoco.set_mjcb_control(None)
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def model(self) -> mujoco.MjModel:
|
|
226
|
+
"""Read-only property to access the MjModel object."""
|
|
227
|
+
return self._model
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def data(self) -> mujoco.MjData:
|
|
231
|
+
"""Read-only property to access the MjData single-step object."""
|
|
232
|
+
return self._data
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def captured_data(self) -> Dict[str, np.ndarray]:
|
|
236
|
+
"""Read-only property to access the entire captured simulation data."""
|
|
237
|
+
if self._captured_data is None:
|
|
238
|
+
raise ValueError("No simulation data captured yet.")
|
|
239
|
+
return self._captured_data.unwrap()
|
|
240
|
+
|
|
241
|
+
@captured_data.deleter
|
|
242
|
+
def captured_data(self):
|
|
243
|
+
self._captured_data = None
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def frames(self) -> List[np.ndarray]:
|
|
247
|
+
"""Read-only property to access the captured frames."""
|
|
248
|
+
if self._frames is None:
|
|
249
|
+
raise ValueError("No frames captured yet.")
|
|
250
|
+
return self._frames
|
|
251
|
+
|
|
252
|
+
@frames.deleter
|
|
253
|
+
def frames(self):
|
|
254
|
+
self._frames.clear()
|
|
255
|
+
import gc
|
|
256
|
+
|
|
257
|
+
gc.collect()
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def duration(self) -> float:
|
|
261
|
+
return self._duration
|
|
262
|
+
|
|
263
|
+
@duration.setter
|
|
264
|
+
def duration(self, value):
|
|
265
|
+
if value < 0:
|
|
266
|
+
raise ValueError("Duration must be greater than zero.")
|
|
267
|
+
self._duration = value
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def fps(self) -> float:
|
|
271
|
+
return self._fps
|
|
272
|
+
|
|
273
|
+
@fps.setter
|
|
274
|
+
def fps(self, value):
|
|
275
|
+
if value < 0:
|
|
276
|
+
raise ValueError("FPS must be greater than zero.")
|
|
277
|
+
self._fps = value
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def resolution(self) -> Tuple[int, int]:
|
|
281
|
+
return self._resolution
|
|
282
|
+
|
|
283
|
+
@resolution.setter
|
|
284
|
+
def resolution(self, values):
|
|
285
|
+
if len(values) != 2:
|
|
286
|
+
raise ValueError("Resolution must be a tuple of width and height.")
|
|
287
|
+
if values[0] < 1 or values[1] < 1:
|
|
288
|
+
raise ValueError("Resolution must be at least 1x1 pixels.")
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
monitor = get_monitors()[0]
|
|
292
|
+
screen_width, screen_height = monitor.width, monitor.height
|
|
293
|
+
except Exception as e:
|
|
294
|
+
screen_width, screen_height = 1920, 1080
|
|
295
|
+
print_warning(
|
|
296
|
+
f"Failed to get screen resolution: {e}. Defaulting to 1920x1080."
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if values[0] > screen_width or values[1] > screen_height:
|
|
300
|
+
raise ValueError("Resolution must be less than the screen resolution.")
|
|
301
|
+
|
|
302
|
+
self._resolution = tuple(int(value) for value in values)
|
|
303
|
+
self._width, self._height = self._resolution
|
|
304
|
+
# Match changes to the model's visual settings
|
|
305
|
+
self._model.vis.Global.offwidth = self._width
|
|
306
|
+
self._model.vis.Global.offheight = self._height
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def init_conditions(self):
|
|
310
|
+
return self._initcond
|
|
311
|
+
|
|
312
|
+
@init_conditions.setter
|
|
313
|
+
def init_conditions(self, values):
|
|
314
|
+
if not isinstance(values, dict):
|
|
315
|
+
raise ValueError("Initial conditions must be a dictionary.")
|
|
316
|
+
invalid_keys = [
|
|
317
|
+
key for key in values.keys() if not hasattr(mujoco.MjData(self._model), key)
|
|
318
|
+
]
|
|
319
|
+
if invalid_keys:
|
|
320
|
+
valid_keys = SimulationData._get_public_keys(self._data)
|
|
321
|
+
print(f"Valid initial condition attributes: {', '.join(valid_keys)}")
|
|
322
|
+
raise ValueError(
|
|
323
|
+
f"Invalid initial condition attributes: {', '.join(invalid_keys)}"
|
|
324
|
+
)
|
|
325
|
+
self._initcond = values
|
|
326
|
+
|
|
327
|
+
@property
|
|
328
|
+
def controller(self) -> Optional[Callable[[mjModel, mjData, Any], None]]:
|
|
329
|
+
"""Controller Function"""
|
|
330
|
+
return self._controller
|
|
331
|
+
|
|
332
|
+
@controller.setter
|
|
333
|
+
def controller(self, func: Callable[[mjModel, mjData, Any], None]) -> None:
|
|
334
|
+
if func is not None and not callable(func):
|
|
335
|
+
raise ValueError("Controller must be a callable function.")
|
|
336
|
+
self._controller = func
|
|
337
|
+
|
|
338
|
+
@property
|
|
339
|
+
def ts(self) -> float:
|
|
340
|
+
return self._model.opt.timestep
|
|
341
|
+
|
|
342
|
+
@ts.setter
|
|
343
|
+
def ts(self, value) -> None:
|
|
344
|
+
if value <= 0:
|
|
345
|
+
raise ValueError("Timestep must be greater than 0.")
|
|
346
|
+
self._model.opt.timestep = value
|
|
347
|
+
|
|
348
|
+
@property
|
|
349
|
+
def data_rate(self) -> int:
|
|
350
|
+
if self._dr is None:
|
|
351
|
+
raise ValueError(
|
|
352
|
+
f"Use '{self.runSim.__name__}' first in order to access this value."
|
|
353
|
+
)
|
|
354
|
+
return self._dr
|
|
355
|
+
|
|
356
|
+
@data_rate.setter
|
|
357
|
+
def data_rate(self, value) -> None:
|
|
358
|
+
if value.is_numeric() and not isinstance(value, int):
|
|
359
|
+
value = round(value)
|
|
360
|
+
print_warning(f"Data rate must be an integer. Rounding to the nearest integer ({value}).")
|
|
361
|
+
if value <= 0:
|
|
362
|
+
raise ValueError("Data rate must be greater than 0.")
|
|
363
|
+
# TODO: Check the math on this validation
|
|
364
|
+
# max_ = int(self._duration / self.ts)
|
|
365
|
+
# if value > max_:
|
|
366
|
+
# print_warning(f"Data rate exceeds the simulation steps. Setting to the maximum possible value ({max_}).")
|
|
367
|
+
# value = max_
|
|
368
|
+
self._dr = value
|
|
369
|
+
|
|
370
|
+
@property
|
|
371
|
+
def gravity(self):
|
|
372
|
+
return self._model.opt.gravity
|
|
373
|
+
|
|
374
|
+
@gravity.setter
|
|
375
|
+
def gravity(self, values):
|
|
376
|
+
if len(values) != 3:
|
|
377
|
+
raise ValueError("Gravity must be a 3D vector.")
|
|
378
|
+
self._model.opt.gravity = values
|
|
379
|
+
|
|
380
|
+
def _setInitialConditions(self):
|
|
381
|
+
for key, value in self._initcond.items():
|
|
382
|
+
if hasattr(self._data, key):
|
|
383
|
+
setattr(self._data, key, value)
|
|
384
|
+
else:
|
|
385
|
+
print_warning(f"'{key}' is not a valid attribute of MjData.")
|
|
386
|
+
|
|
387
|
+
def _resetSimulation(self):
|
|
388
|
+
mujoco.mj_resetData(self._model, self._data)
|
|
389
|
+
self._setInitialConditions()
|
|
390
|
+
self._frames = []
|
|
391
|
+
self._captured_data = SimulationData()
|
|
392
|
+
|
|
393
|
+
@timer
|
|
394
|
+
def runSim(self, render=False, camera=None, data_rate=100, multi_thread=False):
|
|
395
|
+
"""Run the simulation with optional rendering and controlled data capture.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
render (bool): If True, renders the simulation.
|
|
399
|
+
camera (str): The camera view to render from, defaults to None.
|
|
400
|
+
data_rate (int): How often to capture data, expressed as frames per second.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
self: The current Wrapper object for method chaining.
|
|
404
|
+
"""
|
|
405
|
+
try:
|
|
406
|
+
mujoco.set_mjcb_control(self._controller) if self._controller else None
|
|
407
|
+
self._resetSimulation()
|
|
408
|
+
total_steps = int(self._duration / self.ts)
|
|
409
|
+
|
|
410
|
+
# Cache frequently used functions and objects for performance
|
|
411
|
+
mj_step = mujoco.mj_step
|
|
412
|
+
m = self._model
|
|
413
|
+
d = self._data
|
|
414
|
+
|
|
415
|
+
self._dr = data_rate
|
|
416
|
+
capture_rate = data_rate * self.ts
|
|
417
|
+
capture_interval = max(1, int(1.0 / capture_rate))
|
|
418
|
+
render_interval = max(1, int(1.0 / (self._fps * self.ts)))
|
|
419
|
+
|
|
420
|
+
import __main__ as main
|
|
421
|
+
|
|
422
|
+
if hasattr(main, "__file__"):
|
|
423
|
+
PBar = barTerminal
|
|
424
|
+
else:
|
|
425
|
+
PBar = barNotebook
|
|
426
|
+
|
|
427
|
+
if multi_thread:
|
|
428
|
+
# num_threads = os.cpu_count()
|
|
429
|
+
# TODO: Implement multi-threading
|
|
430
|
+
print(
|
|
431
|
+
"Multi-threading not yet implemented. Running simulation in single-thread mode."
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
with (
|
|
435
|
+
PBar(
|
|
436
|
+
total=total_steps, desc="Simulation", unit=" step", leave=False
|
|
437
|
+
) as pbar,
|
|
438
|
+
mujoco.Renderer(self._model, self._height, self._width) as renderer,
|
|
439
|
+
):
|
|
440
|
+
step = 0
|
|
441
|
+
while d.time < self._duration:
|
|
442
|
+
mj_step(m, d)
|
|
443
|
+
|
|
444
|
+
# Capture data at the specified rate
|
|
445
|
+
if step % capture_interval == 0:
|
|
446
|
+
self._captured_data.capture(d)
|
|
447
|
+
|
|
448
|
+
if render and step % render_interval == 0:
|
|
449
|
+
(
|
|
450
|
+
renderer.update_scene(d)
|
|
451
|
+
if camera is None
|
|
452
|
+
else renderer.update_scene(d, camera)
|
|
453
|
+
)
|
|
454
|
+
self._frames.append(renderer.render().copy())
|
|
455
|
+
|
|
456
|
+
pbar.update(1)
|
|
457
|
+
step += 1
|
|
458
|
+
|
|
459
|
+
# if verbose:
|
|
460
|
+
# for warning in mujoco.mjtWarning:
|
|
461
|
+
# if d.warning[warning].number > 0:
|
|
462
|
+
# print_warning(f"{warning.name} - {d.warning[warning].number} occurrences")
|
|
463
|
+
# else:
|
|
464
|
+
# if any(d.warning[warning].number > 0 for warning in mujoco.mjtWarning):
|
|
465
|
+
# print_warning("Please check MUJOCO_LOG.txt for more details.")
|
|
466
|
+
|
|
467
|
+
except Exception as e:
|
|
468
|
+
print(f"Simulation error: {e}")
|
|
469
|
+
finally:
|
|
470
|
+
mujoco.set_mjcb_control(None)
|
|
471
|
+
|
|
472
|
+
return self
|
|
473
|
+
|
|
474
|
+
def renderFrame(self, t=0, frame=0, title=None) -> Optional[str]:
|
|
475
|
+
"""Render a specific frame as an image.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
t (float): Time in seconds for which the frame should be rendered.
|
|
479
|
+
frame (int): The frame index to render.
|
|
480
|
+
title (str): The title of the rendered frame.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
None
|
|
484
|
+
"""
|
|
485
|
+
if not self._frames:
|
|
486
|
+
raise ValueError("No frames captured to render.")
|
|
487
|
+
|
|
488
|
+
if t < 0 or frame < 0:
|
|
489
|
+
raise ValueError(
|
|
490
|
+
"Time and frame index must be greater than or equal to zero."
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
if frame and t:
|
|
494
|
+
raise ValueError("Can only specify singular time or frame parameter")
|
|
495
|
+
|
|
496
|
+
try:
|
|
497
|
+
if t > 0:
|
|
498
|
+
frame = self.t2f(t) # Convert time to frame index
|
|
499
|
+
else:
|
|
500
|
+
frame = int(frame)
|
|
501
|
+
|
|
502
|
+
plt.imshow(self._frames[frame])
|
|
503
|
+
plt.axis("off")
|
|
504
|
+
plt.title(title or f"Frame {frame}", loc="center")
|
|
505
|
+
plt.show()
|
|
506
|
+
|
|
507
|
+
except IndexError as e:
|
|
508
|
+
raise ValueError(f"Invalid frame index: {e}")
|
|
509
|
+
except TypeError as e:
|
|
510
|
+
raise ValueError(f"Invalid type for time or frame: {e}")
|
|
511
|
+
except Exception as e:
|
|
512
|
+
raise RuntimeError(f"Unexpected error while rendering frame: {e}")
|
|
513
|
+
|
|
514
|
+
def renderMedia(self, codec="gif", title=None, save=False) -> media:
|
|
515
|
+
"""Render the simulation as a video or GIF, with an option to save to a file.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
codec (str): The media format to use ("gif" or "mp4").
|
|
519
|
+
title (str): The filename or window title for the media.
|
|
520
|
+
save (bool): Whether to save the media to a file.
|
|
521
|
+
"""
|
|
522
|
+
if not self._frames:
|
|
523
|
+
raise ValueError("No frames captured to create media.")
|
|
524
|
+
# Display the media in a window
|
|
525
|
+
if not save:
|
|
526
|
+
media.show_video(
|
|
527
|
+
self._frames,
|
|
528
|
+
fps=self._fps,
|
|
529
|
+
width=self._width,
|
|
530
|
+
height=self._height,
|
|
531
|
+
codec=codec,
|
|
532
|
+
title=title,
|
|
533
|
+
)
|
|
534
|
+
return
|
|
535
|
+
|
|
536
|
+
if not title.endswith(f".{codec}"):
|
|
537
|
+
title += f".{codec}"
|
|
538
|
+
|
|
539
|
+
# Save the frames to the specified file
|
|
540
|
+
available_codecs = ["gif", "mp4", "h264", "hevc", "vp9"]
|
|
541
|
+
if codec in available_codecs:
|
|
542
|
+
media.write_video(
|
|
543
|
+
title if not None else "render",
|
|
544
|
+
self._frames,
|
|
545
|
+
fps=self._fps,
|
|
546
|
+
codec=codec,
|
|
547
|
+
)
|
|
548
|
+
else:
|
|
549
|
+
raise ValueError(
|
|
550
|
+
f"Unsupported codec '{codec}'. Supported codecs are {', '.join(available_codecs)}"
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
path = os.path.abspath(title)
|
|
554
|
+
print(f"Media saved to {path}")
|
|
555
|
+
return path
|
|
556
|
+
|
|
557
|
+
@lru_cache(maxsize=100)
|
|
558
|
+
def t2f(self, t: float) -> int:
|
|
559
|
+
"""Convert time to frame index."""
|
|
560
|
+
return min(
|
|
561
|
+
int(t * self._fps), int(self._duration * self._fps) - 1
|
|
562
|
+
) # Subtract 1 to convert to 0-based index
|
|
563
|
+
|
|
564
|
+
@lru_cache(maxsize=100)
|
|
565
|
+
def f2t(self, frame: int) -> float:
|
|
566
|
+
"""Convert frame index to time."""
|
|
567
|
+
return frame / self._fps
|
|
568
|
+
|
|
569
|
+
def getBodyData(self, body_name: str, data_name: str = None) -> np.ndarray:
|
|
570
|
+
"""Get the data for a specific body in the simulation.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
body_name (str): The name of the body to retrieve data for.
|
|
574
|
+
data_name (str): The name of the data to retrieve.
|
|
575
|
+
|
|
576
|
+
Returns:
|
|
577
|
+
np.ndarray: The data for the specified body.
|
|
578
|
+
"""
|
|
579
|
+
if body_name not in self._body_names:
|
|
580
|
+
raise ValueError(f"Body '{body_name}' not found in the model.")
|
|
581
|
+
else:
|
|
582
|
+
body_id = mujoco.mj_name2id(
|
|
583
|
+
self._model, mujoco.mjtObj.mjOBJ_BODY, body_name
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
if data_name is None:
|
|
587
|
+
return self._captured_data.unwrap()[body_id]
|
|
588
|
+
if data_name not in self._captured_data.unwrap():
|
|
589
|
+
raise ValueError(f"Data '{data_name}' not found for body '{body_name}'.")
|
|
590
|
+
return self._captured_data.unwrap()[body_id][data_name]
|
|
591
|
+
|
|
592
|
+
def getID(self, id: int) -> str:
|
|
593
|
+
"""Get the name of a body given its ID.
|
|
594
|
+
|
|
595
|
+
Args:
|
|
596
|
+
id (int): The ID of the body.
|
|
597
|
+
|
|
598
|
+
Returns:
|
|
599
|
+
str: The name of the body.
|
|
600
|
+
"""
|
|
601
|
+
if id < 0 or id >= self._model.nbody:
|
|
602
|
+
raise ValueError(f"Invalid body ID: {id}")
|
|
603
|
+
return mujoco.mj_id2name(self._model, mujoco.mjtObj.mjOBJ_BODY, id)
|
|
604
|
+
|
|
605
|
+
def saveYAML(self, name="Model"):
|
|
606
|
+
"""Save simulation data to a YAML file.
|
|
607
|
+
|
|
608
|
+
Args:
|
|
609
|
+
name (str): The filename for the YAML file.
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
None
|
|
613
|
+
"""
|
|
614
|
+
if not name.endswith(".yml"):
|
|
615
|
+
name += ".yml"
|
|
616
|
+
|
|
617
|
+
try:
|
|
618
|
+
# Convert simData's NumPy arrays or lists to a YAML-friendly format
|
|
619
|
+
serialized_data = {
|
|
620
|
+
k: (v.tolist() if isinstance(v, np.ndarray) else v)
|
|
621
|
+
for k, v in self.captured_data.items()
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
with open(name, "w") as f:
|
|
625
|
+
yaml.dump(serialized_data, f, default_flow_style=False)
|
|
626
|
+
|
|
627
|
+
print(f"Simulation data saved to {name}")
|
|
628
|
+
except Exception as e:
|
|
629
|
+
print(f"Failed to save data to YAML: {e}")
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
class SimulationData(object):
|
|
633
|
+
"""A class to store and manage simulation data."""
|
|
634
|
+
|
|
635
|
+
__slots__ = ['_d']
|
|
636
|
+
|
|
637
|
+
def __init__(self):
|
|
638
|
+
self._d = defaultdict(list)
|
|
639
|
+
|
|
640
|
+
def capture(self, mj_data: mujoco.MjData):
|
|
641
|
+
"""Capture data from MjData object, storing specified or all public simulation data."""
|
|
642
|
+
from . import CAPTURE_PARAMETERS, VERBOSITY
|
|
643
|
+
keys = self._get_public_keys(mj_data) if CAPTURE_PARAMETERS == "all" else CAPTURE_PARAMETERS
|
|
644
|
+
|
|
645
|
+
for key in keys:
|
|
646
|
+
try:
|
|
647
|
+
value = getattr(mj_data, key)
|
|
648
|
+
# Check if value is a numpy array and copy if needed
|
|
649
|
+
if isinstance(value, np.ndarray):
|
|
650
|
+
self._d[key].append(value.copy())
|
|
651
|
+
elif np.isscalar(value):
|
|
652
|
+
self._d[key].append(value)
|
|
653
|
+
elif hasattr(value, "copy") and callable(value.copy):
|
|
654
|
+
# Copy if it has a copy method (e.g., MuJoCo's MjArray)
|
|
655
|
+
self._d[key].append(value.copy())
|
|
656
|
+
else:
|
|
657
|
+
self._d[key].append(value)
|
|
658
|
+
except AttributeError:
|
|
659
|
+
print_warning(f"Key '{key}' not found in MjData. Skipping.") if VERBOSITY else None
|
|
660
|
+
except Exception as e:
|
|
661
|
+
print(f"An error occurred while capturing '{key}': {e}") if VERBOSITY else None
|
|
662
|
+
|
|
663
|
+
def unwrap(self) -> Dict[str, np.ndarray]:
|
|
664
|
+
"""Unwrap the captured simulation data into a structured format with NumPy arrays."""
|
|
665
|
+
unwrapped_data = {}
|
|
666
|
+
|
|
667
|
+
for key, value_list in self._d.items():
|
|
668
|
+
if not value_list: # Skip empty lists
|
|
669
|
+
unwrapped_data[key] = np.array([])
|
|
670
|
+
continue
|
|
671
|
+
|
|
672
|
+
try:
|
|
673
|
+
# Check if all items in the list have the same shape for array data
|
|
674
|
+
if isinstance(value_list[0], np.ndarray):
|
|
675
|
+
# Check if arrays have consistent shapes
|
|
676
|
+
shapes = [arr.shape for arr in value_list]
|
|
677
|
+
if all(shape == shapes[0] for shape in shapes):
|
|
678
|
+
unwrapped_data[key] = np.stack(value_list)
|
|
679
|
+
else:
|
|
680
|
+
# For arrays with different shapes, keep as a list
|
|
681
|
+
unwrapped_data[key] = value_list
|
|
682
|
+
else:
|
|
683
|
+
# Convert to a NumPy array if it's a list of scalars
|
|
684
|
+
unwrapped_data[key] = np.array(value_list)
|
|
685
|
+
except ValueError as e:
|
|
686
|
+
print(f"ValueError while unwrapping key '{key}': {e}. Keeping as list.")
|
|
687
|
+
unwrapped_data[key] = value_list # Store as a list if conversion fails
|
|
688
|
+
except Exception as e:
|
|
689
|
+
print(f"Error processing key '{key}': {e}")
|
|
690
|
+
unwrapped_data[key] = value_list
|
|
691
|
+
|
|
692
|
+
return unwrapped_data
|
|
693
|
+
|
|
694
|
+
@property
|
|
695
|
+
def shape(self):
|
|
696
|
+
"""TODO: Implement a method to return the shape of the captured data."""
|
|
697
|
+
if not self._d:
|
|
698
|
+
return None
|
|
699
|
+
|
|
700
|
+
self.unwrap() # Ensure data is unwrapped before checking shape
|
|
701
|
+
|
|
702
|
+
for key, value in self._d.items():
|
|
703
|
+
try:
|
|
704
|
+
if isinstance(value, np.ndarray):
|
|
705
|
+
print(f"{key}: {value.shape}")
|
|
706
|
+
elif isinstance(value, list):
|
|
707
|
+
print(f"{key}: {len(value)}")
|
|
708
|
+
elif isinstance(value, dict):
|
|
709
|
+
print(f"{key}: {len(value)}")
|
|
710
|
+
else: # Handle any other types
|
|
711
|
+
print(f"{key}: {type(value)}")
|
|
712
|
+
|
|
713
|
+
except Exception:
|
|
714
|
+
pass
|
|
715
|
+
|
|
716
|
+
def __del__(self):
|
|
717
|
+
self._d.clear()
|
|
718
|
+
import gc
|
|
719
|
+
|
|
720
|
+
gc.collect()
|
|
721
|
+
|
|
722
|
+
def __len__(self):
|
|
723
|
+
if not self._d:
|
|
724
|
+
return 0
|
|
725
|
+
# Return the length of one of the data lists (assuming all have same length)
|
|
726
|
+
return len(next(iter(self._d.values())))
|
|
727
|
+
|
|
728
|
+
def __str__(self):
|
|
729
|
+
return f"{self.__class__.__name__}({len(self)} Step(s) Captured)"
|
|
730
|
+
|
|
731
|
+
def __repr__(self):
|
|
732
|
+
return self.__str__()
|
|
733
|
+
|
|
734
|
+
@staticmethod
|
|
735
|
+
def _get_public_keys(obj):
|
|
736
|
+
"""Get all public keys of an object."""
|
|
737
|
+
return [
|
|
738
|
+
name
|
|
739
|
+
for name in dir(obj)
|
|
740
|
+
if not name.startswith("_") and not callable(getattr(obj, name))
|
|
741
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mujoco Toolbox
|
|
3
|
+
==============
|
|
4
|
+
A toolbox for working with MuJoCo simulations. This package provides various utilities and controllers to facilitate
|
|
5
|
+
the simulation process.
|
|
6
|
+
|
|
7
|
+
Modules:
|
|
8
|
+
--------
|
|
9
|
+
- Wrapper: Contains the Wrapper class for interfacing with MuJoCo.
|
|
10
|
+
- Controller: Includes sineController, cosineController, and randomController for controlling simulations.
|
|
11
|
+
|
|
12
|
+
Constants:
|
|
13
|
+
----------
|
|
14
|
+
- CAPTURE_PARAMETERS: List of MjData fields to capture during simulation.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
-----------
|
|
18
|
+
- __version__ (str): The current version of the package.
|
|
19
|
+
- __author__ (str): The author of the package.
|
|
20
|
+
- __all__ (list): List of public objects of the module, as interpreted by `from module import *`.
|
|
21
|
+
|
|
22
|
+
Notes:
|
|
23
|
+
------
|
|
24
|
+
This package is still under development. Report any issues to https://github.com/MGross21/mujoco-toolbox/issues.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from .Controller import (cosineController, randomController, sineController,
|
|
28
|
+
stepController)
|
|
29
|
+
from .Utils import print_warning
|
|
30
|
+
from .Wrapper import Wrapper
|
|
31
|
+
|
|
32
|
+
__version__ = "0.1.8"
|
|
33
|
+
__author__ = "Michael Gross"
|
|
34
|
+
__github_repo__ = "mujoco-toolbox"
|
|
35
|
+
__license__ = "MIT"
|
|
36
|
+
__status__ = "Development"
|
|
37
|
+
|
|
38
|
+
# `from mujoco_toolbox import *` will import these objects
|
|
39
|
+
__all__ = [
|
|
40
|
+
"Wrapper",
|
|
41
|
+
"sineController",
|
|
42
|
+
"cosineController",
|
|
43
|
+
"randomController",
|
|
44
|
+
"stepController",
|
|
45
|
+
"timer",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
CAPTURE_PARAMETERS = [
|
|
49
|
+
"time",
|
|
50
|
+
"qpos",
|
|
51
|
+
"qvel",
|
|
52
|
+
"act",
|
|
53
|
+
"qacc",
|
|
54
|
+
"xpos",
|
|
55
|
+
"xquat",
|
|
56
|
+
"xmat",
|
|
57
|
+
"ctrl",
|
|
58
|
+
"sensordata",
|
|
59
|
+
] # MjData default fields to capture during simulation
|
|
60
|
+
|
|
61
|
+
VERBOSITY = False
|
|
62
|
+
|
|
63
|
+
if True:
|
|
64
|
+
print_warning(
|
|
65
|
+
f"{__package__} ({__version__}) is still under development.",
|
|
66
|
+
f"Report any issues to https://github.com/MGross21/{__github_repo__}/issues\n",
|
|
67
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Michael Gross
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: mujoco-toolbox
|
|
3
|
+
Version: 0.1.8
|
|
4
|
+
Summary: A modern package to handle MuJoCo environments
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: MuJoCo,robotics,simulation,toolbox,wrapper
|
|
7
|
+
Author: MGross21
|
|
8
|
+
Author-email: MGross21@TODO.com
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: ffmpeg
|
|
17
|
+
Requires-Dist: ipywidgets
|
|
18
|
+
Requires-Dist: matplotlib
|
|
19
|
+
Requires-Dist: mediapy
|
|
20
|
+
Requires-Dist: mujoco
|
|
21
|
+
Requires-Dist: mypy ; extra == "dev"
|
|
22
|
+
Requires-Dist: numpy
|
|
23
|
+
Requires-Dist: pytest ; extra == "dev"
|
|
24
|
+
Requires-Dist: pyyaml
|
|
25
|
+
Requires-Dist: ruff ; extra == "dev"
|
|
26
|
+
Requires-Dist: scipy
|
|
27
|
+
Requires-Dist: screeninfo
|
|
28
|
+
Requires-Dist: tqdm
|
|
29
|
+
Requires-Dist: trimesh
|
|
30
|
+
Project-URL: Documentation, https://github.com/MGross21/mujoco-toolbox
|
|
31
|
+
Project-URL: Homepage, https://github.com/MGross21/mujoco-toolbox
|
|
32
|
+
Project-URL: Repository, https://github.com/MGross21/mujoco-toolbox
|
|
33
|
+
Project-URL: bug-tracker, https://github.com/MGross21/mujoco-toolbox/issues
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+

|
|
38
|
+

|
|
39
|
+
[](https://github.com/MGross21/mujoco-toolbox/actions/workflows/publish.yml)
|
|
40
|
+
|
|
41
|
+
# Mujoco Toolbox
|
|
42
|
+
|
|
43
|
+
Streamlines the MuJoCo Physics Simulator
|
|
44
|
+
|
|
45
|
+
## PyPI Package
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install -U mujoco-toolbox
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## For Local Install
|
|
52
|
+
|
|
53
|
+
*Navigate to the Directory that you wish to place the package*
|
|
54
|
+
|
|
55
|
+
<details>
|
|
56
|
+
<summary><b>JUPYTER NOTEBOOK</b></summary>
|
|
57
|
+
</br>
|
|
58
|
+
*Place Commands Inside Cell*
|
|
59
|
+
|
|
60
|
+
One-Time Install:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
!git clone https://github.com/MGross21/mujoco-toolbox
|
|
64
|
+
!pip install -U ./mujoco-toolbox
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Check for Updates:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
!git pull origin main
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Install/Check for Updates:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
folder = "mujoco-toolbox"
|
|
77
|
+
repo_url = f"https://github.com/MGross21/{folder}"
|
|
78
|
+
|
|
79
|
+
!if [ -d "{folder}" ]; then cd {folder} && git pull && cd ..; else git clone {repo_url}; fi
|
|
80
|
+
!pip install -U ./{folder}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
</details>
|
|
84
|
+
</br>
|
|
85
|
+
|
|
86
|
+
<details>
|
|
87
|
+
<summary><b>BASH WINDOW</b></summary>
|
|
88
|
+
</br>
|
|
89
|
+
One-Time Install:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
git clone https://github.com/MGross21/mujoco-toolbox
|
|
93
|
+
pip install -U ./mujoco-toolbox
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Check for Updates:
|
|
97
|
+
```bash
|
|
98
|
+
git pull origin main
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Install/Check for Updates:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
REPO_DIR="mujoco-toolbox"
|
|
105
|
+
REPO_URL="https://github.com/MGross21/mujoco-toolbox"
|
|
106
|
+
|
|
107
|
+
if [ -d "$REPO_DIR" ]; then
|
|
108
|
+
cd "$REPO_DIR" && git fetch origin && git pull origin main
|
|
109
|
+
else
|
|
110
|
+
git clone $REPO_URL
|
|
111
|
+
cd "$REPO_DIR"
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
pip install -U ./mujoco-toolbox
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
</details>
|
|
118
|
+
|
|
119
|
+
## Extra Packages
|
|
120
|
+
|
|
121
|
+
<details>
|
|
122
|
+
<summary><b>FFMPEG</b></summary>
|
|
123
|
+
|
|
124
|
+
</br>
|
|
125
|
+
|
|
126
|
+
*Required for [mediapy](https://google.github.io/mediapy/mediapy.html) dependency*
|
|
127
|
+
|
|
128
|
+
**Windows**
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
winget install ffmpeg
|
|
132
|
+
ffmpeg -version
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Linux**
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
sudo apt update && sudo apt install ffmpeg
|
|
139
|
+
ffmpeg -version
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**MacOS**
|
|
143
|
+
|
|
144
|
+
*Using Homebrew*
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
brew install ffmpeg
|
|
148
|
+
ffmpeg -version
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
*Using MacPorts*
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
sudo port install ffmpeg
|
|
155
|
+
ffmpeg -version
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
</details>
|
|
159
|
+
|
|
160
|
+
## To Run in Scripts
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
import mujoco_toolbox as mjtb
|
|
164
|
+
from mujoco_toolbox import Wrapper
|
|
165
|
+
|
|
166
|
+
# Optional Global Variables
|
|
167
|
+
mjtb.VERBOSITY = True # Additional Performance and Debugging Print Statements (Default=False)
|
|
168
|
+
mjtb.CAPTURE_PARAMETERS = "all" # MjData Simulation Parameters (Default = ['time', 'qpos', 'qvel', 'act', 'qacc', 'xpos', 'xquat', 'xmat', 'ctrl', 'sensordata'])
|
|
169
|
+
|
|
170
|
+
# Example Code
|
|
171
|
+
Wrapper("path/to/your/xml").runSim(render=True).renderMedia()
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Pre-Made Controllers
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
import mujoco_toolbox as mjtb
|
|
178
|
+
|
|
179
|
+
mjtb.sineController(m,d,**kwargs)
|
|
180
|
+
mjtb.cosineController(m,d,**kwargs)
|
|
181
|
+
mjtb.stepController(m,d,**kwargs)
|
|
182
|
+
mjtb.randomController(m,d,**kwargs)
|
|
183
|
+
|
|
184
|
+
# Wrapper can use custom controllers as well!
|
|
185
|
+
```
|
|
186
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mujoco_toolbox/__init__.py,sha256=5F4JYO_gNSHdvBeu3F8cIr5V_3SeCKmtoMFZOcNswAY,1818
|
|
2
|
+
mujoco_toolbox/Controller.py,sha256=-q4qt43IWw0wz7TT4FnL6omLX-W7166rAdltTWSOflw,4659
|
|
3
|
+
mujoco_toolbox/Utils.py,sha256=eDGJ973ItipwA-qgrWpKTEJke1-Xx4oc3asCXQp25kc,1900
|
|
4
|
+
mujoco_toolbox/Wrapper.py,sha256=UmSq9vzaI96C2gBXcbshMNviS4y3_a7he168WRpQOwc,29050
|
|
5
|
+
mujoco_toolbox-0.1.8.dist-info/LICENSE,sha256=Fh6x3ZPSwg6wLoA0jW1U0U4cM5tAFm2hi1JMypVc1V8,1089
|
|
6
|
+
mujoco_toolbox-0.1.8.dist-info/METADATA,sha256=pyNDcZtroQGNrOr_-v8gtUryVg37LIy8HQgAs1yDsM8,4071
|
|
7
|
+
mujoco_toolbox-0.1.8.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
|
8
|
+
mujoco_toolbox-0.1.8.dist-info/RECORD,,
|