mujoco-toolbox 0.3.4__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/__init__.py +77 -0
- mujoco_toolbox/assets.py +29 -0
- mujoco_toolbox/builder.py +229 -0
- mujoco_toolbox/controller.py +164 -0
- mujoco_toolbox/utils.py +134 -0
- mujoco_toolbox/wrapper.py +893 -0
- mujoco_toolbox-0.3.4.dist-info/LICENSE +21 -0
- mujoco_toolbox-0.3.4.dist-info/METADATA +150 -0
- mujoco_toolbox-0.3.4.dist-info/RECORD +10 -0
- mujoco_toolbox-0.3.4.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Mujoco Toolbox.
|
|
2
|
+
==============
|
|
3
|
+
A toolbox for working with MuJoCo simulations. This package provides various utilities and controllers to facilitate
|
|
4
|
+
the simulation process.
|
|
5
|
+
|
|
6
|
+
Modules
|
|
7
|
+
--------
|
|
8
|
+
- `Wrapper`: Contains the Wrapper class for interfacing with MuJoCo.
|
|
9
|
+
- `Controller`: Includes sineController, cosineController, and randomController for controlling simulations.
|
|
10
|
+
- `Builder`: Contains the Builder class for creating and merging MuJoCo models.
|
|
11
|
+
- `assets`: Contains pre-defined assets for building MuJoCo models.
|
|
12
|
+
|
|
13
|
+
Constants
|
|
14
|
+
----------
|
|
15
|
+
- CAPTURE_PARAMETERS: List of MjData fields to capture during simulation.
|
|
16
|
+
|
|
17
|
+
Notes
|
|
18
|
+
-----
|
|
19
|
+
This package is still under development. Report any issues to https://github.com/MGross21/mujoco-toolbox/issues.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from .assets import WORLD_ASSETS, glovebox
|
|
24
|
+
from .builder import Builder
|
|
25
|
+
from .controller import (
|
|
26
|
+
cosine_controller,
|
|
27
|
+
live_controller,
|
|
28
|
+
random_controller,
|
|
29
|
+
sine_controller,
|
|
30
|
+
step_controller,
|
|
31
|
+
)
|
|
32
|
+
from .utils import _Platform
|
|
33
|
+
from .wrapper import Wrapper
|
|
34
|
+
|
|
35
|
+
__version__ = "0.3.4"
|
|
36
|
+
__author__ = "Michael Gross"
|
|
37
|
+
__github_repo__ = "mujoco-toolbox"
|
|
38
|
+
__license__ = "MIT"
|
|
39
|
+
__status__ = "Development"
|
|
40
|
+
__all__ = [
|
|
41
|
+
"CAPTURE_PARAMETERS",
|
|
42
|
+
"COMPUTER",
|
|
43
|
+
"WORLD_ASSETS",
|
|
44
|
+
"Builder",
|
|
45
|
+
"Wrapper",
|
|
46
|
+
"cosine_controller",
|
|
47
|
+
"glovebox",
|
|
48
|
+
"live_controller",
|
|
49
|
+
"random_controller",
|
|
50
|
+
"sine_controller",
|
|
51
|
+
"step_controller",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
COMPUTER = _Platform() # Singleton Instance
|
|
55
|
+
MAX_GEOM_SCALAR: int = 2 # Scalar value for mujoco.Renderer.max_geom
|
|
56
|
+
PROGRESS_BAR: bool = False # Enable/Disable progress bar
|
|
57
|
+
CAPTURE_PARAMETERS = [ # MjData default fields to capture during simulation
|
|
58
|
+
"time",
|
|
59
|
+
"qpos",
|
|
60
|
+
"qvel",
|
|
61
|
+
"act",
|
|
62
|
+
"qacc",
|
|
63
|
+
"xpos",
|
|
64
|
+
"xquat",
|
|
65
|
+
"xmat",
|
|
66
|
+
"ctrl",
|
|
67
|
+
"sensordata",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
if __version__.startswith("0"):
|
|
71
|
+
from .utils import _print_warning
|
|
72
|
+
_print_warning(
|
|
73
|
+
f"{__package__} (v{__version__}) is still under development.",
|
|
74
|
+
f"Report any issues to https://github.com/MGross21/{__github_repo__}/issues",
|
|
75
|
+
)
|
|
76
|
+
del _print_warning
|
|
77
|
+
del _Platform
|
mujoco_toolbox/assets.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
WORLD_ASSETS = """
|
|
2
|
+
<asset>
|
|
3
|
+
<texture type="skybox" builtin="gradient" rgb1=".3 .5 .7" rgb2="0 0 0" width="32" height="512"/>
|
|
4
|
+
<texture name="body" type="cube" builtin="flat" mark="cross" width="128" height="128" rgb1="0.8 0.6 0.4" rgb2="0.8 0.6 0.4" markrgb="1 1 1"/>
|
|
5
|
+
<material name="body" texture="body" texuniform="true" rgba="0.8 0.6 .4 1"/>
|
|
6
|
+
<texture name="grid" type="2d" builtin="checker" width="512" height="512" rgb1=".1 .2 .3" rgb2=".2 .3 .4"/>
|
|
7
|
+
<material name="grid" texture="grid" texrepeat="1 1" texuniform="true" reflectance=".2"/>
|
|
8
|
+
<texture name="background" type="2d" builtin="flat" width="256" height="256" rgb1="1 1 1"/>
|
|
9
|
+
<material name="background" texture="background" texuniform="true" rgba="1 1 1 1"/>
|
|
10
|
+
</asset>
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def glovebox(width=1.25, depth=0.75, height=1.0, glass_thickness=0.05) -> str:
|
|
14
|
+
return f"""
|
|
15
|
+
<mujoco>
|
|
16
|
+
<asset>
|
|
17
|
+
<material name="glass" rgba="1 1 1 0.2"/>
|
|
18
|
+
</asset>
|
|
19
|
+
<worldbody>
|
|
20
|
+
<body name="walls" pos="0 0 0">
|
|
21
|
+
<geom type="box" size="{width/2} {glass_thickness/2} {height/2}" material="glass" pos="0 {depth/2+glass_thickness/2} {height/2}"/>
|
|
22
|
+
<geom type="box" size="{width/2} {glass_thickness/2} {height/2}" material="glass" pos="0 {-depth/2-glass_thickness/2} {height/2}"/>
|
|
23
|
+
<geom type="box" size="{glass_thickness/2} {depth/2} {height/2}" material="glass" pos="{width/2-glass_thickness/2} 0 {height/2}"/>
|
|
24
|
+
<geom type="box" size="{glass_thickness/2} {depth/2} {height/2}" material="glass" pos="{-width/2+glass_thickness/2} 0 {height/2}"/>
|
|
25
|
+
<geom type="plane" size="{width/2} {depth/2+glass_thickness} {glass_thickness/2}" material="glass" pos="0 0 {height}"/>
|
|
26
|
+
</body>
|
|
27
|
+
</worldbody>
|
|
28
|
+
</mujoco>
|
|
29
|
+
"""
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import xml.etree.ElementTree as ET
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Builder:
|
|
6
|
+
"""Builder class for loading, merging, and saving MuJoCo models from XML strings or file paths.
|
|
7
|
+
|
|
8
|
+
Attributes:
|
|
9
|
+
xml (str): XML string of the model.
|
|
10
|
+
|
|
11
|
+
Examples:
|
|
12
|
+
>>> # Load a model
|
|
13
|
+
>>> builder1 = Builder("path/to/model1.xml")
|
|
14
|
+
>>> # Merge two models from two object instances
|
|
15
|
+
>>> builder2 = Builder("path/to/model2.xml")
|
|
16
|
+
>>> merged_builder = builder1 + builder2
|
|
17
|
+
>>> merged_builder.save("path/to/merged_model.xml")
|
|
18
|
+
>>> # Merge two models from two file paths
|
|
19
|
+
>>> builder = Builder("path/to/model1.xml", "path/to/model2.xml")
|
|
20
|
+
>>> builder.save("path/to/merged_model.xml")
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, *args: str) -> None:
|
|
25
|
+
"""Initialize with an XML string or a file path. Input is required."""
|
|
26
|
+
for arg in args:
|
|
27
|
+
if not isinstance(arg, str):
|
|
28
|
+
msg = "Input must be an XML string or a file path"
|
|
29
|
+
raise TypeError(msg)
|
|
30
|
+
if len(args) == 0:
|
|
31
|
+
msg = "Input is required to initialize the Builder"
|
|
32
|
+
raise ValueError(msg)
|
|
33
|
+
if len(args) > 1:
|
|
34
|
+
# Create a new Builder instance for each additional argument and merge them
|
|
35
|
+
self.tree, self.root = self._load_model(args[0])
|
|
36
|
+
for additional_arg in args[1:]:
|
|
37
|
+
additional_builder = Builder(additional_arg)
|
|
38
|
+
self.__add__(additional_builder)
|
|
39
|
+
else:
|
|
40
|
+
xml_input = args[0]
|
|
41
|
+
self.tree, self.root = self._load_model(xml_input)
|
|
42
|
+
|
|
43
|
+
def _load_model(self, xml_input):
|
|
44
|
+
"""Load a MuJoCo model from a file or XML string."""
|
|
45
|
+
if not isinstance(xml_input, str):
|
|
46
|
+
msg = "Input must be either an XML string or a file path"
|
|
47
|
+
raise ValueError(msg)
|
|
48
|
+
|
|
49
|
+
# Check if input is an XML string
|
|
50
|
+
if xml_input.strip().startswith("<"):
|
|
51
|
+
# Try parsing as XML string
|
|
52
|
+
try:
|
|
53
|
+
root = ET.fromstring(xml_input)
|
|
54
|
+
tree = ET.ElementTree(root)
|
|
55
|
+
|
|
56
|
+
# If it's a URDF (robot tag), wrap it in mujoco
|
|
57
|
+
if root.tag == "robot":
|
|
58
|
+
return self._wrap_urdf_in_mujoco(tree)
|
|
59
|
+
|
|
60
|
+
# If root is not mujoco, wrap it
|
|
61
|
+
if root.tag != "mujoco":
|
|
62
|
+
mujoco_elem = ET.Element("mujoco")
|
|
63
|
+
mujoco_elem.append(root)
|
|
64
|
+
tree = ET.ElementTree(mujoco_elem)
|
|
65
|
+
|
|
66
|
+
return tree, tree.getroot()
|
|
67
|
+
except ET.ParseError:
|
|
68
|
+
msg = "Invalid XML string provided"
|
|
69
|
+
raise ValueError(msg)
|
|
70
|
+
else:
|
|
71
|
+
# Treat input as file path
|
|
72
|
+
if not os.path.exists(xml_input):
|
|
73
|
+
msg = f"File not found: {xml_input}"
|
|
74
|
+
raise FileNotFoundError(msg)
|
|
75
|
+
|
|
76
|
+
# Handle URDF files
|
|
77
|
+
if xml_input.lower().endswith(".urdf"):
|
|
78
|
+
return self._wrap_urdf_in_mujoco(ET.parse(xml_input))
|
|
79
|
+
|
|
80
|
+
# Parse regular XML file
|
|
81
|
+
try:
|
|
82
|
+
tree = ET.parse(xml_input)
|
|
83
|
+
root = tree.getroot()
|
|
84
|
+
|
|
85
|
+
# If root is not mujoco, wrap it
|
|
86
|
+
if root.tag != "mujoco":
|
|
87
|
+
mujoco_elem = ET.Element("mujoco")
|
|
88
|
+
mujoco_elem.append(root)
|
|
89
|
+
tree = ET.ElementTree(mujoco_elem)
|
|
90
|
+
|
|
91
|
+
return tree, tree.getroot()
|
|
92
|
+
except ET.ParseError:
|
|
93
|
+
msg = f"Invalid XML file: {xml_input}"
|
|
94
|
+
raise ValueError(msg)
|
|
95
|
+
|
|
96
|
+
def _wrap_urdf_in_mujoco(self, urdf_input):
|
|
97
|
+
"""Wrap a URDF inside <mujoco> tags for MuJoCo compatibility.
|
|
98
|
+
Input can be a file path, ElementTree, or XML string.
|
|
99
|
+
"""
|
|
100
|
+
# Handle string input (could be a file path or XML string)
|
|
101
|
+
if isinstance(urdf_input, str):
|
|
102
|
+
# Check if it's an XML string
|
|
103
|
+
if urdf_input.strip().startswith("<"):
|
|
104
|
+
try:
|
|
105
|
+
root = ET.fromstring(urdf_input)
|
|
106
|
+
tree = ET.ElementTree(root)
|
|
107
|
+
except ET.ParseError:
|
|
108
|
+
msg = "Invalid URDF XML string provided"
|
|
109
|
+
raise ValueError(msg)
|
|
110
|
+
else:
|
|
111
|
+
# It's a file path
|
|
112
|
+
try:
|
|
113
|
+
tree = ET.parse(urdf_input)
|
|
114
|
+
except:
|
|
115
|
+
msg = f"Could not parse URDF file: {urdf_input}"
|
|
116
|
+
raise ValueError(msg)
|
|
117
|
+
# Handle ElementTree input
|
|
118
|
+
elif isinstance(urdf_input, ET.ElementTree):
|
|
119
|
+
tree = urdf_input
|
|
120
|
+
else:
|
|
121
|
+
msg = "URDF input must be a string or ElementTree"
|
|
122
|
+
raise TypeError(msg)
|
|
123
|
+
|
|
124
|
+
root = tree.getroot()
|
|
125
|
+
|
|
126
|
+
# Handle the case where the root might already be a mujoco element
|
|
127
|
+
if root.tag == "mujoco":
|
|
128
|
+
return tree, root
|
|
129
|
+
|
|
130
|
+
# Create mujoco element and append the URDF root
|
|
131
|
+
mujoco_elem = ET.Element("mujoco")
|
|
132
|
+
mujoco_elem.append(root)
|
|
133
|
+
|
|
134
|
+
# Create a new tree with the <mujoco> root
|
|
135
|
+
return ET.ElementTree(mujoco_elem), mujoco_elem
|
|
136
|
+
|
|
137
|
+
def _merge_tags(self, tag_name, root_1, root_2) -> None:
|
|
138
|
+
"""Merge a specific tag (e.g., worldbody, asset) from two models."""
|
|
139
|
+
section_1 = root_1.find(tag_name)
|
|
140
|
+
section_2 = root_2.find(tag_name)
|
|
141
|
+
|
|
142
|
+
# If target section doesn't exist in model 1 but exists in model 2, create it
|
|
143
|
+
if section_1 is None and section_2 is not None:
|
|
144
|
+
section_1 = ET.SubElement(root_1, tag_name)
|
|
145
|
+
|
|
146
|
+
# Only proceed if both sections exist now
|
|
147
|
+
if section_1 is not None and section_2 is not None:
|
|
148
|
+
# Merge the contents by appending all elements from model 2 to model 1
|
|
149
|
+
for element in list(section_2): # Use list() to avoid modification during iteration
|
|
150
|
+
section_1.append(element)
|
|
151
|
+
|
|
152
|
+
def __add__(self, other):
|
|
153
|
+
"""Implement the + operator for merging two models."""
|
|
154
|
+
model_root = None
|
|
155
|
+
|
|
156
|
+
if isinstance(other, Builder):
|
|
157
|
+
# Merge the current model with another Builder instance
|
|
158
|
+
model_root = other.root
|
|
159
|
+
elif isinstance(other, str):
|
|
160
|
+
# If the input is a string, load as XML
|
|
161
|
+
_, model_root = self._load_model(other)
|
|
162
|
+
else:
|
|
163
|
+
msg = "Addition only supported between Builder objects or with XML strings/files"
|
|
164
|
+
raise TypeError(msg)
|
|
165
|
+
|
|
166
|
+
# List of sections to merge
|
|
167
|
+
sections_to_merge = ["asset", "worldbody", "camera", "light", "contact", "equality",
|
|
168
|
+
"sensor", "actuator", "default", "tendon", "include"]
|
|
169
|
+
|
|
170
|
+
# Merge all relevant sections
|
|
171
|
+
for section in sections_to_merge:
|
|
172
|
+
self._merge_tags(section, self.root, model_root)
|
|
173
|
+
|
|
174
|
+
# Return self to allow for method chaining
|
|
175
|
+
return self
|
|
176
|
+
|
|
177
|
+
def __radd__(self, other):
|
|
178
|
+
"""Implement the reverse + operator for merging two models."""
|
|
179
|
+
if other == 0: # Handle sum() starting value
|
|
180
|
+
return self
|
|
181
|
+
return self.__add__(other)
|
|
182
|
+
|
|
183
|
+
def save(self, file_path) -> "os.path":
|
|
184
|
+
"""Save the merged model to a file."""
|
|
185
|
+
if self.tree is not None:
|
|
186
|
+
# Format the XML with proper indentation before saving
|
|
187
|
+
self._indent_xml(self.root)
|
|
188
|
+
self.tree.write(file_path, encoding="utf-8", xml_declaration=True)
|
|
189
|
+
else:
|
|
190
|
+
msg = "No model loaded. Cannot save."
|
|
191
|
+
raise ValueError(msg)
|
|
192
|
+
return os.path.abspath(file_path)
|
|
193
|
+
|
|
194
|
+
def _indent_xml(self, elem, level=0) -> None:
|
|
195
|
+
"""Add proper indentation to make the XML file more readable."""
|
|
196
|
+
i = "\n" + level * " "
|
|
197
|
+
if len(elem):
|
|
198
|
+
if not elem.text or not elem.text.strip():
|
|
199
|
+
elem.text = i + " "
|
|
200
|
+
if not elem.tail or not elem.tail.strip():
|
|
201
|
+
elem.tail = i
|
|
202
|
+
for sub_elem in elem:
|
|
203
|
+
self._indent_xml(sub_elem, level + 1)
|
|
204
|
+
if not elem[-1].tail or not elem[-1].tail.strip():
|
|
205
|
+
elem[-1].tail = i
|
|
206
|
+
elif level and (not elem.tail or not elem.tail.strip()):
|
|
207
|
+
elem.tail = i
|
|
208
|
+
|
|
209
|
+
def __str__(self) -> str:
|
|
210
|
+
"""Return the XML string of the model."""
|
|
211
|
+
if self.tree is not None:
|
|
212
|
+
# Format the XML with proper indentation
|
|
213
|
+
self._indent_xml(self.root)
|
|
214
|
+
return ET.tostring(self.root, encoding="unicode", method="xml")
|
|
215
|
+
msg = "No model loaded. Cannot generate string."
|
|
216
|
+
raise ValueError(msg)
|
|
217
|
+
|
|
218
|
+
def __repr__(self) -> str:
|
|
219
|
+
"""Return the XML string of the model."""
|
|
220
|
+
return self.__str__()
|
|
221
|
+
|
|
222
|
+
def __len__(self) -> int:
|
|
223
|
+
"""Return the number of elements in the model."""
|
|
224
|
+
return len(self.root) if self.root is not None else 0
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def xml(self):
|
|
228
|
+
"""XML string of the model."""
|
|
229
|
+
return str(self)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from numpy import cos, pi, random, sin
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _apply_control(model, data, value, joint=None, axis=None, delay=0) -> None:
|
|
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
|
+
"""
|
|
16
|
+
# Don't apply control until after delay
|
|
17
|
+
if data.time < delay:
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
# Determine targets if not explicitly provided
|
|
21
|
+
if joint is None and axis is None:
|
|
22
|
+
joint = range(model.nu)
|
|
23
|
+
|
|
24
|
+
# Apply control to joints or specific axis
|
|
25
|
+
if joint is not None and model.nu > 0:
|
|
26
|
+
for j in joint:
|
|
27
|
+
data.ctrl[j] = value
|
|
28
|
+
elif axis is not None:
|
|
29
|
+
data.qpos[axis] = value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def step_controller(model, data, **kwargs) -> None:
|
|
33
|
+
"""A step controller for the simulation.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
amplitude (float): The amplitude of the step signal (default=1).
|
|
37
|
+
joint (list[int]): The joints to apply the step signal to (default=all).
|
|
38
|
+
axis (int): The axis to apply the step signal to (default=None).
|
|
39
|
+
delay (float): The delay before applying the step signal (default=0).
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
None
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
amplitude = kwargs.get("amplitude", 1)
|
|
46
|
+
joint = kwargs.get("joint")
|
|
47
|
+
axis = kwargs.get("axis")
|
|
48
|
+
delay = kwargs.get("delay", 0)
|
|
49
|
+
|
|
50
|
+
if delay < 0:
|
|
51
|
+
msg = "Delay must be non-negative."
|
|
52
|
+
raise ValueError(msg)
|
|
53
|
+
if joint is not None and axis is not None:
|
|
54
|
+
msg = "Cannot specify both 'joint' and 'axis'."
|
|
55
|
+
raise ValueError(msg)
|
|
56
|
+
|
|
57
|
+
_apply_control(model, data, amplitude, joint=joint, axis=axis, delay=delay)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def sine_controller(model, data, **kwargs) -> None:
|
|
61
|
+
"""A simple sine wave controller for the simulation.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
amplitude (float): The amplitude of the sine wave (default=1).
|
|
65
|
+
frequency (float): The frequency of the sine wave (default=1).
|
|
66
|
+
phase (float): The phase shift of the sine wave (default=0).
|
|
67
|
+
joint (list[int]): The joint to apply the sine wave to (default=all).
|
|
68
|
+
delay (float): The delay before applying the sine wave (default=0).
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
None
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
amplitude = kwargs.get("amplitude", 1)
|
|
75
|
+
frequency = kwargs.get("frequency", 1)
|
|
76
|
+
phase = kwargs.get("phase", 0)
|
|
77
|
+
joint = kwargs.get("joint")
|
|
78
|
+
delay = kwargs.get("delay", 0)
|
|
79
|
+
|
|
80
|
+
if delay < 0:
|
|
81
|
+
msg = "Delay must be non-negative."
|
|
82
|
+
raise ValueError(msg)
|
|
83
|
+
|
|
84
|
+
value = amplitude * sin(2 * pi * frequency * data.time + phase)
|
|
85
|
+
_apply_control(model, data, value, joint=joint, delay=delay)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cosine_controller(model, data, **kwargs) -> None:
|
|
89
|
+
"""A simple cosine wave controller for the simulation.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
amplitude (float): The amplitude of the cosine wave (default=1).
|
|
93
|
+
frequency (float): The frequency of the cosine wave (default=1).
|
|
94
|
+
phase (float): The phase shift of the cosine wave (default=0).
|
|
95
|
+
joint (list[int]): The joint to apply the cosine wave to (default=all).
|
|
96
|
+
delay (float): The delay before applying the cosine wave (default=0).
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
None
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
amplitude = kwargs.get("amplitude", 1)
|
|
103
|
+
frequency = kwargs.get("frequency", 1)
|
|
104
|
+
phase = kwargs.get("phase", 0)
|
|
105
|
+
joint = kwargs.get("joint")
|
|
106
|
+
delay = kwargs.get("delay", 0)
|
|
107
|
+
|
|
108
|
+
if delay < 0:
|
|
109
|
+
msg = "Delay must be non-negative."
|
|
110
|
+
raise ValueError(msg)
|
|
111
|
+
|
|
112
|
+
value = amplitude * cos(2 * pi * frequency * data.time + phase)
|
|
113
|
+
_apply_control(model, data, value, joint=joint, delay=delay)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def random_controller(model, data, **kwargs) -> None:
|
|
117
|
+
"""A random controller for the simulation.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
amplitude (float): The maximum amplitude of the random signal (default=1).
|
|
121
|
+
joint (list[int]): The joints to apply the random signal to (default=all).
|
|
122
|
+
axis (int): The axis to apply the random signal to (default=None).
|
|
123
|
+
delay (float): The delay before applying the random signal (default=0).
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
None
|
|
127
|
+
|
|
128
|
+
"""
|
|
129
|
+
amplitude = kwargs.get("amplitude", 1)
|
|
130
|
+
joint = kwargs.get("joint")
|
|
131
|
+
axis = kwargs.get("axis")
|
|
132
|
+
delay = kwargs.get("delay", 0)
|
|
133
|
+
|
|
134
|
+
if delay < 0:
|
|
135
|
+
msg = "Delay must be non-negative."
|
|
136
|
+
raise ValueError(msg)
|
|
137
|
+
if joint is not None and axis is not None:
|
|
138
|
+
msg = "Cannot specify both 'joint' and 'axis'."
|
|
139
|
+
raise ValueError(msg)
|
|
140
|
+
|
|
141
|
+
value = amplitude * random.rand()
|
|
142
|
+
_apply_control(model, data, value, joint=joint, axis=axis, delay=delay)
|
|
143
|
+
|
|
144
|
+
def live_controller(model, data, **kwargs) -> None:
|
|
145
|
+
"""A real-time controller for the simulation.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
controller_params (dict): Dictionary of parameters to pass to the controller.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
None
|
|
152
|
+
|
|
153
|
+
"""
|
|
154
|
+
from .utils import _print_warning
|
|
155
|
+
for key, value in kwargs.get("controller_params", {}).items():
|
|
156
|
+
if hasattr(data, key):
|
|
157
|
+
setattr(data, key, value)
|
|
158
|
+
else:
|
|
159
|
+
_print_warning(f"'{key}' is not a valid attribute of MjData. Skipping...")
|
|
160
|
+
|
|
161
|
+
# untested
|
|
162
|
+
# if hasattr(data, 'control'):
|
|
163
|
+
# data.control = kwargs.get("control", data.control)
|
|
164
|
+
|
mujoco_toolbox/utils.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import sys
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from multiprocessing import cpu_count
|
|
5
|
+
from time import time
|
|
6
|
+
|
|
7
|
+
from colorama import Fore, Style, init
|
|
8
|
+
from screeninfo import get_monitors
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def timer(func):
|
|
12
|
+
@wraps(func)
|
|
13
|
+
def time_wrapper(*args, **kwargs):
|
|
14
|
+
start_time = time()
|
|
15
|
+
result = func(*args, **kwargs)
|
|
16
|
+
end_time = time()
|
|
17
|
+
total_time = end_time - start_time
|
|
18
|
+
print(f"Function {func.__name__} took {total_time:.4f} seconds to execute")
|
|
19
|
+
return result
|
|
20
|
+
|
|
21
|
+
return time_wrapper
|
|
22
|
+
|
|
23
|
+
def _print_warning(*args, prefix: bool = True) -> None:
|
|
24
|
+
"""Prints the first argument as an orange warning message and prints the other arguments in regular text, all on the same line.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
*args: First argument is printed as an orange warning, other arguments are printed normally.
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
if args:
|
|
31
|
+
if prefix:
|
|
32
|
+
# Make sure the first argument is printed with the 'WARNING:' prefix and in orange
|
|
33
|
+
message = f"WARNING: {args[0]}"
|
|
34
|
+
else:
|
|
35
|
+
message = args[0]
|
|
36
|
+
|
|
37
|
+
# Check if we're on Windows or a Unix-like system for color support
|
|
38
|
+
if sys.platform.startswith("win"):
|
|
39
|
+
try:
|
|
40
|
+
init() # Initialize colorama
|
|
41
|
+
print(f"{Fore.YELLOW}{message}{Style.RESET_ALL}", end=" ") # Yellow color for warning
|
|
42
|
+
print(*args[1:])
|
|
43
|
+
except ImportError:
|
|
44
|
+
# If colorama is not available, print without color
|
|
45
|
+
print(message, end=" ")
|
|
46
|
+
print(*args[1:])
|
|
47
|
+
else:
|
|
48
|
+
# ANSI escape code for orange (for Unix-like systems)
|
|
49
|
+
print(f"\033[38;5;214m{message}\033[0m", end=" ") # Orange ANSI escape code
|
|
50
|
+
print(*args[1:])
|
|
51
|
+
|
|
52
|
+
def _print_success(*args, prefix: bool = True) -> None:
|
|
53
|
+
"""Prints the first argument as a green success message and prints the other arguments in regular text, all on the same line.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
*args: First argument is printed as a green success message, other arguments are printed normally.
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
if args:
|
|
60
|
+
if prefix:
|
|
61
|
+
# Make sure the first argument is printed with the 'SUCCESS:' prefix and in green
|
|
62
|
+
message = f"SUCCESS: {args[0]}"
|
|
63
|
+
else:
|
|
64
|
+
message = args[0]
|
|
65
|
+
|
|
66
|
+
# Check if we're on Windows or a Unix-like system for color support
|
|
67
|
+
if sys.platform.startswith("win"):
|
|
68
|
+
try:
|
|
69
|
+
init() # Initialize colorama
|
|
70
|
+
print(f"{Fore.GREEN}{message}{Style.RESET_ALL}", end=" ") # Green color for success
|
|
71
|
+
print(*args[1:])
|
|
72
|
+
except ImportError:
|
|
73
|
+
# If colorama is not available, print without color
|
|
74
|
+
print(message, end=" ")
|
|
75
|
+
print(*args[1:])
|
|
76
|
+
else:
|
|
77
|
+
# ANSI escape code for green (for Unix-like systems)
|
|
78
|
+
print(f"\033[32m{message}\033[0m", end=" ") # Green ANSI escape code
|
|
79
|
+
print(*args[1:])
|
|
80
|
+
|
|
81
|
+
class _Platform:
|
|
82
|
+
def __init__(self) -> None:
|
|
83
|
+
self.OS = platform.system()
|
|
84
|
+
self.PROCESSOR = platform.processor()
|
|
85
|
+
self.MACHINE = platform.machine()
|
|
86
|
+
self.PYTHON_VERSION = platform.python_version()
|
|
87
|
+
self.IDE = self.get_developer_mode()
|
|
88
|
+
self.CPU_COUNT = cpu_count()
|
|
89
|
+
self.ARCHITECTURE = platform.architecture()[0]
|
|
90
|
+
self.PYTHON_IMPLEMENTATION = platform.python_implementation()
|
|
91
|
+
self.RESOLUTION, self.GUI_ENABLED = self.get_resolution()
|
|
92
|
+
# self.NUM_MONITORS = get_monitors().__len__()
|
|
93
|
+
|
|
94
|
+
def __repr__(self) -> str:
|
|
95
|
+
return (
|
|
96
|
+
f"Operating System: {self.OS}\n"
|
|
97
|
+
f"Architecture: {self.ARCHITECTURE}\n"
|
|
98
|
+
f"Processor: {self.PROCESSOR}\n"
|
|
99
|
+
f"Machine Type: {self.MACHINE}\n"
|
|
100
|
+
f"CPU Count: {self.CPU_COUNT}\n"
|
|
101
|
+
f"Python Version: {self.PYTHON_VERSION}\n"
|
|
102
|
+
f"IDE: {self.IDE}\n"
|
|
103
|
+
f"Python Implementation: {self.PYTHON_IMPLEMENTATION}\n"
|
|
104
|
+
f"Resolution (disp0): {self.RESOLUTION}\n"
|
|
105
|
+
f"GUI Enabled: {self.GUI_ENABLED}"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def __str__(self) -> str:
|
|
109
|
+
return self.__repr__()
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def get_developer_mode() -> str:
|
|
113
|
+
if "ipykernel" in sys.modules and not sys.stdin.isatty():
|
|
114
|
+
return "jupyter"
|
|
115
|
+
return "terminal" if hasattr(sys, "ps1") else "script"
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def get_resolution():
|
|
119
|
+
"""Detects screen resolution and potential headless operation."""
|
|
120
|
+
try:
|
|
121
|
+
monitor = get_monitors()[0]
|
|
122
|
+
return (monitor.width, monitor.height), True
|
|
123
|
+
except Exception:
|
|
124
|
+
_print_warning("Detected headless operation. Disabling GUI...")
|
|
125
|
+
return (1920, 1080), False
|
|
126
|
+
|
|
127
|
+
class _EmptyContextManager:
|
|
128
|
+
__slots__ = ()
|
|
129
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
130
|
+
pass
|
|
131
|
+
def __enter__(self):
|
|
132
|
+
pass
|
|
133
|
+
def __exit__(self, _exc_type, _exc_value, _traceback):
|
|
134
|
+
pass
|