rod 0.2.dev12__tar.gz → 0.2.1.dev33__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.
Files changed (54) hide show
  1. {rod-0.2.dev12 → rod-0.2.1.dev33}/.github/workflows/ci_cd.yml +7 -2
  2. {rod-0.2.dev12 → rod-0.2.1.dev33}/PKG-INFO +2 -1
  3. {rod-0.2.dev12 → rod-0.2.1.dev33}/setup.cfg +1 -0
  4. rod-0.2.1.dev33/src/rod/__init__.py +108 -0
  5. rod-0.2.1.dev33/src/rod/builder/primitives.py +110 -0
  6. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/kinematics/tree_transforms.py +46 -25
  7. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/model.py +2 -0
  8. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/urdf/exporter.py +86 -115
  9. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/utils/frame_convention.py +96 -13
  10. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/utils/gazebo.py +13 -3
  11. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/utils/resolve_frames.py +7 -1
  12. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod.egg-info/PKG-INFO +2 -1
  13. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod.egg-info/SOURCES.txt +2 -0
  14. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod.egg-info/requires.txt +1 -0
  15. rod-0.2.1.dev33/tests/test_meshbuilder.py +61 -0
  16. rod-0.2.1.dev33/tests/test_urdf_exporter.py +125 -0
  17. rod-0.2.dev12/src/rod/__init__.py +0 -51
  18. rod-0.2.dev12/src/rod/builder/primitives.py +0 -56
  19. {rod-0.2.dev12 → rod-0.2.1.dev33}/.github/workflows/style.yml +0 -0
  20. {rod-0.2.dev12 → rod-0.2.1.dev33}/.gitignore +0 -0
  21. {rod-0.2.dev12 → rod-0.2.1.dev33}/LICENSE +0 -0
  22. {rod-0.2.dev12 → rod-0.2.1.dev33}/README.md +0 -0
  23. {rod-0.2.dev12 → rod-0.2.1.dev33}/pyproject.toml +0 -0
  24. {rod-0.2.dev12 → rod-0.2.1.dev33}/setup.py +0 -0
  25. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/builder/__init__.py +0 -0
  26. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/builder/primitive_builder.py +0 -0
  27. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/kinematics/__init__.py +0 -0
  28. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/kinematics/kinematic_tree.py +0 -0
  29. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/logging.py +0 -0
  30. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/pretty_printer.py +0 -0
  31. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/__init__.py +0 -0
  32. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/collision.py +0 -0
  33. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/common.py +0 -0
  34. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/element.py +0 -0
  35. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/geometry.py +0 -0
  36. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/joint.py +0 -0
  37. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/link.py +0 -0
  38. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/material.py +0 -0
  39. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/physics.py +0 -0
  40. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/scene.py +0 -0
  41. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/sdf.py +0 -0
  42. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/visual.py +0 -0
  43. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/sdf/world.py +0 -0
  44. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/tree/__init__.py +0 -0
  45. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/tree/directed_tree.py +0 -0
  46. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/tree/tree_elements.py +0 -0
  47. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/urdf/__init__.py +0 -0
  48. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/utils/__init__.py +0 -0
  49. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod/utils/resolve_uris.py +0 -0
  50. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod.egg-info/dependency_links.txt +0 -0
  51. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod.egg-info/not-zip-safe +0 -0
  52. {rod-0.2.dev12 → rod-0.2.1.dev33}/src/rod.egg-info/top_level.txt +0 -0
  53. {rod-0.2.dev12 → rod-0.2.1.dev33}/tests/test_urdf_parsing.py +0 -0
  54. {rod-0.2.dev12 → rod-0.2.1.dev33}/tests/utils_models.py +0 -0
@@ -104,7 +104,11 @@ jobs:
104
104
  if: matrix.type == 'apt'
105
105
  run: |
106
106
  sudo apt-get update
107
- sudo apt-get install -y --no-install-recommends gazebo
107
+ sudo apt-get install lsb-release wget gnupg
108
+ wget https://packages.osrfoundation.org/gazebo.gpg -O /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg
109
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] http://packages.osrfoundation.org/gazebo/ubuntu-stable $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/gazebo-stable.list > /dev/null
110
+ sudo apt-get update
111
+ sudo apt-get install --no-install-recommends libsdformat13 gz-tools2
108
112
 
109
113
  - name: Install conda dependencies
110
114
  if: matrix.type == 'conda'
@@ -122,7 +126,8 @@ jobs:
122
126
  pptree \
123
127
  idyntree \
124
128
  pytest \
125
- robot_descriptions
129
+ robot_descriptions \
130
+ trimesh
126
131
  # pytest-icdiff \ # creates problems on macOS
127
132
  mamba install -y gz-sim7 idyntree
128
133
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rod
3
- Version: 0.2.dev12
3
+ Version: 0.2.1.dev33
4
4
  Summary: The ultimate Python tool for RObot Descriptions processing.
5
5
  Home-page: https://github.com/ami-iit/rod
6
6
  Author: Diego Ferigo
@@ -38,6 +38,7 @@ Requires-Dist: numpy
38
38
  Requires-Dist: packaging
39
39
  Requires-Dist: resolve-robotics-uri-py
40
40
  Requires-Dist: scipy
41
+ Requires-Dist: trimesh
41
42
  Requires-Dist: xmltodict
42
43
  Provides-Extra: style
43
44
  Requires-Dist: black; extra == "style"
@@ -58,6 +58,7 @@ install_requires =
58
58
  packaging
59
59
  resolve-robotics-uri-py
60
60
  scipy
61
+ trimesh
61
62
  xmltodict
62
63
 
63
64
  [options.extras_require]
@@ -0,0 +1,108 @@
1
+ from . import logging
2
+ from .sdf.collision import Collision
3
+ from .sdf.common import Frame, Pose, Xyz
4
+ from .sdf.geometry import (
5
+ Box,
6
+ Capsule,
7
+ Cylinder,
8
+ Ellipsoid,
9
+ Geometry,
10
+ Heightmap,
11
+ Mesh,
12
+ Plane,
13
+ Sphere,
14
+ )
15
+ from .sdf.joint import Axis, Dynamics, Joint, Limit
16
+ from .sdf.link import Inertia, Inertial, Link
17
+ from .sdf.material import Material
18
+ from .sdf.model import Model
19
+ from .sdf.physics import Physics
20
+ from .sdf.scene import Scene
21
+ from .sdf.sdf import Sdf
22
+ from .sdf.visual import Visual
23
+ from .sdf.world import World
24
+ from .utils.frame_convention import FrameConvention
25
+
26
+ # ===============================
27
+ # Configure the logging verbosity
28
+ # ===============================
29
+
30
+
31
+ def _is_editable():
32
+ """
33
+ Check if the rod package is installed in editable mode.
34
+ """
35
+
36
+ import importlib.util
37
+ import pathlib
38
+ import site
39
+
40
+ # Get the ModuleSpec of rod
41
+ rod_spec = importlib.util.find_spec(name="rod")
42
+
43
+ # This can be None. If it's None, assume non-editable installation.
44
+ if rod_spec.origin is None:
45
+ return False
46
+
47
+ # Get the folder containing the rod package
48
+ rod_package_dir = str(pathlib.Path(rod_spec.origin).parent.parent)
49
+
50
+ # The installation is editable if the package dir is not in any {site|dist}-packages
51
+ return rod_package_dir not in site.getsitepackages()
52
+
53
+
54
+ # Initialize the logging verbosity depending on the installation mode.
55
+ logging.configure(
56
+ level=logging.LoggingLevel.DEBUG if _is_editable() else logging.LoggingLevel.WARNING
57
+ )
58
+
59
+ del _is_editable
60
+
61
+ # =====================================
62
+ # Check for compatible sdformat version
63
+ # =====================================
64
+
65
+
66
+ def check_compatible_sdformat(specification_version: str) -> None:
67
+ """
68
+ Check if the installed sdformat version produces SDF files compatible with ROD.
69
+
70
+ Args:
71
+ specification_version: The minimum required SDF specification version.
72
+
73
+ Note:
74
+ This check runs only if sdformat is installed in the system.
75
+ """
76
+
77
+ import os
78
+
79
+ import packaging.version
80
+ import xmltodict
81
+
82
+ from rod.utils.gazebo import GazeboHelper
83
+
84
+ if os.environ.get("ROD_SKIP_SDFORMAT_CHECK", "0") == "1":
85
+ return
86
+
87
+ if not GazeboHelper.has_gazebo():
88
+ return
89
+ else:
90
+ cmdline = GazeboHelper.get_gazebo_executable()
91
+ logging.info(f"Calling sdformat through '{cmdline} sdf'")
92
+
93
+ output_sdf_version = packaging.version.Version(
94
+ xmltodict.parse(
95
+ xml_input=GazeboHelper.process_model_description_with_sdformat(
96
+ model_description="<sdf version='1.4'/>"
97
+ )
98
+ )["sdf"]["@version"]
99
+ )
100
+
101
+ if output_sdf_version < packaging.version.Version(specification_version):
102
+ msg = "The found sdformat installation only supports the '{}' specification, "
103
+ msg += "while ROD requires at least the '{}' specification."
104
+ raise RuntimeError(msg.format(output_sdf_version, specification_version))
105
+
106
+
107
+ check_compatible_sdformat(specification_version="1.10")
108
+ del check_compatible_sdformat
@@ -0,0 +1,110 @@
1
+ import dataclasses
2
+ import pathlib
3
+ from typing import Union
4
+
5
+ import trimesh
6
+ from numpy.typing import NDArray
7
+
8
+ import rod
9
+ from rod.builder.primitive_builder import PrimitiveBuilder
10
+
11
+
12
+ @dataclasses.dataclass
13
+ class SphereBuilder(PrimitiveBuilder):
14
+ radius: float
15
+
16
+ def _inertia(self) -> rod.Inertia:
17
+ return rod.Inertia(
18
+ ixx=2 / 5 * self.mass * (self.radius) ** 2,
19
+ iyy=2 / 5 * self.mass * (self.radius) ** 2,
20
+ izz=2 / 5 * self.mass * (self.radius) ** 2,
21
+ )
22
+
23
+ def _geometry(self) -> rod.Geometry:
24
+ return rod.Geometry(sphere=rod.Sphere(radius=self.radius))
25
+
26
+
27
+ @dataclasses.dataclass
28
+ class BoxBuilder(PrimitiveBuilder):
29
+ x: float
30
+ y: float
31
+ z: float
32
+
33
+ def _inertia(self) -> rod.Inertia:
34
+ return rod.Inertia(
35
+ ixx=self.mass / 12 * (self.y**2 + self.z**2),
36
+ iyy=self.mass / 12 * (self.x**2 + self.z**2),
37
+ izz=self.mass / 12 * (self.x**2 + self.y**2),
38
+ )
39
+
40
+ def _geometry(self) -> rod.Geometry:
41
+ return rod.Geometry(box=rod.Box(size=[self.x, self.y, self.z]))
42
+
43
+
44
+ @dataclasses.dataclass
45
+ class CylinderBuilder(PrimitiveBuilder):
46
+ radius: float
47
+ length: float
48
+
49
+ def _inertia(self) -> rod.Inertia:
50
+ ixx_iyy = self.mass * (3 * self.radius**2 + self.length**2) / 12
51
+
52
+ return rod.Inertia(
53
+ ixx=ixx_iyy,
54
+ iyy=ixx_iyy,
55
+ izz=0.5 * self.mass * self.radius**2,
56
+ )
57
+
58
+ def _geometry(self) -> rod.Geometry:
59
+ return rod.Geometry(
60
+ cylinder=rod.Cylinder(radius=self.radius, length=self.length)
61
+ )
62
+
63
+
64
+ @dataclasses.dataclass
65
+ class MeshBuilder(PrimitiveBuilder):
66
+ mesh_path: Union[str, pathlib.Path]
67
+ scale: NDArray
68
+
69
+ def __post_init__(self) -> None:
70
+ """
71
+ Post-initialization method for the class.
72
+ Loads the mesh from the specified file path and performs necessary checks.
73
+
74
+ Raises:
75
+ AssertionError: If the scale is not a 3D vector.
76
+ TypeError: If the mesh_path is not a str or pathlib.Path.
77
+ """
78
+
79
+ if isinstance(self.mesh_path, str):
80
+ extension = pathlib.Path(self.mesh_path).suffix
81
+ elif isinstance(self.mesh_path, pathlib.Path):
82
+ extension = self.mesh_path.suffix
83
+ else:
84
+ raise TypeError(
85
+ f"Expected str or pathlib.Path for mesh_path, got {type(self.mesh_path)}"
86
+ )
87
+
88
+ self.mesh: trimesh.base.Trimesh = trimesh.load(
89
+ str(self.mesh_path),
90
+ force="mesh",
91
+ file_type=extension,
92
+ )
93
+
94
+ assert self.scale.shape == (
95
+ 3,
96
+ ), f"Scale must be a 3D vector, got {self.scale.shape}"
97
+
98
+ def _inertia(self) -> rod.Inertia:
99
+ inertia = self.mesh.moment_inertia
100
+ return rod.Inertia(
101
+ ixx=inertia[0, 0],
102
+ ixy=inertia[0, 1],
103
+ ixz=inertia[0, 2],
104
+ iyy=inertia[1, 1],
105
+ iyz=inertia[1, 2],
106
+ izz=inertia[2, 2],
107
+ )
108
+
109
+ def _geometry(self) -> rod.Geometry:
110
+ return rod.Geometry(mesh=rod.Mesh(uri=str(self.mesh_path), scale=self.scale))
@@ -14,18 +14,15 @@ class TreeTransforms:
14
14
  kinematic_tree: KinematicTree = dataclasses.dataclass(init=False)
15
15
 
16
16
  @staticmethod
17
- def build(
18
- model: "rod.Model",
19
- is_top_level: bool = True,
20
- prevent_switching_frame_convention: bool = False,
21
- ) -> "TreeTransforms":
17
+ def build(model: "rod.Model", is_top_level: bool = True) -> "TreeTransforms":
18
+
19
+ # Operate on a deep copy of the model to avoid side effects.
22
20
  model = copy.deepcopy(model)
23
21
 
22
+ # Make sure that all elements have a pose attribute with explicit 'relative_to'.
24
23
  model.resolve_frames(is_top_level=is_top_level, explicit_frames=True)
25
24
 
26
- if not prevent_switching_frame_convention:
27
- model.switch_frame_convention(frame_convention=rod.FrameConvention.Urdf)
28
-
25
+ # Build the kinematic tree and return the TreeTransforms object.
29
26
  return TreeTransforms(
30
27
  kinematic_tree=KinematicTree.build(model=model, is_top_level=is_top_level)
31
28
  )
@@ -54,30 +51,54 @@ class TreeTransforms:
54
51
 
55
52
  return W_H_E
56
53
 
57
- if (
58
- name in self.kinematic_tree.link_names()
59
- or name in self.kinematic_tree.frame_names()
60
- ):
61
- element = (
62
- self.kinematic_tree.links_dict[name]
63
- if name in self.kinematic_tree.link_names()
64
- else self.kinematic_tree.frames_dict[name]
65
- )
66
- assert element.name() == name
54
+ if name in self.kinematic_tree.link_names():
67
55
 
68
- # Get the pose of the frame in which the node's pose is expressed
56
+ element = self.kinematic_tree.links_dict[name]
57
+
58
+ assert element.name() == name
69
59
  assert element._source.pose.relative_to not in {"", None}
70
- x_H_N = element._source.pose.transform()
60
+
61
+ # Get the pose of the frame in which the link's pose is expressed.
62
+ x_H_L = element._source.pose.transform()
71
63
  W_H_x = self.transform(name=element._source.pose.relative_to)
72
64
 
73
- # Compute and cache the world-to-node transform
74
- W_H_N = W_H_x @ x_H_N
65
+ # Compute the world transform of the link.
66
+ W_H_L = W_H_x @ x_H_L
67
+ return W_H_L
68
+
69
+ if name in self.kinematic_tree.frame_names():
70
+
71
+ element = self.kinematic_tree.frames_dict[name]
72
+
73
+ assert element.name() == name
74
+ assert element._source.pose.relative_to not in {"", None}
75
75
 
76
- return W_H_N
76
+ # Get the pose of the frame in which the frame's pose is expressed.
77
+ x_H_F = element._source.pose.transform()
78
+ W_H_x = self.transform(name=element._source.pose.relative_to)
79
+
80
+ # Compute the world transform of the frame.
81
+ W_H_F = W_H_x @ x_H_F
82
+ return W_H_F
77
83
 
78
84
  raise ValueError(name)
79
85
 
80
86
  def relative_transform(self, relative_to: str, name: str) -> npt.NDArray:
81
- return np.linalg.inv(self.transform(name=relative_to)) @ self.transform(
82
- name=name
87
+
88
+ world_H_name = self.transform(name=name)
89
+ world_H_relative_to = self.transform(name=relative_to)
90
+
91
+ return TreeTransforms.inverse(world_H_relative_to) @ world_H_name
92
+
93
+ @staticmethod
94
+ def inverse(transform: npt.NDArray) -> npt.NDArray:
95
+
96
+ R = transform[0:3, 0:3]
97
+ p = np.vstack(transform[0:3, 3])
98
+
99
+ return np.block(
100
+ [
101
+ [R.T, -R.T @ p],
102
+ [0, 0, 0, 1],
103
+ ]
83
104
  )
@@ -158,6 +158,7 @@ class Model(Element):
158
158
  frame_convention: "rod.FrameConvention",
159
159
  is_top_level: bool = True,
160
160
  explicit_frames: bool = True,
161
+ attach_frames_to_links: bool = True,
161
162
  ) -> None:
162
163
  from rod.utils.frame_convention import switch_frame_convention
163
164
 
@@ -165,6 +166,7 @@ class Model(Element):
165
166
  model=self,
166
167
  frame_convention=frame_convention,
167
168
  is_top_level=is_top_level,
169
+ attach_frames_to_links=attach_frames_to_links,
168
170
  )
169
171
 
170
172
  self.resolve_frames(is_top_level=is_top_level, explicit_frames=explicit_frames)