Skip to content

Commit 6e190a7

Browse files
shameekgangulyscpetersAddisu Z. Taddese
authored
Support nested models in sdformat -> mjcf conversion (#119)
* Support nested models in sdformat -> mjcf conversion Signed-off-by: Shameek Ganguly <shameek@intrinsic.ai> * Remove leftover comment Signed-off-by: Shameek Ganguly <shameek@intrinsic.ai> * Fix typo Signed-off-by: Shameek Ganguly <shameek@intrinsic.ai> * Handle nested parent link for fixed joint Signed-off-by: Shameek Ganguly <shameek@intrinsic.ai> * Improve local var name Signed-off-by: Shameek Ganguly <shameek@intrinsic.ai> * Simplify scoped name tracking (#121) * Simplify scoped name tracking Signed-off-by: Addisu Z. Taddese <addisu@openrobotics.org> * Fix style Signed-off-by: Addisu Z. Taddese <addisu@openrobotics.org> --------- Signed-off-by: Addisu Z. Taddese <addisu@openrobotics.org> --------- Signed-off-by: Shameek Ganguly <shameek@intrinsic.ai> Signed-off-by: Addisu Z. Taddese <addisu@openrobotics.org> Co-authored-by: Steve Peters <scpeters@openrobotics.org> Co-authored-by: Addisu Z. Taddese <addisu@openrobotics.org>
1 parent 1e4dfa2 commit 6e190a7

File tree

5 files changed

+211
-20
lines changed

5 files changed

+211
-20
lines changed

sdformat_mjcf/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ stated, SDFormat elements that affect the behavior of the simulator (i.e not the
8888

8989
## Currently unimplemented features
9090

91-
- Nested models
9291
- Links with multiple parents and kinematic loops
9392
- Revolute2 and Universal joints
9493
- `<scene>` element in SDFormat

sdformat_mjcf/src/sdformat_mjcf/sdformat_to_mjcf/converters/link.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import sdformat_mjcf.utils.sdf_utils as su
2323

2424

25-
def add_link(body, link, parent_name="world", link_pose=None):
25+
def add_link(body, link, parent_name="world", link_pose=None, link_name=None):
2626
"""
2727
Converts a link from SDFormat to MJCF and add it to the given
2828
body/worldbody.
@@ -32,7 +32,8 @@ def add_link(body, link, parent_name="world", link_pose=None):
3232
:param str parent_name: Name of parent link.
3333
:param gz.math.Pose3d link_pose: Pose of link. This is optional and
3434
if set to None, the pose will be resolved from the link.
35-
:return: The newly created MJCF body.
35+
:param str link_name: The name to be assigned for the link body. This is
36+
optional and if set to None, `link.name()` will be used.
3637
:rtype: mjcf.Element
3738
"""
3839
# Currently the following SDFormat elements inside <link> are ignored
@@ -55,8 +56,9 @@ def add_link(body, link, parent_name="world", link_pose=None):
5556
else:
5657
pose = su.graph_resolver.resolve_pose(sem_pose, parent_name)
5758

59+
link_name = link_name if link_name else link.name()
5860
body = body.add("body",
59-
name=su.find_unique_name(body, "body", link.name()),
61+
name=su.find_unique_name(body, "body", link_name),
6062
pos=su.vec3d_to_list(pose.pos()),
6163
euler=su.quat_to_euler_list(pose.rot()))
6264

sdformat_mjcf/src/sdformat_mjcf/sdformat_to_mjcf/converters/model.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,17 @@ def add_model(mjcf_root, model):
3636
:rtype: mjcf.Element
3737
"""
3838

39+
# The resulting kinematic hierarchy includes all links and joints from
40+
# all child models.
3941
kin_hierarchy = KinematicHierarchy(model)
4042
model_pose = graph_resolver.resolve_pose(model.semantic_pose())
4143

42-
def convert_node(body, node, link_pose=None):
43-
child_body = add_link(body, node.link, node.parent_node.link.name(),
44-
link_pose)
44+
def convert_node(body, node, link_pose):
45+
# A node may be assigned a scoped name if it belongs to a child model
46+
# for disambiguation. In this case, use the scoped name for the link
47+
# body.
48+
child_body = add_link(body, node.link, node.parent_node.scoped_name,
49+
link_pose, node.scoped_name)
4550

4651
add_joint(child_body, node.joint)
4752
# Geoms added to bodies attached to the worldbody without a
@@ -62,7 +67,7 @@ def convert_node(body, node, link_pose=None):
6267
should_add_exclusions = is_fixed_joint and is_body_world
6368

6469
for cn in node.child_nodes:
65-
grand_child_body = convert_node(child_body, cn)
70+
grand_child_body = convert_node(child_body, cn, cn.resolved_pose)
6671
if should_add_exclusions:
6772
body.root.contact.add(
6873
"exclude",
@@ -74,8 +79,8 @@ def convert_node(body, node, link_pose=None):
7479

7580
for cn in kin_hierarchy.world_node.child_nodes:
7681
# Adjust the poses of each of the nodes to account for the model
77-
link_pose = graph_resolver.resolve_pose(cn.link.semantic_pose())
78-
new_link_pose = model_pose * link_pose
82+
new_link_pose = model_pose * cn.resolved_pose
83+
7984
convert_node(mjcf_root.worldbody, cn, new_link_pose)
8085

8186
return mjcf_root

sdformat_mjcf/src/sdformat_mjcf/sdformat_to_mjcf/sdf_kinematics.py

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
14+
#
15+
from gz.math import Pose3d
1516
import sdformat as sdf
1617
import sdformat_mjcf.utils.sdf_utils as su
1718

@@ -48,9 +49,18 @@ def __init__(self, link, parent=None, joint=FreeJoint()):
4849
self.joint = joint
4950
self.child_nodes = []
5051

52+
# Pose computed for all links. The value would be relative to the
53+
# parent of the link. If the link has no joints, it is considered to be
54+
# connected to the world link with a free joint.
55+
self.resolved_pose = Pose3d()
56+
57+
# Scoped name assigned for child model links
58+
self.scoped_name = None
59+
5160
def __repr__(self):
5261
child_repr = " ".join(str(node) for node in self.child_nodes)
53-
return f"{self.link.name()}->({child_repr})"
62+
link_name = self.scoped_name if self.scoped_name else self.link.name()
63+
return f"{link_name}->({child_repr})"
5464

5565
def add_child(self, node, joint):
5666
"""
@@ -89,14 +99,39 @@ def __init__(self, model):
8999
self.world_link = sdf.Link()
90100
self.world_link.set_name("world")
91101
self.world_node = LinkNode(self.world_link)
92-
93-
link_to_node_dict = {self.world_link: self.world_node}
102+
self.world_node.scoped_name = "world"
103+
104+
self.link_to_node_dict = {self.world_link: self.world_node}
105+
106+
# Recursively create child model kin hierarchies and merge them up.
107+
# When merging, all children of the child model kin hierarchy world
108+
# node are initially listed under the current world node. If there is a
109+
# joint with a child link in a child model, it will be re-parented when
110+
# joints are processed below.
111+
for mi in range(model.model_count()):
112+
child_model = model.model_by_index(mi)
113+
child_kh = KinematicHierarchy(child_model)
114+
child_model_pose = su.graph_resolver.resolve_pose(
115+
child_model.semantic_pose())
116+
for cn in child_kh.world_node.child_nodes:
117+
cn.resolved_pose *= child_model_pose
118+
# Removing the child node from child_kh.world_node is
119+
# unnecessary and dangerous since it affects the iterable.
120+
self.world_node.add_child(cn, cn.joint)
121+
cn.scoped_name = child_model.name() + '::' + cn.scoped_name
122+
123+
# Merge child model link to node map
124+
self.link_to_node_dict.update(child_kh.link_to_node_dict)
94125

95126
# Start with every link being a child of world link. Later on, we will
96127
# process joints to build the hierarchy.
97128
for li in range(model.link_count()):
98129
node = LinkNode(model.link_by_index(li), self.world_node)
99-
link_to_node_dict[node.link] = node
130+
node.scoped_name = node.link.name()
131+
node.resolved_pose = su.graph_resolver.resolve_pose(
132+
node.link.semantic_pose()
133+
)
134+
self.link_to_node_dict[node.link] = node
100135
joint = StaticFixedJoint() if model.static() else FreeJoint()
101136
self.world_node.add_child(node, joint)
102137

@@ -109,10 +144,16 @@ def __init__(self, model):
109144
parent = self.world_link
110145

111146
child_link_name = su.graph_resolver.resolve_child_link_name(joint)
112-
# TODO (azeey) We assume that the child link is in the current
113-
# model, i.e., not nested. Remove this assumption when we support
114-
# nesting.
115-
child_node = link_to_node_dict[model.link_by_name(child_link_name)]
147+
child_node = self.link_to_node_dict[
148+
model.link_by_name(child_link_name)
149+
]
150+
151+
if parent_link_name != "world":
152+
parent_node = self.link_to_node_dict[parent]
153+
parent_pose_inv = parent_node.resolved_pose.inverse()
154+
child_node.resolved_pose = (
155+
parent_pose_inv * child_node.resolved_pose
156+
)
116157

117158
self.world_node.remove_child(child_node)
118-
link_to_node_dict[parent].add_child(child_node, joint)
159+
self.link_to_node_dict[parent].add_child(child_node, joint)

sdformat_mjcf/tests/sdformat_to_mjcf/test_add_model.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,150 @@ def test_static_model(self):
194194
self.assertFalse(mj_link1.get_children("joint"))
195195
self.assertFalse(mj_link1.get_children("freejoint"))
196196

197+
def test_nested_model_fixed_joint(self):
198+
test_model_sdf = """
199+
<sdf version="1.11">
200+
<model name="modelA">
201+
<pose>1 0 0 0 0 0</pose>
202+
<link name="linkA">
203+
<pose>1 0 0 0 0 0</pose>
204+
<collision name="c1">
205+
<geometry>
206+
<sphere><radius>1</radius></sphere>
207+
</geometry>
208+
</collision>
209+
</link>
210+
<model name="modelB">
211+
<pose>0 1 0 0 0 0</pose>
212+
<link name="linkA">
213+
<pose>0 1 0 0 0 0</pose>
214+
<collision name="c1">
215+
<geometry>
216+
<sphere><radius>2</radius></sphere>
217+
</geometry>
218+
</collision>
219+
</link>
220+
</model>
221+
<joint name="modelB_joint" type="fixed">
222+
<parent>linkA</parent>
223+
<child>modelB::linkA</child>
224+
</joint>
225+
</model>
226+
</sdf>
227+
"""
228+
root = sdf.Root()
229+
root.load_sdf_string(test_model_sdf)
230+
mj_root = add_root(root)
231+
self.assertIsNotNone(mj_root)
232+
mj_modelA_linkA = mj_root.find("body", "linkA")
233+
self.assertIsNotNone(mj_modelA_linkA)
234+
235+
# Check that modelB::linkA was added as a child of (modelA::)linkA
236+
modelA_linkA_children = mj_modelA_linkA.get_children("body")
237+
self.assertEqual(1, len(modelA_linkA_children))
238+
mj_modelB_linkA = modelA_linkA_children[0]
239+
self.assertEqual("modelB::linkA", mj_modelB_linkA.name)
240+
241+
# Verify that the right pos was set for modelB::linkA
242+
sdf_linkA = root.model().link_by_name("linkA")
243+
sdf_linkA_pose = sdf_linkA.semantic_pose().resolve(
244+
_resolveTo="modelB::linkA")
245+
modelB_linkA_expected_pos = su.vec3d_to_list(
246+
sdf_linkA_pose.inverse().pos())
247+
assert_allclose(modelB_linkA_expected_pos, mj_modelB_linkA.pos)
248+
249+
def test_nested_model_multi_level(self):
250+
test_model_sdf = """
251+
<sdf version="1.11">
252+
<model name='top_nested'>
253+
<model name='sub_nested'>
254+
<model name='simple_model'>
255+
<link name='link'/>
256+
</model>
257+
<model name='extra_model'>
258+
<link name='link'/>
259+
</model>
260+
</model>
261+
</model>
262+
</sdf>
263+
"""
264+
root = sdf.Root()
265+
root.load_sdf_string(test_model_sdf)
266+
mj_root = add_root(root)
267+
self.assertIsNotNone(mj_root)
268+
mj_simple_model_link = mj_root.find("body",
269+
"sub_nested::simple_model::link")
270+
self.assertIsNotNone(mj_simple_model_link)
271+
self.assertIsNotNone(mj_simple_model_link.get_children("freejoint"))
272+
273+
mj_extra_model_link = mj_root.find("body",
274+
"sub_nested::extra_model::link")
275+
self.assertIsNotNone(mj_extra_model_link)
276+
self.assertIsNotNone(mj_extra_model_link.get_children("freejoint"))
277+
278+
def test_nested_model_fixed_joint_with_nested_parent_link(self):
279+
test_model_sdf = """
280+
<sdf version="1.11">
281+
<model name="modelA">
282+
<pose>1 0 0 0 0 0</pose>
283+
<link name="linkA">
284+
<pose>1 0 0 0 0 0</pose>
285+
<collision name="c1">
286+
<geometry>
287+
<sphere><radius>1</radius></sphere>
288+
</geometry>
289+
</collision>
290+
</link>
291+
<model name="modelB">
292+
<pose>0 1 0 0 0 0</pose>
293+
<link name="linkA">
294+
<pose>0 10 0 0 0 0</pose>
295+
<collision name="c2">
296+
<geometry>
297+
<sphere><radius>2</radius></sphere>
298+
</geometry>
299+
</collision>
300+
</link>
301+
</model>
302+
303+
<joint name="modelB_joint" type="fixed">
304+
<parent>modelB::linkA</parent>
305+
<child>linkA</child>
306+
</joint>
307+
</model>
308+
</sdf>
309+
"""
310+
root = sdf.Root()
311+
root.load_sdf_string(test_model_sdf)
312+
mj_root = add_root(root)
313+
self.assertIsNotNone(mj_root)
314+
315+
mj_modelB_linkA = mj_root.find("body", "modelB::linkA")
316+
self.assertIsNotNone(mj_modelB_linkA)
317+
318+
# Check that (modelA::)linkA was added as a child of modelB::linkA
319+
modelB_linkA_children = mj_modelB_linkA.get_children("body")
320+
self.assertEqual(1, len(modelB_linkA_children))
321+
mj_modelA_linkA = modelB_linkA_children[0]
322+
self.assertEqual("linkA", mj_modelA_linkA.name)
323+
324+
sdf_modelA = root.model()
325+
326+
# Verify that the right pos was set for top level node modelB::linkA
327+
sdf_modelB_linkA_pose = sdf_modelA.semantic_pose().resolve(
328+
_resolveTo="modelA::modelB::linkA").inverse()
329+
modelB_linkA_expected_pos = su.vec3d_to_list(
330+
(sdf_modelA.raw_pose() * sdf_modelB_linkA_pose).pos()
331+
)
332+
assert_allclose(modelB_linkA_expected_pos, mj_modelB_linkA.pos)
333+
334+
# Verify that the right pos was set for (modelA::)linkA
335+
sdf_linkA = sdf_modelA.link_by_name("linkA")
336+
sdf_linkA_pose = sdf_linkA.semantic_pose().resolve(
337+
_resolveTo="modelB::linkA")
338+
modelA_linkA_expected_pos = su.vec3d_to_list(sdf_linkA_pose.pos())
339+
assert_allclose(modelA_linkA_expected_pos, mj_modelA_linkA.pos)
340+
197341

198342
if __name__ == "__main__":
199343
unittest.main()

0 commit comments

Comments
 (0)