Skip to content

Commit 19329ad

Browse files
fix: add MJCF robot methods to config and update examples
1 parent 07b406a commit 19329ad

File tree

6 files changed

+537
-34
lines changed

6 files changed

+537
-34
lines changed

examples/simulation/main.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
import plotly
1111
from controllers import PIDController
1212
from loguru import logger
13-
from mods import modify_ballbot
13+
from mods import create_ballbot_config
1414
from mujoco.usd import exporter
1515
from scipy.spatial.transform import Rotation
1616
from transformations import compute_motor_torques
1717

1818
from onshape_robotics_toolkit.connect import Client
19+
from onshape_robotics_toolkit.formats import MJCFSerializer
1920
from onshape_robotics_toolkit.graph import KinematicGraph
2021
from onshape_robotics_toolkit.models.document import Document
2122
from onshape_robotics_toolkit.parse import CAD
@@ -315,9 +316,10 @@ def find_best_design_variables(trial):
315316
graph = KinematicGraph.from_cad(cad, use_user_defined_root=True)
316317
ballbot = Robot.from_graph(kinematic_graph=graph, client=client, name="ballbot")
317318

318-
ballbot.set_robot_position(pos=(0, 0, 0.35))
319-
ballbot = modify_ballbot(ballbot)
320-
ballbot.save("ballbot.xml")
319+
# Create config with simulation settings and save
320+
config = create_ballbot_config(position=(0, 0, 0.35))
321+
serializer = MJCFSerializer(config)
322+
serializer.save(ballbot, "ballbot.xml")
321323

322324
model = mujoco.MjModel.from_xml_path(filename="ballbot.xml")
323325
data = mujoco.MjData(model)

examples/simulation/mods.py

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,50 @@
11
from lxml import etree as ET
22

3+
from onshape_robotics_toolkit.formats import MJCFConfig, load_element
34
from onshape_robotics_toolkit.models.mjcf import IMU, Gyro
4-
from onshape_robotics_toolkit.robot import Robot, load_element
55

66

7-
def modify_ballbot(ballbot: Robot) -> Robot:
8-
ballbot.add_light(
7+
def create_ballbot_config(position: tuple[float, float, float] = (0, 0, 0.35)) -> MJCFConfig:
8+
"""
9+
Create an MJCFConfig with all the ballbot simulation settings.
10+
11+
Args:
12+
position: The robot position in world coordinates
13+
14+
Returns:
15+
MJCFConfig configured for ballbot simulation
16+
"""
17+
config = MJCFConfig(position=position)
18+
19+
# Add light
20+
config.add_light(
921
name="light_1",
1022
directional=True,
11-
diffuse=[0.6, 0.6, 0.6],
12-
specular=[0.2, 0.2, 0.2],
13-
pos=[0, 0, 4],
14-
direction=[0, 0, -1],
23+
diffuse=(0.6, 0.6, 0.6),
24+
specular=(0.2, 0.2, 0.2),
25+
pos=(0, 0, 4),
26+
direction=(0, 0, -1),
1527
castshadow=False,
1628
)
1729

18-
ballbot.add_actuator(
30+
# Add actuators with encoders and force sensors
31+
config.add_actuator(
1932
actuator_name="motor_1",
2033
joint_name="Revolute_1",
2134
ctrl_limited=True,
2235
ctrl_range=(-50, 50),
2336
add_encoder=True,
2437
add_force_sensor=True,
2538
)
26-
ballbot.add_actuator(
39+
config.add_actuator(
2740
actuator_name="motor_2",
2841
joint_name="Revolute_2",
2942
ctrl_limited=True,
3043
ctrl_range=(-50, 50),
3144
add_encoder=True,
3245
add_force_sensor=True,
3346
)
34-
ballbot.add_actuator(
47+
config.add_actuator(
3548
actuator_name="motor_3",
3649
joint_name="Revolute_3",
3750
ctrl_limited=True,
@@ -40,20 +53,21 @@ def modify_ballbot(ballbot: Robot) -> Robot:
4053
add_force_sensor=True,
4154
)
4255

43-
# For adding to specific named elements (like bodies)
56+
# Add IMU site to specific body
4457
imu_site = ET.Element("site", name="imu", size="0.01", pos="0 0 0")
45-
ballbot.add_custom_element_by_name("imu", "Part_3_1", imu_site)
58+
config.add_custom_element_by_name("imu", "Part_3_1", imu_site)
4659

47-
# Add sensor
48-
ballbot.add_sensor(
60+
# Add sensors
61+
config.add_sensor(
4962
name="imu",
5063
sensor=IMU(name="imu", objtype="site", objname="imu"),
5164
)
52-
ballbot.add_sensor(
65+
config.add_sensor(
5366
name="gyro_1",
5467
sensor=Gyro(name="gyro_1", site="imu"),
5568
)
5669

70+
# Create contact pairs
5771
contact = ET.Element("contact")
5872
pair_1 = ET.SubElement(contact, "pair")
5973
pair_1.set("geom1", "Part_2_3_collision")
@@ -90,20 +104,17 @@ def modify_ballbot(ballbot: Robot) -> Robot:
90104
pair_7.set("geom2", "floor")
91105
pair_7.set("friction", "1 10 3 10 10")
92106

93-
ballbot.add_custom_element_by_tag(name="contact", parent_tag="mujoco", element=contact)
107+
config.add_custom_element_by_tag(name="contact", parent_tag="mujoco", element=contact)
94108

109+
# Add custom mesh and ball element
95110
ballbot_mesh = ET.Element("mesh", attrib={"name": "Part_1_1", "file": "meshes\\ball.stl"})
96-
ballbot.add_custom_element_by_tag(name="ballbot", parent_tag="asset", element=ballbot_mesh)
111+
config.add_custom_element_by_tag(name="ballbot", parent_tag="asset", element=ballbot_mesh)
97112
ball = load_element("ball.xml")
98-
ballbot.add_custom_element_by_tag(name="ball", parent_tag="worldbody", element=ball)
99-
100-
# # set friction="1.0 0.01 0.001" for Part-2-1, Part-2-2, Part-2-3
101-
# ballbot.set_element_attributes(element_name="Part-2-1-collision", attributes={"friction": "0.1 0.05 0.001"})
102-
# ballbot.set_element_attributes(element_name="Part-2-2-collision", attributes={"friction": "0.1 0.05 0.001"})
103-
# ballbot.set_element_attributes(element_name="Part-2-3-collision", attributes={"friction": "0.1 0.05 0.001"})
113+
config.add_custom_element_by_tag(name="ball", parent_tag="worldbody", element=ball)
104114

105-
ballbot.set_element_attributes(element_name="Revolute_1", attributes={"axis": "0 0 1", "damping": "0.05"})
106-
ballbot.set_element_attributes(element_name="Revolute_2", attributes={"axis": "0 0 1", "damping": "0.05"})
107-
ballbot.set_element_attributes(element_name="Revolute_3", attributes={"axis": "0 0 1", "damping": "0.05"})
115+
# Set joint attributes
116+
config.set_element_attributes(element_name="Revolute_1", attributes={"axis": "0 0 1", "damping": "0.05"})
117+
config.set_element_attributes(element_name="Revolute_2", attributes={"axis": "0 0 1", "damping": "0.05"})
118+
config.set_element_attributes(element_name="Revolute_3", attributes={"axis": "0 0 1", "damping": "0.05"})
108119

109-
return ballbot
120+
return config

onshape_robotics_toolkit/formats/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"""
2828

2929
from onshape_robotics_toolkit.formats.base import RobotDeserializer, RobotSerializer
30-
from onshape_robotics_toolkit.formats.mjcf import MJCFConfig, MJCFSerializer
30+
from onshape_robotics_toolkit.formats.mjcf import MJCFConfig, MJCFSerializer, load_element
3131
from onshape_robotics_toolkit.formats.urdf import URDFSerializer
3232

3333
__all__ = [
@@ -36,4 +36,5 @@
3636
"RobotDeserializer",
3737
"RobotSerializer",
3838
"URDFSerializer",
39+
"load_element",
3940
]

onshape_robotics_toolkit/formats/mjcf.py

Lines changed: 146 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,28 @@
1616

1717
from onshape_robotics_toolkit.formats.base import RobotDeserializer, RobotSerializer
1818
from onshape_robotics_toolkit.models.joint import BaseJoint
19-
from onshape_robotics_toolkit.models.mjcf import Actuator, Light, Sensor
19+
from onshape_robotics_toolkit.models.mjcf import Actuator, Encoder, ForceSensor, Light, Sensor
2020
from onshape_robotics_toolkit.utilities.helpers import format_number
2121

22+
23+
def load_element(file_name: str) -> ET._Element:
24+
"""
25+
Load an XML element from a file.
26+
27+
Args:
28+
file_name: The path to the XML file.
29+
30+
Returns:
31+
The root element of the XML file.
32+
33+
Examples:
34+
>>> element = load_element("ball.xml")
35+
>>> config.add_custom_element_by_tag("ball", "worldbody", element)
36+
"""
37+
tree: ET._ElementTree = ET.parse(file_name) # noqa: S320
38+
root: ET._Element = tree.getroot()
39+
return root
40+
2241
if TYPE_CHECKING:
2342
from onshape_robotics_toolkit.parse import PathKey
2443
from onshape_robotics_toolkit.robot import Robot
@@ -180,14 +199,17 @@ def set_element_attributes(
180199
self,
181200
element_name: str,
182201
attributes: dict[str, str],
183-
) -> None:
202+
) -> "MJCFConfig":
184203
"""
185204
Register attribute modifications for an existing XML element.
186205
187206
Args:
188207
element_name: The name of the element to modify
189208
attributes: Dictionary of attribute key-value pairs to set/update
190209
210+
Returns:
211+
Self for method chaining
212+
191213
Examples:
192214
>>> config = MJCFConfig()
193215
>>> config.set_element_attributes(
@@ -196,6 +218,128 @@ def set_element_attributes(
196218
... )
197219
"""
198220
self.mutated_elements[element_name] = attributes
221+
return self
222+
223+
def add_light(
224+
self,
225+
name: str,
226+
directional: bool,
227+
diffuse: tuple[float, float, float],
228+
specular: tuple[float, float, float],
229+
pos: tuple[float, float, float],
230+
direction: tuple[float, float, float],
231+
castshadow: bool,
232+
) -> "MJCFConfig":
233+
"""
234+
Add a light source to the scene.
235+
236+
Args:
237+
name: The name of the light
238+
directional: Whether the light is directional
239+
diffuse: The diffuse color (r, g, b)
240+
specular: The specular color (r, g, b)
241+
pos: The position (x, y, z)
242+
direction: The direction (x, y, z)
243+
castshadow: Whether the light casts shadows
244+
245+
Returns:
246+
Self for method chaining
247+
248+
Examples:
249+
>>> config = MJCFConfig()
250+
>>> config.add_light(
251+
... name="sun",
252+
... directional=True,
253+
... diffuse=(0.8, 0.8, 0.8),
254+
... specular=(0.2, 0.2, 0.2),
255+
... pos=(0, 0, 10),
256+
... direction=(0, 0, -1),
257+
... castshadow=True
258+
... )
259+
"""
260+
self.lights[name] = Light(
261+
directional=directional,
262+
diffuse=diffuse,
263+
specular=specular,
264+
pos=pos,
265+
direction=direction,
266+
castshadow=castshadow,
267+
)
268+
return self
269+
270+
def add_actuator(
271+
self,
272+
actuator_name: str,
273+
joint_name: str,
274+
ctrl_limited: bool = False,
275+
ctrl_range: tuple[float, float] = (0, 0),
276+
gear: float = 1.0,
277+
add_encoder: bool = False,
278+
add_force_sensor: bool = False,
279+
) -> "MJCFConfig":
280+
"""
281+
Add an actuator to the model.
282+
283+
Args:
284+
actuator_name: The name of the actuator
285+
joint_name: The name of the joint to actuate
286+
ctrl_limited: Whether the actuator has control limits
287+
ctrl_range: The control range (min, max)
288+
gear: The gear ratio
289+
add_encoder: Whether to add an encoder sensor
290+
add_force_sensor: Whether to add a force sensor
291+
292+
Returns:
293+
Self for method chaining
294+
295+
Examples:
296+
>>> config = MJCFConfig()
297+
>>> config.add_actuator(
298+
... actuator_name="motor1",
299+
... joint_name="joint1",
300+
... ctrl_limited=True,
301+
... ctrl_range=(-10, 10),
302+
... add_encoder=True,
303+
... add_force_sensor=True
304+
... )
305+
"""
306+
self.actuators[actuator_name] = Actuator(
307+
name=actuator_name,
308+
joint=joint_name,
309+
ctrllimited=ctrl_limited,
310+
ctrlrange=ctrl_range,
311+
gear=gear,
312+
)
313+
314+
if add_encoder:
315+
encoder_name = f"{actuator_name}-enc"
316+
self.sensors[encoder_name] = Encoder(encoder_name, actuator_name)
317+
318+
if add_force_sensor:
319+
force_name = f"{actuator_name}-frc"
320+
self.sensors[force_name] = ForceSensor(force_name, actuator_name)
321+
322+
return self
323+
324+
def add_sensor(self, name: str, sensor: Sensor) -> "MJCFConfig":
325+
"""
326+
Add a sensor to the model.
327+
328+
Args:
329+
name: The name of the sensor
330+
sensor: The sensor object (IMU, Gyro, Encoder, ForceSensor)
331+
332+
Returns:
333+
Self for method chaining
334+
335+
Examples:
336+
>>> from onshape_robotics_toolkit.models.mjcf import IMU, Gyro
337+
>>> config = MJCFConfig()
338+
>>> config.add_sensor("imu", IMU(name="imu", objtype="site", objname="imu"))
339+
>>> config.add_sensor("gyro", Gyro(name="gyro", site="imu"))
340+
"""
341+
self.sensors[name] = sensor
342+
return self
199343

200344

201345
class MJCFSerializer(RobotSerializer):

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ dependencies = [
2525
]
2626

2727
[project.optional-dependencies]
28+
simulation = [
29+
"mujoco>=3.3.7",
30+
"optuna>=4.6.0",
31+
"plotly>=6.5.0",
32+
"usd-core>=25.11",
33+
]
2834
docs = [
2935
"mkdocs>=1.4.2",
3036
"mkdocs-material>=9.2.7",

0 commit comments

Comments
 (0)