mujoco-toolbox 0.2.0__tar.gz → 0.2.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mujoco-toolbox
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: A modern package to handle MuJoCo environments
5
5
  License: MIT
6
6
  Keywords: MuJoCo,robotics,simulation,toolbox,wrapper
@@ -38,6 +38,7 @@ Description-Content-Type: text/markdown
38
38
  ![Python](https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12%20|%203.13-blue)
39
39
  ![License](https://img.shields.io/github/license/MGross21/mujoco-toolbox)
40
40
  [![PyPI](https://github.com/MGross21/mujoco-toolbox/actions/workflows/publish.yml/badge.svg)](https://github.com/MGross21/mujoco-toolbox/actions/workflows/publish.yml)
41
+ [![Docs](https://github.com/MGross21/mujoco-toolbox/actions/workflows/docs.yml/badge.svg)](https://github.com/MGross21/mujoco-toolbox/actions/workflows/docs.yml)
41
42
 
42
43
  # Mujoco Toolbox
43
44
 
@@ -102,7 +103,6 @@ ffmpeg -version
102
103
  import mujoco_toolbox as mjtb
103
104
 
104
105
  # Optional Global Variables
105
- mjtb.VERBOSITY = True # Additional Performance and Debugging Print Statements (Default=False)
106
106
  mjtb.CAPTURE_PARAMETERS = "all" # MjData Simulation Parameters (Default = ['time', 'qpos', 'qvel', 'act', 'qacc', 'xpos', 'xquat', 'xmat', 'ctrl', 'sensordata'])
107
107
 
108
108
  # Example Code
@@ -2,6 +2,7 @@
2
2
  ![Python](https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12%20|%203.13-blue)
3
3
  ![License](https://img.shields.io/github/license/MGross21/mujoco-toolbox)
4
4
  [![PyPI](https://github.com/MGross21/mujoco-toolbox/actions/workflows/publish.yml/badge.svg)](https://github.com/MGross21/mujoco-toolbox/actions/workflows/publish.yml)
5
+ [![Docs](https://github.com/MGross21/mujoco-toolbox/actions/workflows/docs.yml/badge.svg)](https://github.com/MGross21/mujoco-toolbox/actions/workflows/docs.yml)
5
6
 
6
7
  # Mujoco Toolbox
7
8
 
@@ -66,7 +67,6 @@ ffmpeg -version
66
67
  import mujoco_toolbox as mjtb
67
68
 
68
69
  # Optional Global Variables
69
- mjtb.VERBOSITY = True # Additional Performance and Debugging Print Statements (Default=False)
70
70
  mjtb.CAPTURE_PARAMETERS = "all" # MjData Simulation Parameters (Default = ['time', 'qpos', 'qvel', 'act', 'qacc', 'xpos', 'xquat', 'xmat', 'ctrl', 'sensordata'])
71
71
 
72
72
  # Example Code
@@ -1,5 +1,4 @@
1
- """
2
- Mujoco Toolbox
1
+ """Mujoco Toolbox.
3
2
  ==============
4
3
  A toolbox for working with MuJoCo simulations. This package provides various utilities and controllers to facilitate
5
4
  the simulation process.
@@ -13,43 +12,42 @@ Modules:
13
12
  Constants:
14
13
  ----------
15
14
  - CAPTURE_PARAMETERS: List of MjData fields to capture during simulation.
16
- - VERBOSITY: Global variable to control verbosity of the package.
17
15
 
18
- Notes:
19
- ------
16
+ Notes
17
+ -----
20
18
  This package is still under development. Report any issues to https://github.com/MGross21/mujoco-toolbox/issues.
19
+
21
20
  """
22
21
 
23
22
  from .assets import WORLD_ASSETS, GloveBox
24
23
  from .builder import Builder
25
- from .Controller import cosineController, randomController, realTimeController, sineController, stepController
26
- from .Utils import _Platform, print_success, print_warning, timer
27
- from .Wrapper import Wrapper
24
+ from .controller import cosineController, randomController, realTimeController, sineController, stepController
25
+ from .utils import _Platform, print_success, print_warning, timer
26
+ from .wrapper import Wrapper
28
27
 
29
- __version__ = "0.2.0"
28
+ __version__ = "0.2.2"
30
29
  __author__ = "Michael Gross"
31
30
  __github_repo__ = "mujoco-toolbox"
32
31
  __license__ = "MIT"
33
32
  __status__ = "Development"
34
-
35
- # `from mujoco_toolbox import *` will import these objects
36
33
  __all__ = [
34
+ "CAPTURE_PARAMETERS",
35
+ "COMPUTER",
36
+ "WORLD_ASSETS",
37
+ "Builder",
38
+ "GloveBox",
37
39
  "Wrapper",
38
- "sineController",
39
40
  "cosineController",
40
41
  "randomController",
41
- "stepController",
42
42
  "realTimeController",
43
+ "sineController",
44
+ "stepController",
43
45
  "timer",
44
- "Computer",
45
- "Builder",
46
- "GloveBox",
47
- "WORLD_ASSETS",
48
- "CAPTURE_PARAMETERS",
49
- "VERBOSITY",
50
46
  ]
51
47
 
52
- CAPTURE_PARAMETERS = [
48
+ COMPUTER = _Platform() # Singleton Instance
49
+ MAX_GEOM_SCALAR: int = 2 # Scalar value for mujoco.Renderer.max_geom
50
+ CAPTURE_PARAMETERS = [ # MjData default fields to capture during simulation
53
51
  "time",
54
52
  "qpos",
55
53
  "qvel",
@@ -60,16 +58,10 @@ CAPTURE_PARAMETERS = [
60
58
  "xmat",
61
59
  "ctrl",
62
60
  "sensordata",
63
- ] # MjData default fields to capture during simulation
64
-
65
- VERBOSITY = False
61
+ ]
66
62
 
67
- if True:
68
- from .Utils import print_warning
63
+ if __version__.split(".")[0] == "0":
69
64
  print_warning(
70
65
  f"{__package__} (v{__version__}) is still under development.",
71
66
  f"Report any issues to https://github.com/MGross21/{__github_repo__}/issues",
72
67
  )
73
-
74
- # Create a singleton instance
75
- Computer = _Platform()
@@ -10,7 +10,7 @@ WORLD_ASSETS = """
10
10
  </asset>
11
11
  """
12
12
 
13
- def GloveBox(width=1.25, depth=0.75, height=1.0, glass_thickness=0.05):
13
+ def GloveBox(width=1.25, depth=0.75, height=1.0, glass_thickness=0.05) -> str:
14
14
  return f"""
15
15
  <mujoco>
16
16
  <asset>
@@ -3,14 +3,15 @@ import xml.etree.ElementTree as ET
3
3
 
4
4
 
5
5
  class Builder:
6
- def __init__(self, xml_input: str):
7
- """ Initialize with an XML string or a file path. Input is required."""
6
+ def __init__(self, xml_input: str) -> None:
7
+ """Initialize with an XML string or a file path. Input is required."""
8
8
  self.tree, self.root = self.load_model(xml_input)
9
9
 
10
10
  def load_model(self, xml_input):
11
- """ Load a MuJoCo model from a file or XML string."""
11
+ """Load a MuJoCo model from a file or XML string."""
12
12
  if not isinstance(xml_input, str):
13
- raise ValueError("Input must be either an XML string or a file path")
13
+ msg = "Input must be either an XML string or a file path"
14
+ raise ValueError(msg)
14
15
 
15
16
  # Check if input is an XML string
16
17
  if xml_input.strip().startswith("<"):
@@ -31,11 +32,13 @@ class Builder:
31
32
 
32
33
  return tree, tree.getroot()
33
34
  except ET.ParseError:
34
- raise ValueError("Invalid XML string provided")
35
+ msg = "Invalid XML string provided"
36
+ raise ValueError(msg)
35
37
  else:
36
38
  # Treat input as file path
37
39
  if not os.path.exists(xml_input):
38
- raise FileNotFoundError(f"File not found: {xml_input}")
40
+ msg = f"File not found: {xml_input}"
41
+ raise FileNotFoundError(msg)
39
42
 
40
43
  # Handle URDF files
41
44
  if xml_input.lower().endswith(".urdf"):
@@ -54,11 +57,11 @@ class Builder:
54
57
 
55
58
  return tree, tree.getroot()
56
59
  except ET.ParseError:
57
- raise ValueError(f"Invalid XML file: {xml_input}")
60
+ msg = f"Invalid XML file: {xml_input}"
61
+ raise ValueError(msg)
58
62
 
59
63
  def wrap_urdf_in_mujoco(self, urdf_input):
60
- """
61
- Wrap a URDF inside <mujoco> tags for MuJoCo compatibility.
64
+ """Wrap a URDF inside <mujoco> tags for MuJoCo compatibility.
62
65
  Input can be a file path, ElementTree, or XML string.
63
66
  """
64
67
  # Handle string input (could be a file path or XML string)
@@ -69,18 +72,21 @@ class Builder:
69
72
  root = ET.fromstring(urdf_input)
70
73
  tree = ET.ElementTree(root)
71
74
  except ET.ParseError:
72
- raise ValueError("Invalid URDF XML string provided")
75
+ msg = "Invalid URDF XML string provided"
76
+ raise ValueError(msg)
73
77
  else:
74
78
  # It's a file path
75
79
  try:
76
80
  tree = ET.parse(urdf_input)
77
81
  except:
78
- raise ValueError(f"Could not parse URDF file: {urdf_input}")
82
+ msg = f"Could not parse URDF file: {urdf_input}"
83
+ raise ValueError(msg)
79
84
  # Handle ElementTree input
80
85
  elif isinstance(urdf_input, ET.ElementTree):
81
86
  tree = urdf_input
82
87
  else:
83
- raise TypeError("URDF input must be a string or ElementTree")
88
+ msg = "URDF input must be a string or ElementTree"
89
+ raise TypeError(msg)
84
90
 
85
91
  root = tree.getroot()
86
92
 
@@ -95,8 +101,8 @@ class Builder:
95
101
  # Create a new tree with the <mujoco> root
96
102
  return ET.ElementTree(mujoco_elem), mujoco_elem
97
103
 
98
- def merge_tags(self, tag_name, root_1, root_2):
99
- """ Merge a specific tag (e.g., worldbody, asset) from two models."""
104
+ def merge_tags(self, tag_name, root_1, root_2) -> None:
105
+ """Merge a specific tag (e.g., worldbody, asset) from two models."""
100
106
  section_1 = root_1.find(tag_name)
101
107
  section_2 = root_2.find(tag_name)
102
108
 
@@ -111,7 +117,7 @@ class Builder:
111
117
  section_1.append(element)
112
118
 
113
119
  def __add__(self, other):
114
- """ Implement the + operator for merging two models."""
120
+ """Implement the + operator for merging two models."""
115
121
  model_root = None
116
122
 
117
123
  if isinstance(other, Builder):
@@ -121,7 +127,8 @@ class Builder:
121
127
  # If the input is a string, load as XML
122
128
  _, model_root = self.load_model(other)
123
129
  else:
124
- raise TypeError("Addition only supported between Builder objects or with XML strings/files")
130
+ msg = "Addition only supported between Builder objects or with XML strings/files"
131
+ raise TypeError(msg)
125
132
 
126
133
  # List of sections to merge
127
134
  sections_to_merge = ["asset", "worldbody", "camera", "light", "contact", "equality",
@@ -135,22 +142,23 @@ class Builder:
135
142
  return self
136
143
 
137
144
  def __radd__(self, other):
138
- """ Implement the reverse + operator for merging two models."""
145
+ """Implement the reverse + operator for merging two models."""
139
146
  if other == 0: # Handle sum() starting value
140
147
  return self
141
148
  return self.__add__(other)
142
149
 
143
- def save(self, file_path):
144
- """ Save the merged model to a file."""
150
+ def save(self, file_path) -> None:
151
+ """Save the merged model to a file."""
145
152
  if self.tree is not None:
146
153
  # Format the XML with proper indentation before saving
147
154
  self._indent_xml(self.root)
148
155
  self.tree.write(file_path, encoding="utf-8", xml_declaration=True)
149
156
  else:
150
- raise ValueError("No model loaded. Cannot save.")
157
+ msg = "No model loaded. Cannot save."
158
+ raise ValueError(msg)
151
159
 
152
- def _indent_xml(self, elem, level=0):
153
- """ Add proper indentation to make the XML file more readable."""
160
+ def _indent_xml(self, elem, level=0) -> None:
161
+ """Add proper indentation to make the XML file more readable."""
154
162
  i = "\n" + level * " "
155
163
  if len(elem):
156
164
  if not elem.text or not elem.text.strip():
@@ -161,24 +169,23 @@ class Builder:
161
169
  self._indent_xml(sub_elem, level + 1)
162
170
  if not elem[-1].tail or not elem[-1].tail.strip():
163
171
  elem[-1].tail = i
164
- else:
165
- if level and (not elem.tail or not elem.tail.strip()):
166
- elem.tail = i
172
+ elif level and (not elem.tail or not elem.tail.strip()):
173
+ elem.tail = i
167
174
 
168
- def __str__(self):
169
- """ Return the XML string of the model."""
175
+ def __str__(self) -> str:
176
+ """Return the XML string of the model."""
170
177
  if self.tree is not None:
171
178
  # Format the XML with proper indentation
172
179
  self._indent_xml(self.root)
173
180
  return ET.tostring(self.root, encoding="unicode", method="xml")
174
- else:
175
- raise ValueError("No model loaded. Cannot generate string.")
181
+ msg = "No model loaded. Cannot generate string."
182
+ raise ValueError(msg)
176
183
 
177
- def __repr__(self):
178
- """ Return the XML string of the model."""
184
+ def __repr__(self) -> str:
185
+ """Return the XML string of the model."""
179
186
  return self.__str__()
180
187
 
181
188
  @property
182
189
  def xml(self):
183
- """ Property to quickly grab the XML string of the model."""
190
+ """Property to quickly grab the XML string of the model."""
184
191
  return str(self)
@@ -1,7 +1,7 @@
1
1
  from numpy import cos, pi, random, sin
2
2
 
3
3
 
4
- def _apply_control(model, data, value, joint=None, axis=None, delay=0):
4
+ def _apply_control(model, data, value, joint=None, axis=None, delay=0) -> None:
5
5
  """Common helper function for controller logic to reduce redundancy.
6
6
 
7
7
  Args:
@@ -11,6 +11,7 @@ def _apply_control(model, data, value, joint=None, axis=None, delay=0):
11
11
  joint (list[int], optional): Joints to apply control to.
12
12
  axis (int, optional): Axis to apply control to.
13
13
  delay (float, optional): Delay before applying control.
14
+
14
15
  """
15
16
  # Don't apply control until after delay
16
17
  if data.time < delay:
@@ -28,7 +29,7 @@ def _apply_control(model, data, value, joint=None, axis=None, delay=0):
28
29
  data.qpos[axis] = value
29
30
 
30
31
 
31
- def stepController(model, data, **kwargs):
32
+ def stepController(model, data, **kwargs) -> None:
32
33
  """A step controller for the simulation.
33
34
 
34
35
  Args:
@@ -39,21 +40,24 @@ def stepController(model, data, **kwargs):
39
40
 
40
41
  Returns:
41
42
  None
43
+
42
44
  """
43
45
  amplitude = kwargs.get("amplitude", 1)
44
- joint = kwargs.get("joint", None)
45
- axis = kwargs.get("axis", None)
46
+ joint = kwargs.get("joint")
47
+ axis = kwargs.get("axis")
46
48
  delay = kwargs.get("delay", 0)
47
49
 
48
50
  if delay < 0:
49
- raise ValueError("Delay must be non-negative.")
51
+ msg = "Delay must be non-negative."
52
+ raise ValueError(msg)
50
53
  if joint is not None and axis is not None:
51
- raise ValueError("Cannot specify both 'joint' and 'axis'.")
54
+ msg = "Cannot specify both 'joint' and 'axis'."
55
+ raise ValueError(msg)
52
56
 
53
57
  _apply_control(model, data, amplitude, joint=joint, axis=axis, delay=delay)
54
58
 
55
59
 
56
- def sineController(model, data, **kwargs):
60
+ def sineController(model, data, **kwargs) -> None:
57
61
  """A simple sine wave controller for the simulation.
58
62
 
59
63
  Args:
@@ -65,21 +69,23 @@ def sineController(model, data, **kwargs):
65
69
 
66
70
  Returns:
67
71
  None
72
+
68
73
  """
69
74
  amplitude = kwargs.get("amplitude", 1)
70
75
  frequency = kwargs.get("frequency", 1)
71
76
  phase = kwargs.get("phase", 0)
72
- joint = kwargs.get("joint", None)
77
+ joint = kwargs.get("joint")
73
78
  delay = kwargs.get("delay", 0)
74
79
 
75
80
  if delay < 0:
76
- raise ValueError("Delay must be non-negative.")
81
+ msg = "Delay must be non-negative."
82
+ raise ValueError(msg)
77
83
 
78
84
  value = amplitude * sin(2 * pi * frequency * data.time + phase)
79
85
  _apply_control(model, data, value, joint=joint, delay=delay)
80
86
 
81
87
 
82
- def cosineController(model, data, **kwargs):
88
+ def cosineController(model, data, **kwargs) -> None:
83
89
  """A simple cosine wave controller for the simulation.
84
90
 
85
91
  Args:
@@ -91,21 +97,23 @@ def cosineController(model, data, **kwargs):
91
97
 
92
98
  Returns:
93
99
  None
100
+
94
101
  """
95
102
  amplitude = kwargs.get("amplitude", 1)
96
103
  frequency = kwargs.get("frequency", 1)
97
104
  phase = kwargs.get("phase", 0)
98
- joint = kwargs.get("joint", None)
105
+ joint = kwargs.get("joint")
99
106
  delay = kwargs.get("delay", 0)
100
107
 
101
108
  if delay < 0:
102
- raise ValueError("Delay must be non-negative.")
109
+ msg = "Delay must be non-negative."
110
+ raise ValueError(msg)
103
111
 
104
112
  value = amplitude * cos(2 * pi * frequency * data.time + phase)
105
113
  _apply_control(model, data, value, joint=joint, delay=delay)
106
114
 
107
115
 
108
- def randomController(model, data, **kwargs):
116
+ def randomController(model, data, **kwargs) -> None:
109
117
  """A random controller for the simulation.
110
118
 
111
119
  Args:
@@ -116,21 +124,24 @@ def randomController(model, data, **kwargs):
116
124
 
117
125
  Returns:
118
126
  None
127
+
119
128
  """
120
129
  amplitude = kwargs.get("amplitude", 1)
121
- joint = kwargs.get("joint", None)
122
- axis = kwargs.get("axis", None)
130
+ joint = kwargs.get("joint")
131
+ axis = kwargs.get("axis")
123
132
  delay = kwargs.get("delay", 0)
124
133
 
125
134
  if delay < 0:
126
- raise ValueError("Delay must be non-negative.")
135
+ msg = "Delay must be non-negative."
136
+ raise ValueError(msg)
127
137
  if joint is not None and axis is not None:
128
- raise ValueError("Cannot specify both 'joint' and 'axis'.")
138
+ msg = "Cannot specify both 'joint' and 'axis'."
139
+ raise ValueError(msg)
129
140
 
130
141
  value = amplitude * random.rand()
131
142
  _apply_control(model, data, value, joint=joint, axis=axis, delay=delay)
132
143
 
133
- def realTimeController(model, data, **kwargs):
144
+ def realTimeController(model, data, **kwargs) -> None:
134
145
  """A real-time controller for the simulation.
135
146
 
136
147
  Args:
@@ -138,8 +149,9 @@ def realTimeController(model, data, **kwargs):
138
149
 
139
150
  Returns:
140
151
  None
152
+
141
153
  """
142
- from .Utils import print_warning
154
+ from .utils import print_warning
143
155
  for key, value in kwargs.get("controller_params", {}).items():
144
156
  if hasattr(data, key):
145
157
  setattr(data, key, value)
@@ -150,4 +162,3 @@ def realTimeController(model, data, **kwargs):
150
162
  # if hasattr(data, 'control'):
151
163
  # data.control = kwargs.get("control", data.control)
152
164
 
153
- return
@@ -15,21 +15,17 @@ def timer(func):
15
15
  result = func(*args, **kwargs)
16
16
  end_time = time()
17
17
  total_time = end_time - start_time
18
-
19
- from . import VERBOSITY
20
-
21
- if VERBOSITY:
22
- print(f"Function '{func.__name__}' took {total_time:.4f} seconds")
18
+ print(f"Function {func.__name__} took {total_time:.4f} seconds to execute")
23
19
  return result
24
20
 
25
21
  return time_wrapper
26
22
 
27
23
  def print_warning(*args, prefix: bool = True) -> None:
28
- """
29
- Prints the first argument as an orange warning message and prints the other arguments in regular text, all on the same line.
24
+ """Prints the first argument as an orange warning message and prints the other arguments in regular text, all on the same line.
30
25
 
31
26
  Args:
32
27
  *args: First argument is printed as an orange warning, other arguments are printed normally.
28
+
33
29
  """
34
30
  if args:
35
31
  if prefix:
@@ -54,11 +50,11 @@ def print_warning(*args, prefix: bool = True) -> None:
54
50
  print(*args[1:])
55
51
 
56
52
  def print_success(*args, prefix: bool = True) -> None:
57
- """
58
- Prints the first argument as a green success message and prints the other arguments in regular text, all on the same line.
53
+ """Prints the first argument as a green success message and prints the other arguments in regular text, all on the same line.
59
54
 
60
55
  Args:
61
56
  *args: First argument is printed as a green success message, other arguments are printed normally.
57
+
62
58
  """
63
59
  if args:
64
60
  if prefix:
@@ -83,7 +79,7 @@ def print_success(*args, prefix: bool = True) -> None:
83
79
  print(*args[1:])
84
80
 
85
81
  class _Platform:
86
- def __init__(self):
82
+ def __init__(self) -> None:
87
83
  self.OS = platform.system()
88
84
  self.PROCESSOR = platform.processor()
89
85
  self.MACHINE = platform.machine()
@@ -95,7 +91,7 @@ class _Platform:
95
91
  self.RESOLUTION, self.GUI_ENABLED = self.get_resolution()
96
92
  # self.NUM_MONITORS = get_monitors().__len__()
97
93
 
98
- def __repr__(self):
94
+ def __repr__(self) -> str:
99
95
  return (
100
96
  f"Operating System: {self.OS}\n"
101
97
  f"Processor: {self.PROCESSOR}\n"
@@ -109,11 +105,11 @@ class _Platform:
109
105
  f"GUI Enabled: {self.GUI_ENABLED}"
110
106
  )
111
107
 
112
- def __str__(self):
108
+ def __str__(self) -> str:
113
109
  return self.__repr__()
114
110
 
115
111
  @staticmethod
116
- def get_developer_mode():
112
+ def get_developer_mode() -> str:
117
113
  if "ipykernel" in sys.modules and not sys.stdin.isatty():
118
114
  return "jupyter"
119
115
  return "terminal" if hasattr(sys, "ps1") else "script"