Skip to content

Commit e08ab4a

Browse files
authored
Merge pull request #34 from ami-iit/enhance_sdf_frames
Enhance processing and URDF export of `//frame` elements
2 parents b7daaf7 + 1d009f9 commit e08ab4a

File tree

9 files changed

+438
-159
lines changed

9 files changed

+438
-159
lines changed

.github/workflows/ci_cd.yml

+5-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,11 @@ jobs:
104104
if: matrix.type == 'apt'
105105
run: |
106106
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
108112
109113
- name: Install conda dependencies
110114
if: matrix.type == 'conda'

src/rod/__init__.py

+58-1
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,16 @@
2323
from .sdf.world import World
2424
from .utils.frame_convention import FrameConvention
2525

26+
# ===============================
27+
# Configure the logging verbosity
28+
# ===============================
29+
2630

2731
def _is_editable():
32+
"""
33+
Check if the rod package is installed in editable mode.
34+
"""
35+
2836
import importlib.util
2937
import pathlib
3038
import site
@@ -43,9 +51,58 @@ def _is_editable():
4351
return rod_package_dir not in site.getsitepackages()
4452

4553

46-
# Initialize the logging verbosity
54+
# Initialize the logging verbosity depending on the installation mode.
4755
logging.configure(
4856
level=logging.LoggingLevel.DEBUG if _is_editable() else logging.LoggingLevel.WARNING
4957
)
5058

5159
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

src/rod/kinematics/tree_transforms.py

+46-25
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,15 @@ class TreeTransforms:
1414
kinematic_tree: KinematicTree = dataclasses.dataclass(init=False)
1515

1616
@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.
2220
model = copy.deepcopy(model)
2321

22+
# Make sure that all elements have a pose attribute with explicit 'relative_to'.
2423
model.resolve_frames(is_top_level=is_top_level, explicit_frames=True)
2524

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.
2926
return TreeTransforms(
3027
kinematic_tree=KinematicTree.build(model=model, is_top_level=is_top_level)
3128
)
@@ -54,30 +51,54 @@ def transform(self, name: str) -> npt.NDArray:
5451

5552
return W_H_E
5653

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():
6755

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
6959
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()
7163
W_H_x = self.transform(name=element._source.pose.relative_to)
7264

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}
7575

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
7783

7884
raise ValueError(name)
7985

8086
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+
]
83104
)

src/rod/sdf/model.py

+2
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,15 @@ def switch_frame_convention(
158158
frame_convention: "rod.FrameConvention",
159159
is_top_level: bool = True,
160160
explicit_frames: bool = True,
161+
attach_frames_to_links: bool = True,
161162
) -> None:
162163
from rod.utils.frame_convention import switch_frame_convention
163164

164165
switch_frame_convention(
165166
model=self,
166167
frame_convention=frame_convention,
167168
is_top_level=is_top_level,
169+
attach_frames_to_links=attach_frames_to_links,
168170
)
169171

170172
self.resolve_frames(is_top_level=is_top_level, explicit_frames=explicit_frames)

0 commit comments

Comments
 (0)