mujoco-toolbox 0.3.4__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Michael Gross
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.3
2
+ Name: mujoco-toolbox
3
+ Version: 0.3.4
4
+ Summary: A modern package to handle MuJoCo environments
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
27
+ Author: MGross21
28
+ Author-email: MGross21@TODO.com
29
+ Requires-Python: >=3.10
30
+ Classifier: Development Status :: 4 - Beta
31
+ Classifier: Intended Audience :: Developers
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Programming Language :: Python :: 3.10
34
+ Classifier: Topic :: Software Development :: Libraries
35
+ Requires-Dist: colorama
36
+ Requires-Dist: ffmpeg
37
+ Requires-Dist: ipywidgets
38
+ Requires-Dist: matplotlib
39
+ Requires-Dist: mediapy
40
+ Requires-Dist: mujoco
41
+ Requires-Dist: numpy
42
+ Requires-Dist: pyyaml
43
+ Requires-Dist: scipy
44
+ Requires-Dist: screeninfo
45
+ Requires-Dist: tqdm
46
+ Requires-Dist: trimesh
47
+ Project-URL: Bug Tracker, https://github.com/MGross21/mujoco-toolbox/issues
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
52
+ Description-Content-Type: text/markdown
53
+
54
+ ![Build](https://github.com/MGross21/mujoco-toolbox/actions/workflows/ci.yml/badge.svg)
55
+ ![Python](https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12%20|%203.13-blue)
56
+ ![License](https://img.shields.io/github/license/MGross21/mujoco-toolbox)
57
+ [![PyPI](https://github.com/MGross21/mujoco-toolbox/actions/workflows/publish.yml/badge.svg)](https://github.com/MGross21/mujoco-toolbox/actions/workflows/publish.yml)
58
+ [![Docs](https://github.com/MGross21/mujoco-toolbox/actions/workflows/docs.yml/badge.svg)](https://github.com/MGross21/mujoco-toolbox/actions/workflows/docs.yml)
59
+
60
+ # Mujoco Toolbox
61
+
62
+ Streamlines the MuJoCo Physics Simulator
63
+
64
+ ## PyPI Package
65
+
66
+ ```bash
67
+ pip install -U mujoco-toolbox
68
+ ```
69
+
70
+ ## Local Install
71
+
72
+ ```bash
73
+ pip install -U git+https://github.com/MGross21/mujoco-toolbox.git@main
74
+ ```
75
+
76
+ ## Extra Packages
77
+
78
+ <details>
79
+ <summary><b>FFMPEG</b></summary>
80
+
81
+ </br>
82
+
83
+ *Required for [mediapy](https://google.github.io/mediapy/mediapy.html) dependency*
84
+
85
+ **Windows**
86
+
87
+ ```bash
88
+ winget install ffmpeg
89
+ ffmpeg -version
90
+ ```
91
+
92
+ **Linux**
93
+
94
+ ```bash
95
+ sudo apt update && sudo apt install ffmpeg
96
+ ffmpeg -version
97
+ ```
98
+
99
+ **MacOS**
100
+
101
+ *Using Homebrew*
102
+
103
+ ```bash
104
+ brew install ffmpeg
105
+ ffmpeg -version
106
+ ```
107
+
108
+ *Using MacPorts*
109
+
110
+ ```bash
111
+ sudo port install ffmpeg
112
+ ffmpeg -version
113
+ ```
114
+
115
+ </details>
116
+
117
+ ## Example Script
118
+
119
+ *Bare minimum to run MuJoCo simulation and display result*
120
+
121
+ ```python
122
+ import mujoco_toolbox as mjtb
123
+
124
+ mjtb.Wrapper("path/to/your/xml").run(render=True).save()
125
+ ```
126
+
127
+ ## Pre-Made Controllers
128
+
129
+ ```python
130
+ from mujoco_toolbox.controller import (
131
+ cosine_controller,
132
+ random_controller,
133
+ live_controller,
134
+ sine_controller,
135
+ step_controller,
136
+ )
137
+
138
+ # Wrapper can use custom controllers as well!
139
+ ```
140
+
141
+ ## File Support
142
+
143
+ ### XML / MJCF (Native)
144
+
145
+ ![Glovebox](assets/images/glovebox_sample.png)
146
+
147
+ ### URDF
148
+
149
+ ![UR5](assets/images/ur5_render_no_gui.png)
150
+
@@ -0,0 +1,96 @@
1
+ ![Build](https://github.com/MGross21/mujoco-toolbox/actions/workflows/ci.yml/badge.svg)
2
+ ![Python](https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12%20|%203.13-blue)
3
+ ![License](https://img.shields.io/github/license/MGross21/mujoco-toolbox)
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)
6
+
7
+ # Mujoco Toolbox
8
+
9
+ Streamlines the MuJoCo Physics Simulator
10
+
11
+ ## PyPI Package
12
+
13
+ ```bash
14
+ pip install -U mujoco-toolbox
15
+ ```
16
+
17
+ ## Local Install
18
+
19
+ ```bash
20
+ pip install -U git+https://github.com/MGross21/mujoco-toolbox.git@main
21
+ ```
22
+
23
+ ## Extra Packages
24
+
25
+ <details>
26
+ <summary><b>FFMPEG</b></summary>
27
+
28
+ </br>
29
+
30
+ *Required for [mediapy](https://google.github.io/mediapy/mediapy.html) dependency*
31
+
32
+ **Windows**
33
+
34
+ ```bash
35
+ winget install ffmpeg
36
+ ffmpeg -version
37
+ ```
38
+
39
+ **Linux**
40
+
41
+ ```bash
42
+ sudo apt update && sudo apt install ffmpeg
43
+ ffmpeg -version
44
+ ```
45
+
46
+ **MacOS**
47
+
48
+ *Using Homebrew*
49
+
50
+ ```bash
51
+ brew install ffmpeg
52
+ ffmpeg -version
53
+ ```
54
+
55
+ *Using MacPorts*
56
+
57
+ ```bash
58
+ sudo port install ffmpeg
59
+ ffmpeg -version
60
+ ```
61
+
62
+ </details>
63
+
64
+ ## Example Script
65
+
66
+ *Bare minimum to run MuJoCo simulation and display result*
67
+
68
+ ```python
69
+ import mujoco_toolbox as mjtb
70
+
71
+ mjtb.Wrapper("path/to/your/xml").run(render=True).save()
72
+ ```
73
+
74
+ ## Pre-Made Controllers
75
+
76
+ ```python
77
+ from mujoco_toolbox.controller import (
78
+ cosine_controller,
79
+ random_controller,
80
+ live_controller,
81
+ sine_controller,
82
+ step_controller,
83
+ )
84
+
85
+ # Wrapper can use custom controllers as well!
86
+ ```
87
+
88
+ ## File Support
89
+
90
+ ### XML / MJCF (Native)
91
+
92
+ ![Glovebox](assets/images/glovebox_sample.png)
93
+
94
+ ### URDF
95
+
96
+ ![UR5](assets/images/ur5_render_no_gui.png)
@@ -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
@@ -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)