mujoco-toolbox 0.2.2__tar.gz → 0.3.3__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,20 +1,36 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mujoco-toolbox
3
- Version: 0.2.2
3
+ Version: 0.3.3
4
4
  Summary: A modern package to handle MuJoCo environments
5
- License: MIT
6
- Keywords: MuJoCo,robotics,simulation,toolbox,wrapper
5
+ License: MIT License
6
+
7
+ Copyright (c) 2025 Michael Gross
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+ Keywords: MuJoCo,mujoco,robotics,simulation,toolbox,wrapper
7
27
  Author: MGross21
8
28
  Author-email: MGross21@TODO.com
9
29
  Requires-Python: >=3.10
10
- Classifier: Development Status :: 3 - Alpha
30
+ Classifier: Development Status :: 4 - Beta
11
31
  Classifier: Intended Audience :: Developers
12
32
  Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Programming Language :: Python :: 3
14
33
  Classifier: Programming Language :: Python :: 3.10
15
- Classifier: Programming Language :: Python :: 3.11
16
- Classifier: Programming Language :: Python :: 3.12
17
- Classifier: Programming Language :: Python :: 3.13
18
34
  Classifier: Topic :: Software Development :: Libraries
19
35
  Requires-Dist: colorama
20
36
  Requires-Dist: ffmpeg
@@ -29,9 +45,10 @@ Requires-Dist: screeninfo
29
45
  Requires-Dist: tqdm
30
46
  Requires-Dist: trimesh
31
47
  Project-URL: Bug Tracker, https://github.com/MGross21/mujoco-toolbox/issues
32
- Project-URL: documentation, https://github.com/MGross21/mujoco-toolbox
33
- Project-URL: homepage, https://github.com/MGross21/mujoco-toolbox
34
- Project-URL: repository, https://github.com/MGross21/mujoco-toolbox
48
+ Project-URL: Documentation, https://github.com/MGross21/mujoco-toolbox
49
+ Project-URL: Homepage, https://github.com/MGross21/mujoco-toolbox
50
+ Project-URL: Repository, https://github.com/MGross21/mujoco-toolbox
51
+ Project-URL: changelog, https://github.com/MGross21/mujoco-toolbox/blob/main/CHANGELOG.md
35
52
  Description-Content-Type: text/markdown
36
53
 
37
54
  ![Build](https://github.com/MGross21/mujoco-toolbox/actions/workflows/ci.yml/badge.svg)
@@ -97,27 +114,26 @@ ffmpeg -version
97
114
 
98
115
  </details>
99
116
 
100
- ## To Run in Scripts
117
+ ## Example Script
118
+
119
+ *Bare minimum to run MuJoCo simulation and display result*
101
120
 
102
121
  ```python
103
122
  import mujoco_toolbox as mjtb
104
123
 
105
- # Optional Global Variables
106
- mjtb.CAPTURE_PARAMETERS = "all" # MjData Simulation Parameters (Default = ['time', 'qpos', 'qvel', 'act', 'qacc', 'xpos', 'xquat', 'xmat', 'ctrl', 'sensordata'])
107
-
108
- # Example Code
109
- mjtb.Wrapper("path/to/your/xml").runSim(render=True).renderMedia()
124
+ mjtb.Wrapper("path/to/your/xml").run(render=True).save()
110
125
  ```
111
126
 
112
127
  ## Pre-Made Controllers
113
128
 
114
129
  ```python
115
- from mujoco_toolbox import \
116
- sineController,\
117
- cosineController,\
118
- stepController,\
119
- randomController,\
120
- realTimeController
130
+ from mujoco_toolbox.controller import (
131
+ cosine_controller,
132
+ random_controller,
133
+ live_controller,
134
+ sine_controller,
135
+ step_controller,
136
+ )
121
137
 
122
138
  # Wrapper can use custom controllers as well!
123
139
  ```
@@ -131,3 +147,4 @@ realTimeController
131
147
  ### URDF
132
148
 
133
149
  ![UR5](assets/images/ur5_render_no_gui.png)
150
+
@@ -61,27 +61,26 @@ ffmpeg -version
61
61
 
62
62
  </details>
63
63
 
64
- ## To Run in Scripts
64
+ ## Example Script
65
+
66
+ *Bare minimum to run MuJoCo simulation and display result*
65
67
 
66
68
  ```python
67
69
  import mujoco_toolbox as mjtb
68
70
 
69
- # Optional Global Variables
70
- mjtb.CAPTURE_PARAMETERS = "all" # MjData Simulation Parameters (Default = ['time', 'qpos', 'qvel', 'act', 'qacc', 'xpos', 'xquat', 'xmat', 'ctrl', 'sensordata'])
71
-
72
- # Example Code
73
- mjtb.Wrapper("path/to/your/xml").runSim(render=True).renderMedia()
71
+ mjtb.Wrapper("path/to/your/xml").run(render=True).save()
74
72
  ```
75
73
 
76
74
  ## Pre-Made Controllers
77
75
 
78
76
  ```python
79
- from mujoco_toolbox import \
80
- sineController,\
81
- cosineController,\
82
- stepController,\
83
- randomController,\
84
- realTimeController
77
+ from mujoco_toolbox.controller import (
78
+ cosine_controller,
79
+ random_controller,
80
+ live_controller,
81
+ sine_controller,
82
+ step_controller,
83
+ )
85
84
 
86
85
  # Wrapper can use custom controllers as well!
87
86
  ```
@@ -94,4 +93,4 @@ realTimeController
94
93
 
95
94
  ### URDF
96
95
 
97
- ![UR5](assets/images/ur5_render_no_gui.png)
96
+ ![UR5](assets/images/ur5_render_no_gui.png)
@@ -3,13 +3,14 @@
3
3
  A toolbox for working with MuJoCo simulations. This package provides various utilities and controllers to facilitate
4
4
  the simulation process.
5
5
 
6
- Modules:
6
+ Modules
7
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.
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.
11
12
 
12
- Constants:
13
+ Constants
13
14
  ----------
14
15
  - CAPTURE_PARAMETERS: List of MjData fields to capture during simulation.
15
16
 
@@ -19,13 +20,19 @@ This package is still under development. Report any issues to https://github.com
19
20
 
20
21
  """
21
22
 
22
- from .assets import WORLD_ASSETS, GloveBox
23
+ from .assets import WORLD_ASSETS, glovebox
23
24
  from .builder import Builder
24
- from .controller import cosineController, randomController, realTimeController, sineController, stepController
25
- from .utils import _Platform, print_success, print_warning, timer
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
26
33
  from .wrapper import Wrapper
27
34
 
28
- __version__ = "0.2.2"
35
+ __version__ = "0.3.3"
29
36
  __author__ = "Michael Gross"
30
37
  __github_repo__ = "mujoco-toolbox"
31
38
  __license__ = "MIT"
@@ -35,18 +42,18 @@ __all__ = [
35
42
  "COMPUTER",
36
43
  "WORLD_ASSETS",
37
44
  "Builder",
38
- "GloveBox",
39
45
  "Wrapper",
40
- "cosineController",
41
- "randomController",
42
- "realTimeController",
43
- "sineController",
44
- "stepController",
45
- "timer",
46
+ "cosine_controller",
47
+ "glovebox",
48
+ "live_controller",
49
+ "random_controller",
50
+ "sine_controller",
51
+ "step_controller",
46
52
  ]
47
53
 
48
54
  COMPUTER = _Platform() # Singleton Instance
49
55
  MAX_GEOM_SCALAR: int = 2 # Scalar value for mujoco.Renderer.max_geom
56
+ PROGRESS_BAR: bool = False # Enable/Disable progress bar
50
57
  CAPTURE_PARAMETERS = [ # MjData default fields to capture during simulation
51
58
  "time",
52
59
  "qpos",
@@ -60,8 +67,11 @@ CAPTURE_PARAMETERS = [ # MjData default fields to capture during simulation
60
67
  "sensordata",
61
68
  ]
62
69
 
63
- if __version__.split(".")[0] == "0":
64
- print_warning(
70
+ if __version__.startswith("0"):
71
+ from .utils import _print_warning
72
+ _print_warning(
65
73
  f"{__package__} (v{__version__}) is still under development.",
66
74
  f"Report any issues to https://github.com/MGross21/{__github_repo__}/issues",
67
75
  )
76
+ del _print_warning
77
+ del _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) -> str:
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,11 +3,44 @@ import xml.etree.ElementTree as ET
3
3
 
4
4
 
5
5
  class Builder:
6
- def __init__(self, xml_input: str) -> None:
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:
7
25
  """Initialize with an XML string or a file path. Input is required."""
8
- self.tree, self.root = self.load_model(xml_input)
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)
9
42
 
10
- def load_model(self, xml_input):
43
+ def _load_model(self, xml_input):
11
44
  """Load a MuJoCo model from a file or XML string."""
12
45
  if not isinstance(xml_input, str):
13
46
  msg = "Input must be either an XML string or a file path"
@@ -22,7 +55,7 @@ class Builder:
22
55
 
23
56
  # If it's a URDF (robot tag), wrap it in mujoco
24
57
  if root.tag == "robot":
25
- return self.wrap_urdf_in_mujoco(tree)
58
+ return self._wrap_urdf_in_mujoco(tree)
26
59
 
27
60
  # If root is not mujoco, wrap it
28
61
  if root.tag != "mujoco":
@@ -42,7 +75,7 @@ class Builder:
42
75
 
43
76
  # Handle URDF files
44
77
  if xml_input.lower().endswith(".urdf"):
45
- return self.wrap_urdf_in_mujoco(ET.parse(xml_input))
78
+ return self._wrap_urdf_in_mujoco(ET.parse(xml_input))
46
79
 
47
80
  # Parse regular XML file
48
81
  try:
@@ -60,7 +93,7 @@ class Builder:
60
93
  msg = f"Invalid XML file: {xml_input}"
61
94
  raise ValueError(msg)
62
95
 
63
- def wrap_urdf_in_mujoco(self, urdf_input):
96
+ def _wrap_urdf_in_mujoco(self, urdf_input):
64
97
  """Wrap a URDF inside <mujoco> tags for MuJoCo compatibility.
65
98
  Input can be a file path, ElementTree, or XML string.
66
99
  """
@@ -101,7 +134,7 @@ class Builder:
101
134
  # Create a new tree with the <mujoco> root
102
135
  return ET.ElementTree(mujoco_elem), mujoco_elem
103
136
 
104
- def merge_tags(self, tag_name, root_1, root_2) -> None:
137
+ def _merge_tags(self, tag_name, root_1, root_2) -> None:
105
138
  """Merge a specific tag (e.g., worldbody, asset) from two models."""
106
139
  section_1 = root_1.find(tag_name)
107
140
  section_2 = root_2.find(tag_name)
@@ -125,7 +158,7 @@ class Builder:
125
158
  model_root = other.root
126
159
  elif isinstance(other, str):
127
160
  # If the input is a string, load as XML
128
- _, model_root = self.load_model(other)
161
+ _, model_root = self._load_model(other)
129
162
  else:
130
163
  msg = "Addition only supported between Builder objects or with XML strings/files"
131
164
  raise TypeError(msg)
@@ -136,7 +169,7 @@ class Builder:
136
169
 
137
170
  # Merge all relevant sections
138
171
  for section in sections_to_merge:
139
- self.merge_tags(section, self.root, model_root)
172
+ self._merge_tags(section, self.root, model_root)
140
173
 
141
174
  # Return self to allow for method chaining
142
175
  return self
@@ -147,7 +180,7 @@ class Builder:
147
180
  return self
148
181
  return self.__add__(other)
149
182
 
150
- def save(self, file_path) -> None:
183
+ def save(self, file_path) -> "os.path":
151
184
  """Save the merged model to a file."""
152
185
  if self.tree is not None:
153
186
  # Format the XML with proper indentation before saving
@@ -156,6 +189,7 @@ class Builder:
156
189
  else:
157
190
  msg = "No model loaded. Cannot save."
158
191
  raise ValueError(msg)
192
+ return os.path.abspath(file_path)
159
193
 
160
194
  def _indent_xml(self, elem, level=0) -> None:
161
195
  """Add proper indentation to make the XML file more readable."""
@@ -185,7 +219,11 @@ class Builder:
185
219
  """Return the XML string of the model."""
186
220
  return self.__str__()
187
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
+
188
226
  @property
189
227
  def xml(self):
190
- """Property to quickly grab the XML string of the model."""
228
+ """XML string of the model."""
191
229
  return str(self)
@@ -29,7 +29,7 @@ def _apply_control(model, data, value, joint=None, axis=None, delay=0) -> None:
29
29
  data.qpos[axis] = value
30
30
 
31
31
 
32
- def stepController(model, data, **kwargs) -> None:
32
+ def step_controller(model, data, **kwargs) -> None:
33
33
  """A step controller for the simulation.
34
34
 
35
35
  Args:
@@ -57,7 +57,7 @@ def stepController(model, data, **kwargs) -> None:
57
57
  _apply_control(model, data, amplitude, joint=joint, axis=axis, delay=delay)
58
58
 
59
59
 
60
- def sineController(model, data, **kwargs) -> None:
60
+ def sine_controller(model, data, **kwargs) -> None:
61
61
  """A simple sine wave controller for the simulation.
62
62
 
63
63
  Args:
@@ -85,7 +85,7 @@ def sineController(model, data, **kwargs) -> None:
85
85
  _apply_control(model, data, value, joint=joint, delay=delay)
86
86
 
87
87
 
88
- def cosineController(model, data, **kwargs) -> None:
88
+ def cosine_controller(model, data, **kwargs) -> None:
89
89
  """A simple cosine wave controller for the simulation.
90
90
 
91
91
  Args:
@@ -113,7 +113,7 @@ def cosineController(model, data, **kwargs) -> None:
113
113
  _apply_control(model, data, value, joint=joint, delay=delay)
114
114
 
115
115
 
116
- def randomController(model, data, **kwargs) -> None:
116
+ def random_controller(model, data, **kwargs) -> None:
117
117
  """A random controller for the simulation.
118
118
 
119
119
  Args:
@@ -141,7 +141,7 @@ def randomController(model, data, **kwargs) -> None:
141
141
  value = amplitude * random.rand()
142
142
  _apply_control(model, data, value, joint=joint, axis=axis, delay=delay)
143
143
 
144
- def realTimeController(model, data, **kwargs) -> None:
144
+ def live_controller(model, data, **kwargs) -> None:
145
145
  """A real-time controller for the simulation.
146
146
 
147
147
  Args:
@@ -151,12 +151,12 @@ def realTimeController(model, data, **kwargs) -> None:
151
151
  None
152
152
 
153
153
  """
154
- from .utils import print_warning
154
+ from .utils import _print_warning
155
155
  for key, value in kwargs.get("controller_params", {}).items():
156
156
  if hasattr(data, key):
157
157
  setattr(data, key, value)
158
158
  else:
159
- print_warning(f"'{key}' is not a valid attribute of MjData. Skipping...")
159
+ _print_warning(f"'{key}' is not a valid attribute of MjData. Skipping...")
160
160
 
161
161
  # untested
162
162
  # if hasattr(data, 'control'):
@@ -20,7 +20,7 @@ def timer(func):
20
20
 
21
21
  return time_wrapper
22
22
 
23
- def print_warning(*args, prefix: bool = True) -> None:
23
+ def _print_warning(*args, prefix: bool = True) -> None:
24
24
  """Prints the first argument as an orange warning message and prints the other arguments in regular text, all on the same line.
25
25
 
26
26
  Args:
@@ -49,7 +49,7 @@ def print_warning(*args, prefix: bool = True) -> None:
49
49
  print(f"\033[38;5;214m{message}\033[0m", end=" ") # Orange ANSI escape code
50
50
  print(*args[1:])
51
51
 
52
- def print_success(*args, prefix: bool = True) -> None:
52
+ def _print_success(*args, prefix: bool = True) -> None:
53
53
  """Prints the first argument as a green success message and prints the other arguments in regular text, all on the same line.
54
54
 
55
55
  Args:
@@ -94,12 +94,12 @@ class _Platform:
94
94
  def __repr__(self) -> str:
95
95
  return (
96
96
  f"Operating System: {self.OS}\n"
97
+ f"Architecture: {self.ARCHITECTURE}\n"
97
98
  f"Processor: {self.PROCESSOR}\n"
98
99
  f"Machine Type: {self.MACHINE}\n"
100
+ f"CPU Count: {self.CPU_COUNT}\n"
99
101
  f"Python Version: {self.PYTHON_VERSION}\n"
100
102
  f"IDE: {self.IDE}\n"
101
- f"CPU Count: {self.CPU_COUNT}\n"
102
- f"Architecture: {self.ARCHITECTURE}\n"
103
103
  f"Python Implementation: {self.PYTHON_IMPLEMENTATION}\n"
104
104
  f"Resolution (disp0): {self.RESOLUTION}\n"
105
105
  f"GUI Enabled: {self.GUI_ENABLED}"
@@ -121,5 +121,14 @@ class _Platform:
121
121
  monitor = get_monitors()[0]
122
122
  return (monitor.width, monitor.height), True
123
123
  except Exception:
124
- print_warning("Detected headless operation. Disabling GUI...")
124
+ _print_warning("Detected headless operation. Disabling GUI...")
125
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
@@ -4,10 +4,10 @@ import threading
4
4
  import time
5
5
  import xml.etree.ElementTree as ET
6
6
  from collections import defaultdict
7
+ from collections.abc import Callable
7
8
  from functools import lru_cache
8
- from typing import Any, Callable, Optional, TypeAlias
9
+ from typing import Any, TypeAlias
9
10
 
10
- import matplotlib.pyplot as plt
11
11
  import mediapy as media
12
12
  import mujoco
13
13
  import mujoco.viewer
@@ -15,7 +15,7 @@ import numpy as np
15
15
  import trimesh
16
16
  import yaml
17
17
 
18
- from .utils import print_warning
18
+ from .utils import _print_warning
19
19
 
20
20
  assert sys.version_info >= (3, 10), "This code requires Python 3.10.0 or later."
21
21
  assert mujoco.__version__ >= "2.0.0", "This code requires MuJoCo 2.0.0 or later."
@@ -27,17 +27,23 @@ mjData: TypeAlias = mujoco.MjData
27
27
  class Wrapper:
28
28
  """A class to handle MuJoCo simulations."""
29
29
 
30
- def __init__(self, xml:str, duration:int=10, fps:int=30, resolution:tuple[int,int]=(400,300), initialConditions:Optional[dict[str, list]]=None, controller:Optional[Callable[[mjModel, mjData, Any], None]]=None, *args, **kwargs) -> None:
30
+ def __init__(self,
31
+ xml:str,
32
+ duration:int=10,
33
+ data_rate:int=100,
34
+ fps:int=30,
35
+ resolution:tuple[int,int]=(400,300),
36
+ initial_conditions:dict[str, list] | None=None,
37
+ controller:Callable[[mjModel, mjData, Any], None] | None=None,
38
+ *args,
39
+ **kwargs) -> None:
31
40
  # xml = "<mujoco></mujoco>" if xml.strip() == "<mujoco/>" else xml
32
- if initialConditions is None:
33
- initialConditions = {}
34
- assert xml is not None, "XML file or string is required to initialize the Wrapper."
35
41
  self._load_model(xml, **kwargs)
36
42
 
37
43
  self.duration = duration
38
44
  self.fps = fps
45
+ self.data_rate = data_rate
39
46
  self.resolution = resolution # recursively sets width and height
40
- self.init_conditions = initialConditions
41
47
  self.controller = controller
42
48
 
43
49
  # Predefined simulation parameters but can be overridden
@@ -46,6 +52,7 @@ class Wrapper:
46
52
  self.gravity = kwargs.get("gravity", self._model.opt.gravity)
47
53
 
48
54
  self._data = mujoco.MjData(self._model)
55
+ self.init_conditions = initial_conditions or {} # MUST BE AFTER DATA INITIALIZATION
49
56
 
50
57
  # Auto-Populate the names of bodies, joints, and actuators
51
58
  self._body_names = [self._model.body(i).name for i in range(self._model.nbody)]
@@ -58,11 +65,10 @@ class Wrapper:
58
65
  def _load_model(self, xml: str, **kwargs: Any) -> None:
59
66
  """Load a MuJoCo model from an XML file or a string."""
60
67
  try:
61
- # Convert the XML path to an absolute path
62
- xml_path = os.path.abspath(xml)
63
-
64
68
  # Check if the path exists
65
- if os.path.exists(xml_path):
69
+ if os.path.exists(xml):
70
+ # Convert the XML path to an absolute path
71
+ xml_path = os.path.abspath(xml)
66
72
  # Extract and validate file extension
67
73
  extension = os.path.splitext(xml_path)[1].lower()[1:]
68
74
 
@@ -72,33 +78,24 @@ class Wrapper:
72
78
  self._load_urdf_file(xml_path, **kwargs)
73
79
  else:
74
80
  msg = f"Unsupported file extension: '{extension}'. Please provide an XML or URDF file."
75
- raise ValueError(
76
- msg,
77
- )
78
- # If the file doesn't exist, assume it's a string and attempt to load it as XML
79
- elif "<mujoco>" in xml or "<mujoco/>" in xml or "<robot>" in xml:
80
- self._load_xml_string(xml)
81
+ raise ValueError(msg)
81
82
  else:
82
- msg = f"Model file not found: {xml_path}. Ensure the file path is correct and accessible."
83
- raise FileNotFoundError(
84
- msg,
85
- )
86
-
83
+ # If the file doesn't exist, assume it's a string and attempt to load it as XML
84
+ try:
85
+ ET.fromstring(xml) # Try parsing the string as XML
86
+ self._load_xml_string(xml)
87
+ except ET.ParseError:
88
+ msg = f"Model not able to be loaded from the provided XML string: {xml}"
89
+ raise ValueError(msg)
87
90
  except FileNotFoundError as e:
88
91
  msg = f"Failed to load the MuJoCo model: {e}"
89
92
  raise FileNotFoundError(msg) from e
90
-
91
93
  except ValueError as e:
92
94
  msg = f"Invalid value encountered while loading the model: {e}"
93
- raise ValueError(
94
- msg,
95
- ) from e
96
-
95
+ raise ValueError(msg) from e
97
96
  except Exception as e:
98
97
  msg = f"Unexpected error while loading the MuJoCo model: {e}"
99
- raise Exception(
100
- msg,
101
- )
98
+ raise Exception(msg)
102
99
 
103
100
  def _load_xml_file(self, xml: str, **kwargs) -> None:
104
101
  """Load a MuJoCo model from an XML file."""
@@ -119,6 +116,7 @@ class Wrapper:
119
116
 
120
117
  def _load_urdf_file(self, urdf_path: str, **kwargs: Any) -> None:
121
118
  """Process and load a URDF file for use with MuJoCo."""
119
+ # BUG: Generate actuators from joints in URDF so the controller can actually work
122
120
 
123
121
  def convert_dae_to_stl(meshdir: str) -> None:
124
122
  """Convert all DAE files in a directory (including subdirectories) to STL."""
@@ -241,13 +239,13 @@ class Wrapper:
241
239
  thread.join()
242
240
 
243
241
  @property
244
- def model(self) -> mujoco.MjModel:
242
+ def model(self) -> mjModel:
245
243
  """Read-only property to access the MjModel object."""
246
244
  return self._model
247
245
 
248
246
  @property
249
- def data(self) -> mujoco.MjData:
250
- """Read-only property to access the MjData single-step object."""
247
+ def data(self) -> mjData:
248
+ """Read-only property to access the MjData single-step object. Use `captured_data` to access the entire simulation data."""
251
249
  return self._data
252
250
 
253
251
  @property
@@ -272,13 +270,14 @@ class Wrapper:
272
270
 
273
271
  @frames.deleter
274
272
  def frames(self) -> None:
273
+ # BUG: This is not working as intended
275
274
  self._frames.clear()
276
275
  import gc
277
-
278
276
  gc.collect()
279
277
 
280
278
  @property
281
279
  def duration(self) -> float:
280
+ """Duration of the simulation in seconds."""
282
281
  return self._duration
283
282
 
284
283
  @duration.setter
@@ -290,6 +289,7 @@ class Wrapper:
290
289
 
291
290
  @property
292
291
  def fps(self) -> float:
292
+ """Frames per second."""
293
293
  return self._fps
294
294
 
295
295
  @fps.setter
@@ -301,6 +301,7 @@ class Wrapper:
301
301
 
302
302
  @property
303
303
  def resolution(self) -> tuple[int, int]:
304
+ """Resolution of the simulation in pixels."""
304
305
  return self._resolution
305
306
 
306
307
  @resolution.setter
@@ -326,27 +327,33 @@ class Wrapper:
326
327
  self._model.vis.Global.offheight = self._height
327
328
 
328
329
  @property
329
- def init_conditions(self):
330
+ def initial_conditions(self):
331
+ """Initial conditions for the simulation."""
330
332
  return self._initcond
331
333
 
332
- @init_conditions.setter
333
- def init_conditions(self, values) -> None:
334
+ @initial_conditions.setter
335
+ def initial_conditions(self, values) -> None:
334
336
  if not isinstance(values, dict):
335
337
  msg = "Initial conditions must be a dictionary."
336
338
  raise ValueError(msg)
337
- invalid_keys = [
338
- key for key in values if not hasattr(mujoco.MjData(self._model), key)
339
- ]
339
+
340
+ invalid_keys = [key for key in values if not hasattr(mujoco.MjData(self._model), key)]
340
341
  if invalid_keys:
341
- SimulationData._get_public_keys(self._data)
342
+ _SimulationData._get_public_keys(self._data)
342
343
  msg = f"Invalid initial condition attributes: {', '.join(invalid_keys)}"
343
- raise ValueError(
344
- msg,
345
- )
344
+ raise ValueError(msg)
345
+
346
346
  self._initcond = values
347
347
 
348
+ # Apply initial conditions directly when set
349
+ for key, value in values.items():
350
+ if hasattr(self._data, key):
351
+ setattr(self._data, key, value)
352
+ else:
353
+ _print_warning(f"'{key}' is not a valid attribute of MjData.")
354
+
348
355
  @property
349
- def controller(self) -> Optional[Callable[[mjModel, mjData, Any], None]]:
356
+ def controller(self) -> Callable[[mjModel, mjData, Any], None] | None:
350
357
  """Controller Function."""
351
358
  return self._controller
352
359
 
@@ -359,6 +366,7 @@ class Wrapper:
359
366
 
360
367
  @property
361
368
  def ts(self) -> float:
369
+ """Timestep of the simulation in seconds."""
362
370
  return self._model.opt.timestep
363
371
 
364
372
  @ts.setter
@@ -370,17 +378,18 @@ class Wrapper:
370
378
 
371
379
  @property
372
380
  def data_rate(self) -> int:
381
+ """Data rate of the simulation in frames per second."""
373
382
  try:
374
383
  return self._dr
375
384
  except AttributeError:
376
- print_warning(f"Use '{self.runSim.__name__}' first in order to access this value.")
385
+ _print_warning(f"Use '{self.run.__name__}' first in order to access this value.")
377
386
  return None
378
387
 
379
388
  @data_rate.setter
380
- def data_rate(self, value) -> None:
389
+ def data_rate(self, value: int) -> None:
381
390
  if isinstance(value, float) and not isinstance(value, int):
382
391
  value = round(value)
383
- print_warning(f"Data rate must be an integer. Rounding to the nearest integer ({value}).")
392
+ _print_warning(f"Data rate must be an integer. Rounding to the nearest integer ({value}).")
384
393
  if value <= 0:
385
394
  msg = "Data rate must be greater than 0."
386
395
  raise ValueError(msg)
@@ -393,23 +402,21 @@ class Wrapper:
393
402
 
394
403
  @property
395
404
  def gravity(self):
405
+ """Gravity vector of the simulation."""
396
406
  return self._model.opt.gravity
397
407
 
398
408
  @gravity.setter
399
- def gravity(self, values) -> None:
400
- if len(values) != 3:
409
+ def gravity(self, values: list | tuple | np.ndarray) -> None:
410
+ if not isinstance(values, (list, tuple, np.ndarray)) or len(values) != 3:
401
411
  msg = "Gravity must be a 3D vector."
402
412
  raise ValueError(msg)
403
- self._model.opt.gravity = values
404
-
405
- def _setInitialConditions(self) -> None:
406
- for key, value in self._initcond.items():
407
- if hasattr(self._data, key):
408
- setattr(self._data, key, value)
409
- else:
410
- print_warning(f"'{key}' is not a valid attribute of MjData.")
413
+ self._model.opt.gravity = np.array(values)
411
414
 
412
- def runSim(self, render: bool = False, camera: Optional[str] = None, data_rate: int = 100, interactive: bool = False, multi_thread: bool = False) -> "Wrapper":
415
+ def run(self,
416
+ render: bool = False,
417
+ camera: str | None = None,
418
+ interactive: bool = False,
419
+ multi_thread: bool = False) -> "Wrapper":
413
420
  """Run the simulation with optional rendering and controlled data capture.
414
421
 
415
422
  Args:
@@ -428,26 +435,19 @@ class Wrapper:
428
435
  try:
429
436
  mujoco.set_mjcb_control(self._controller) if self._controller else None
430
437
  mujoco.mj_resetData(self._model, self._data)
431
- self._setInitialConditions()
432
438
 
433
439
  # self._frames = [None] * (self.duration * self.fps + 1)
434
440
  frames = []
435
- sim_data = SimulationData()
441
+ sim_data = _SimulationData()
436
442
  total_steps = int(self._duration / self.ts)
437
443
 
438
444
  # Cache frequently used functions and objects for performance
439
445
  mj_step = mujoco.mj_step
440
446
  m = self._model
441
447
  d = self._data
442
- h = self._height
443
- w = self._width
444
448
  dur = self._duration
445
- num_geoms = self._geom_names.__len__()
446
449
 
447
- # TODO: Fix references to data rate in future.
448
- # May be desired to specify in object creation
449
- self._dr = data_rate
450
- capture_rate = data_rate * self.ts
450
+ capture_rate = self.data_rate * self.ts
451
451
  capture_interval = max(1, int(1.0 / capture_rate))
452
452
  render_interval = max(1, int(1.0 / (self._fps * self.ts)))
453
453
 
@@ -459,19 +459,22 @@ class Wrapper:
459
459
  # TODO: Implement multi-threading
460
460
  pass
461
461
 
462
- from . import COMPUTER, MAX_GEOM_SCALAR
463
- if COMPUTER.IDE == "jupyter":
464
- from tqdm.notebook import tqdm as bar
462
+ from . import COMPUTER, MAX_GEOM_SCALAR, PROGRESS_BAR
463
+ from .utils import _EmptyContextManager
464
+
465
+ if PROGRESS_BAR and render:
466
+ if COMPUTER.IDE == "jupyter":
467
+ from tqdm.notebook import tqdm as ProgressBar
468
+ else:
469
+ from tqdm import tqdm as ProgressBar
465
470
  else:
466
- from tqdm import tqdm as bar
467
-
468
- with (
469
- bar(total=total_steps,
470
- desc="Simulation",
471
- unit=" step", leave=False,
472
- ) as pbar,
473
- mujoco.Renderer(m, h, w, num_geoms * MAX_GEOM_SCALAR) as renderer,
474
- ):
471
+ ProgressBar = _EmptyContextManager
472
+
473
+ # PEP Convention - Prevent Naming Conflict
474
+ _ProgressBar = ProgressBar(total=total_steps, desc="Simulation", unit=" step", leave=False)
475
+ _Renderer = mujoco.Renderer(self.model, self._height, self._width, self._geom_names.__len__() * MAX_GEOM_SCALAR)
476
+
477
+ with _ProgressBar as pbar, _Renderer as renderer:
475
478
  step = 0
476
479
  while d.time < dur:
477
480
  mj_step(m, d)
@@ -489,7 +492,7 @@ class Wrapper:
489
492
  # frame_count += 1
490
493
  frames.append(renderer.render().copy())
491
494
 
492
- pbar.update(1)
495
+ pbar.update(1) if PROGRESS_BAR else None
493
496
  step += 1
494
497
 
495
498
  # if verbose:
@@ -511,143 +514,196 @@ class Wrapper:
511
514
 
512
515
  return self
513
516
 
514
- def _window(self) -> None:
517
+ def _window(self, show_menu: bool =True) -> None:
515
518
  """Open a window to display the simulation in real time."""
519
+ try:
520
+ m = self._model
521
+ d = self._data
522
+
523
+ def key_callback(key) -> bool:
524
+ return key in (27, ord("q")) # 27 = ESC key, 'q' to quit
525
+
526
+ # NOTE: launch_passive may blocking despite docstring saying otherwise
527
+ _Viewer = mujoco.viewer.launch_passive(m, d,
528
+ show_left_ui=show_menu,
529
+ show_right_ui=show_menu,
530
+ key_callback=key_callback)
531
+ with _Viewer as viewer:
532
+ viewer.sync()
533
+ start_time = time.time()
534
+
535
+ try:
536
+ while viewer.is_running():
537
+ current_time = time.time()
538
+ dt = current_time - start_time # Time difference between frames
539
+
540
+ mujoco.mj_step(m, d) # Advance simulation by one step
541
+ viewer.sync() # Sync the viewer
516
542
 
543
+ start_time = current_time # Reset start_time for the next frame
544
+ time.sleep(max(0, 1.0 / self.fps - dt)) # Adjust sleep to match self.fps
517
545
 
546
+ except KeyboardInterrupt:
547
+ viewer.close()
548
+ except Exception as e:
549
+ msg = "An error occurred while running the simulation."
550
+ raise RuntimeError(msg) from e
551
+ finally:
552
+ mujoco.set_mjcb_control(None)
518
553
 
519
554
  def liveView(self, show_menu: bool = True) -> None:
520
555
  """Open a window to display the simulation in real time."""
521
- # BUG: Generate actuators from joints in URDF so the controller can actually work
556
+ # Run the window in a separate thread
557
+ gui = threading.Thread(target=self._window, kwargs={"show_menu": show_menu})
558
+ gui.start()
522
559
 
523
- def window() -> None:
524
- try:
525
- m = self._model
526
- d = self._data
527
-
528
- def key_callback(key) -> bool:
529
- if key in (27, ord("q")): # 27 = ESC key, 'q' to quit
530
- return True
531
- return False
532
-
533
- # NOTE: launch_passive is blocking despite docstring saying otherwise
534
- with mujoco.viewer.launch_passive(m, d,
535
- show_left_ui=show_menu,
536
- show_right_ui=show_menu,
537
- key_callback=key_callback) as viewer:
538
- viewer.sync()
539
- start_time = time.time()
540
-
541
- try:
542
- while viewer.is_running():
543
- current_time = time.time()
544
- dt = current_time - start_time # Time difference between frames
545
-
546
- mujoco.mj_step(m, d) # Advance simulation by one step
547
- viewer.sync() # Sync the viewer
548
-
549
- start_time = current_time # Reset start_time for the next frame
550
- # Sleep to match real-time simulation speed
551
- time.sleep(max(0, 0.01 - dt)) # Adjust sleep to match real-time
552
- except KeyboardInterrupt:
553
- viewer.close()
554
- except Exception as e:
555
- msg = "An error occurred while running the simulation."
556
- raise RuntimeError(msg) from e
557
- finally:
558
- mujoco.set_mjcb_control(None)
559
-
560
- viewer = threading.Thread(target=window)
561
- viewer.start()
562
-
563
- def renderFrame(self, t=0, frame=0, title=None) -> Optional[str]:
564
- """Render a specific frame as an image.
560
+ def _validate_and_extract_frames(
561
+ self,
562
+ frame_idx: int | tuple[int, int] | None = None,
563
+ time_idx: float | tuple[float, float] | None = None,
564
+ ) -> list[Any]:
565
+ """Validate and extract frames based on frame or time indices.
565
566
 
566
567
  Args:
567
- t (float): Time in seconds for which the frame should be rendered.
568
- frame (int): The frame index to render.
569
- title (str): The title of the rendered frame.
568
+ frame_idx (int or tuple, optional): Single frame index or (start, stop) frame indices.
569
+ time_idx (float or tuple, optional): Single time or (start, end) times in seconds.
570
570
 
571
571
  Returns:
572
- None
572
+ List of frames
573
+
574
+ Raises:
575
+ ValueError: For invalid input parameters.
573
576
 
574
577
  """
575
578
  if not self._frames:
576
579
  msg = "No frames captured to render."
577
580
  raise ValueError(msg)
578
581
 
579
- if t < 0 or frame < 0:
580
- msg = "Time and frame index must be greater than or equal to zero."
581
- raise ValueError(
582
- msg,
583
- )
584
-
585
- if frame and t:
586
- msg = "Can only specify singular time or frame parameter"
582
+ # Validate input parameters
583
+ if frame_idx is not None and time_idx is not None:
584
+ msg = "Can only specify either frame_idx or time_idx, not both."
587
585
  raise ValueError(msg)
588
586
 
589
- try:
590
- if t > 0:
591
- frame = self.t2f(t) # Convert time to frame index
587
+ # If both are None, use all frames
588
+ if frame_idx is None and time_idx is None:
589
+ return self._frames
590
+
591
+ # Handle time index conversion
592
+ if time_idx is not None:
593
+ if isinstance(time_idx, (int, float)):
594
+ frame_idx = self.t2f(time_idx)
595
+ elif isinstance(time_idx, tuple):
596
+ frame_idx = (self.t2f(time_idx[0]), self.t2f(time_idx[1]))
592
597
  else:
593
- frame = int(frame)
598
+ msg = "time_idx must be a number or a tuple of numbers."
599
+ raise ValueError(msg)
594
600
 
595
- plt.imshow(self._frames[frame])
596
- plt.axis("off")
597
- plt.title(title or f"Frame {frame}", loc="center")
598
- plt.show()
601
+ # Convert single index to tuple range
602
+ if isinstance(frame_idx, (int, float)):
603
+ frame_idx = (frame_idx, frame_idx + 1)
599
604
 
600
- except IndexError as e:
601
- msg = f"Invalid frame index: {e}"
605
+ # Validate frame indices
606
+ start, stop = frame_idx
607
+ max_frames = len(self._frames)
608
+
609
+ if start < 0:
610
+ msg = f"Start index must be non-negative. Got {start}."
602
611
  raise ValueError(msg)
603
- except TypeError as e:
604
- msg = f"Invalid type for time or frame: {e}"
612
+ if stop > max_frames:
613
+ msg = f"Stop index must not exceed total frames ({max_frames}). Got {stop}."
605
614
  raise ValueError(msg)
606
- except Exception as e:
607
- msg = f"Unexpected error while rendering frame: {e}"
608
- raise RuntimeError(msg)
615
+ if start >= stop:
616
+ msg = f"Start index ({start}) must be less than stop index ({stop})."
617
+ raise ValueError(msg)
618
+
619
+ # Select subset of frames
620
+ return self._frames[start:stop]
609
621
 
610
- def renderMedia(self, codec="gif", title=None, save=False) -> media:
611
- """Render the simulation as a video or GIF, with an option to save to a file.
622
+ def show(
623
+ self,
624
+ title: str | None = None,
625
+ codec: str = "gif",
626
+ frame_idx: int | tuple[int, int] | None = None,
627
+ time_idx: float | tuple[float, float] | None = None,
628
+ ) -> None:
629
+ """Render specific frame(s) as a video or GIF in a window.
612
630
 
613
631
  Args:
614
- codec (str): The media format to use ("gif" or "mp4").
615
- title (str): The filename or window title for the media.
616
- save (bool): Whether to save the media to a file.
632
+ title (str, optional): Title for the rendered media.
633
+ codec (str, optional): Video codec/format. Defaults to "gif".
634
+ frame_idx (int or tuple, optional): Single frame index or (start, stop) frame indices.
635
+ time_idx (float or tuple, optional): Single time or (start, end) times in seconds.
636
+
637
+ Raises:
638
+ ValueError: If no frames are captured or invalid input parameters.
617
639
 
618
640
  """
619
- if not self._frames:
620
- msg = "No frames captured to create media."
621
- raise ValueError(msg)
622
- # Display the media in a window
623
- if not save:
641
+ try:
642
+ # Extract frames
643
+ subset_frames = self._validate_and_extract_frames(
644
+ frame_idx=frame_idx,
645
+ time_idx=time_idx,
646
+ )
647
+
648
+ # Show the video
624
649
  media.show_video(
625
- self._frames,
626
- fps=self._fps,
650
+ subset_frames,
651
+ fps=1 if len(subset_frames) == 1 else self._fps,
627
652
  width=self._width,
628
653
  height=self._height,
629
654
  codec=codec,
630
655
  title=title,
631
656
  )
632
- return None
657
+ except Exception as e:
658
+ msg = "Error while showing video subset."
659
+ raise Exception(msg) from e
660
+
661
+ def save(
662
+ self,
663
+ title: str = "render",
664
+ codec: str = "gif",
665
+ frame_idx: int | tuple[int, int] | None = None,
666
+ time_idx: float | tuple[float, float] | None = None,
667
+ ) -> str:
668
+ """Save specific frame(s) as a video or GIF to a file.
669
+
670
+ Args:
671
+ title (str, optional): Filename for the saved media.
672
+ codec (str, optional): Video codec/format. Defaults to "gif".
673
+ frame_idx (int or tuple, optional): Single frame index or (start, stop) frame indices.
674
+ time_idx (float or tuple, optional): Single time or (start, end) times in seconds.
675
+
676
+ Returns:
677
+ str: Absolute path to the saved file.
678
+
679
+ Raises:
680
+ ValueError: If no frames are captured or invalid input parameters.
633
681
 
634
- if not title.endswith(f".{codec}"):
635
- title += f".{codec}"
682
+ """
683
+ try:
684
+ # Extract frames
685
+ subset_frames = self._validate_and_extract_frames(
686
+ frame_idx=frame_idx,
687
+ time_idx=time_idx,
688
+ )
636
689
 
637
- # Save the frames to the specified file
638
- available_codecs = ["gif", "mp4", "h264", "hevc", "vp9"]
639
- if codec in available_codecs:
690
+ # Ensure the title ends with the correct codec extension
691
+ if not title.endswith(f".{codec}"):
692
+ title += f".{codec}"
693
+
694
+ # Save the video
640
695
  media.write_video(
641
- title if not None else "render",
642
- self._frames,
643
- fps=self._fps,
696
+ title,
697
+ subset_frames,
698
+ fps=1 if len(subset_frames) == 1 else self._fps,
644
699
  codec=codec,
645
700
  )
646
- else:
647
- msg = f"Unsupported codec '{codec}'. Supported codecs are {', '.join(available_codecs)}"
648
- raise ValueError(msg)
649
701
 
650
- return os.path.abspath(title)
702
+ return os.path.abspath(title)
703
+
704
+ except Exception as e:
705
+ msg = "Error while saving video subset."
706
+ raise Exception(msg) from e
651
707
 
652
708
  @lru_cache(maxsize=100)
653
709
  def t2f(self, t: float) -> int:
@@ -661,7 +717,7 @@ class Wrapper:
661
717
  """Convert frame index to time."""
662
718
  return frame / self._fps
663
719
 
664
- def getBodyData(self, body_name: str, data_name: Optional[str] = None) -> np.ndarray:
720
+ def getBodyData(self, body_name: str, data_name: str | None = None) -> np.ndarray:
665
721
  """Get the data for a specific body in the simulation.
666
722
 
667
723
  Args:
@@ -727,7 +783,7 @@ class Wrapper:
727
783
  raise ValueError(msg) from e
728
784
 
729
785
 
730
- class SimulationData:
786
+ class _SimulationData:
731
787
  """A class to store and manage simulation data."""
732
788
 
733
789
  __slots__ = ["_d"]
@@ -1,48 +1,53 @@
1
1
  [build-system]
2
- requires = ["poetry-core"]
2
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
3
3
  build-backend = "poetry.core.masonry.api"
4
4
 
5
- [tool.poetry]
5
+ [project]
6
6
  name = "mujoco-toolbox"
7
- version = "0.2.2"
7
+ version = "0.3.3"
8
8
  description = "A modern package to handle MuJoCo environments"
9
9
  readme = "README.md"
10
- license = "MIT"
10
+ license = { file = "LICENSE" }
11
11
  packages = [{ include = "mujoco_toolbox" }]
12
- authors = ["MGross21 <MGross21@TODO.com>"]
13
- keywords = ["MuJoCo", "robotics", "simulation", "toolbox", "wrapper"]
12
+ authors = [{ name = "MGross21", email = "MGross21@TODO.com" }]
13
+ keywords = ["MuJoCo", "mujoco", "robotics", "simulation", "toolbox", "wrapper"]
14
14
  classifiers = [
15
- "Development Status :: 3 - Alpha",
15
+ "Development Status :: 4 - Beta",
16
16
  "Intended Audience :: Developers",
17
17
  "License :: OSI Approved :: MIT License",
18
18
  "Programming Language :: Python :: 3.10",
19
19
  "Topic :: Software Development :: Libraries"
20
20
  ]
21
+ requires-python = ">=3.10"
22
+ dependencies = [
23
+ "mujoco",
24
+ "numpy",
25
+ "scipy",
26
+ "matplotlib",
27
+ "pyyaml",
28
+ "mediapy",
29
+ "tqdm",
30
+ "ipywidgets",
31
+ "ffmpeg",
32
+ "trimesh",
33
+ "screeninfo",
34
+ "colorama"
35
+ ]
36
+
37
+ # [project.extras]
38
+ # beautified = ["colorama","ipywidgets","tqdm"]
39
+ # gym = ["gym"]
21
40
 
22
- [tool.poetry.urls]
41
+ [project.urls]
23
42
  homepage = "https://github.com/MGross21/mujoco-toolbox"
24
43
  repository = "https://github.com/MGross21/mujoco-toolbox"
25
44
  documentation = "https://github.com/MGross21/mujoco-toolbox"
45
+ changelog = "https://github.com/MGross21/mujoco-toolbox/blob/main/CHANGELOG.md"
26
46
  "Bug Tracker" = "https://github.com/MGross21/mujoco-toolbox/issues"
27
47
 
28
- [tool.poetry.dependencies]
29
- python = ">=3.10"
30
- mujoco = "*"
31
- numpy = "*"
32
- scipy = "*"
33
- matplotlib = "*"
34
- pyyaml = "*"
35
- mediapy = "*"
36
- tqdm = "*"
37
- ipywidgets = "*"
38
- ffmpeg = "*"
39
- trimesh = "*"
40
- screeninfo = "*"
41
- colorama = "*"
42
-
43
48
  [tool.poetry.group.dev.dependencies]
44
49
  pytest = "*"
45
- ruff = "^0.9.9"
50
+ ruff = "^0.11.2"
46
51
  mypy = "^1.15.0"
47
52
  pytypes = "^1.0b10"
48
53
  black = "^25.1.0"
@@ -54,14 +59,14 @@ pylint = "^3.3.4"
54
59
  [tool.semantic_release]
55
60
  version_variables = [
56
61
  "mujoco_toolbox/__init__.py:__version__",
57
- "pyproject.toml:tool.poetry.version",
62
+ "pyproject.toml:project.version",
58
63
  "CITATION.cff:version"
59
64
  ]
60
- branch = "main"
65
+ branch = "main" # Branch to track for triggering the release process
61
66
  changelog_file = "CHANGELOG.md"
62
67
  build_command = "pip install poetry && poetry build"
63
- dist_path = "dist/"
64
- create_pr = true
68
+ dist_path = "dist/" # Directory where the distribution files will be stored
69
+ create_pr = true # Option to create a pull request when a new release version is created
65
70
  pr_branch_prefix = "release/"
66
71
  upload_to_pypi = true
67
72
  upload_to_release = true
@@ -72,8 +77,12 @@ commit_message = "chore(release): {version} [skip ci]"
72
77
  commit_author = "semantic-release <semantic-release@example.com>"
73
78
 
74
79
  [tool.semantic_release.plugins]
75
- commit_analyzer = "@semantic-release/commit-analyzer"
76
- release_notes_generator = "@semantic-release/release-notes-generator"
80
+ commit_analyzer = "@semantic-release/commit-analyzer" # Analyzes commit messages to determine version bumps
81
+ release_notes_generator = "@semantic-release/release-notes-generator" # Generates release notes based on commit messages
82
+ changelog = "@semantic-release/changelog" # Handles changelog generation
83
+ git = "@semantic-release/git" # Commits changes (version bump, changelog) back to the repo
84
+ github = "@semantic-release/github" # Creates GitHub releases (tags) and adds release notes
85
+ pypi = "@semantic-release/pypi" # Uploads the distribution to PyPI
77
86
 
78
87
  [tool.semantic_release.commit_analyzer]
79
88
  preset = "angular"
@@ -111,7 +120,7 @@ preset_config = {types = [
111
120
  ]}
112
121
 
113
122
  [tool.ruff]
114
- line-length = 120
123
+ line-length = 79 # PEP8 line length
115
124
 
116
125
  [tool.ruff.lint]
117
126
  # select = ["E", "F", "W", "C90"] # Select error, warning, and flake8 rules
File without changes