Skip to content

Commit a4eaa35

Browse files
authored
Fix xform handling for floating=False in MJCF/URDF importers (#1168)
Signed-off-by: Eric Heiden <eric-heiden@outlook.com>
1 parent 32b20d6 commit a4eaa35

File tree

3 files changed

+182
-3
lines changed

3 files changed

+182
-3
lines changed

newton/_src/utils/import_mjcf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ def parse_body(
726726
elif floating is not None and floating:
727727
joint_indices.append(builder.add_joint_free(link, key="floating_base"))
728728
else:
729-
joint_indices.append(builder.add_joint_fixed(-1, link, parent_xform=_xform, key="fixed_base"))
729+
joint_indices.append(builder.add_joint_fixed(-1, link, parent_xform=world_xform, key="fixed_base"))
730730

731731
else:
732732
joint_pos = joint_pos[0] if len(joint_pos) > 0 else wp.vec3(0.0, 0.0, 0.0)

newton/tests/test_import_mjcf.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,91 @@ def test_joint_stiffness_damping(self):
10371037
self.assertAlmostEqual(joint_target_ke[dof_idx], expected["target_ke"], places=1)
10381038
self.assertAlmostEqual(joint_target_kd[dof_idx], expected["target_kd"], places=1)
10391039

1040+
def test_xform_with_floating_false(self):
1041+
"""Test that xform parameter is respected when floating=False"""
1042+
local_pos = wp.vec3(1.0, 2.0, 3.0)
1043+
local_quat = wp.quat_rpy(0.5, -0.8, 0.7)
1044+
local_xform = wp.transform(local_pos, local_quat)
1045+
1046+
# Create a simple MJCF with a body that has a freejoint
1047+
mjcf_content = f"""<?xml version="1.0" encoding="utf-8"?>
1048+
<mujoco model="test_xform">
1049+
<worldbody>
1050+
<body name="test_body" pos="{local_pos.x} {local_pos.y} {local_pos.z}" quat="{local_quat.w} {local_quat.x} {local_quat.y} {local_quat.z}">
1051+
<freejoint/>
1052+
<geom type="sphere" size="0.1"/>
1053+
</body>
1054+
</worldbody>
1055+
</mujoco>
1056+
"""
1057+
# Create a non-identity transform to apply
1058+
xform_pos = wp.vec3(5.0, 10.0, 15.0)
1059+
xform_quat = wp.quat_from_axis_angle(wp.vec3(0.0, 0.0, 1.0), wp.pi / 4.0) # 45 degree rotation around Z
1060+
xform = wp.transform(xform_pos, xform_quat)
1061+
1062+
# Parse with floating=False and the xform
1063+
builder = newton.ModelBuilder()
1064+
builder.add_mjcf(mjcf_content, floating=False, xform=xform)
1065+
model = builder.finalize()
1066+
1067+
# Verify the model has a fixed joint
1068+
self.assertEqual(model.joint_count, 1)
1069+
joint_type = model.joint_type.numpy()[0]
1070+
self.assertEqual(joint_type, newton.JointType.FIXED)
1071+
1072+
# Verify the fixed joint has the correct parent_xform
1073+
# The joint_X_p should match the world_xform (xform * local_xform)
1074+
joint_X_p = model.joint_X_p.numpy()[0]
1075+
1076+
expected_xform = xform * local_xform
1077+
1078+
# Check position
1079+
np.testing.assert_allclose(
1080+
joint_X_p[:3],
1081+
[expected_xform.p[0], expected_xform.p[1], expected_xform.p[2]],
1082+
rtol=1e-5,
1083+
atol=1e-5,
1084+
err_msg="Fixed joint parent_xform position does not match expected xform",
1085+
)
1086+
1087+
# Check quaternion (note: quaternions can be negated and still represent the same rotation)
1088+
expected_quat = np.array([expected_xform.q[0], expected_xform.q[1], expected_xform.q[2], expected_xform.q[3]])
1089+
actual_quat = joint_X_p[3:7]
1090+
1091+
# Check if quaternions match (accounting for q and -q representing the same rotation)
1092+
quat_match = np.allclose(actual_quat, expected_quat, rtol=1e-5, atol=1e-5) or np.allclose(
1093+
actual_quat, -expected_quat, rtol=1e-5, atol=1e-5
1094+
)
1095+
self.assertTrue(
1096+
quat_match,
1097+
f"Fixed joint parent_xform quaternion does not match expected xform.\n"
1098+
f"Expected: {expected_quat}\nActual: {actual_quat}",
1099+
)
1100+
1101+
# Verify body_q after eval_fk also matches the expected transform
1102+
state = model.state()
1103+
newton.eval_fk(model, model.joint_q, model.joint_qd, state)
1104+
1105+
body_q = state.body_q.numpy()[0]
1106+
np.testing.assert_allclose(
1107+
body_q[:3],
1108+
[expected_xform.p[0], expected_xform.p[1], expected_xform.p[2]],
1109+
rtol=1e-5,
1110+
atol=1e-5,
1111+
err_msg="Body position after eval_fk does not match expected xform",
1112+
)
1113+
1114+
# Check body quaternion
1115+
body_quat = body_q[3:7]
1116+
quat_match = np.allclose(body_quat, expected_quat, rtol=1e-5, atol=1e-5) or np.allclose(
1117+
body_quat, -expected_quat, rtol=1e-5, atol=1e-5
1118+
)
1119+
self.assertTrue(
1120+
quat_match,
1121+
f"Body quaternion after eval_fk does not match expected xform.\n"
1122+
f"Expected: {expected_quat}\nActual: {body_quat}",
1123+
)
1124+
10401125

10411126
if __name__ == "__main__":
10421127
unittest.main(verbosity=2)

newton/tests/test_import_urdf.py

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from unittest.mock import patch
2020

2121
import numpy as np
22+
import warp as wp
2223

2324
import newton
2425
import newton.examples
@@ -204,8 +205,12 @@ def parse_urdf(urdf: str, builder: newton.ModelBuilder, res_dir: dict[str, str]
204205
urdf: URDF file to parse
205206
res_dir: dict[str, str]: (filename, content): extra resources files to include in the directory"""
206207

208+
# Default to up_axis="Y" if not specified in kwargs
209+
if "up_axis" not in kwargs:
210+
kwargs["up_axis"] = "Y"
211+
207212
if not res_dir:
208-
builder.add_urdf(urdf, up_axis="Y", **kwargs)
213+
builder.add_urdf(urdf, **kwargs)
209214
return
210215

211216
urdf_filename = "robot.urdf"
@@ -220,7 +225,7 @@ def parse_urdf(urdf: str, builder: newton.ModelBuilder, res_dir: dict[str, str]
220225

221226
# Parse the URDF file
222227
urdf_path = Path(temp_dir) / urdf_filename
223-
builder.add_urdf(str(urdf_path), up_axis="Y", **kwargs)
228+
builder.add_urdf(str(urdf_path), **kwargs)
224229

225230
def test_sphere_urdf(self):
226231
# load a urdf containing a sphere with r=0.5 and pos=(1.0,2.0,3.0)
@@ -462,6 +467,95 @@ def test_joint_body_ordering_bfs(self):
462467
]
463468
assert builder.joint_key == ["fixed_base", "joint_2", "joint_1", "joint_2b", "joint_2a", "joint_1a", "joint_1b"]
464469

470+
def test_xform_with_floating_false(self):
471+
"""Test that xform parameter is respected when floating=False"""
472+
473+
# Create a simple URDF with a link (no position/orientation in URDF for root link)
474+
urdf_content = """<?xml version="1.0" encoding="utf-8"?>
475+
<robot name="test_xform">
476+
<link name="base_link">
477+
<inertial>
478+
<mass value="1.0"/>
479+
<inertia ixx="0.1" ixy="0.0" ixz="0.0" iyy="0.1" iyz="0.0" izz="0.1"/>
480+
</inertial>
481+
<visual>
482+
<geometry>
483+
<sphere radius="0.1"/>
484+
</geometry>
485+
</visual>
486+
</link>
487+
</robot>
488+
"""
489+
# Create a non-identity transform to apply
490+
xform_pos = wp.vec3(5.0, 10.0, 15.0)
491+
xform_quat = wp.quat_from_axis_angle(wp.vec3(0.0, 0.0, 1.0), wp.pi / 4.0) # 45 degree rotation around Z
492+
xform = wp.transform(xform_pos, xform_quat)
493+
494+
# Parse with floating=False and the xform
495+
# Use up_axis="Z" to match builder default and avoid axis transformation
496+
builder = newton.ModelBuilder()
497+
self.parse_urdf(urdf_content, builder, floating=False, xform=xform, up_axis="Z")
498+
model = builder.finalize()
499+
500+
# Verify the model has a fixed joint
501+
self.assertEqual(model.joint_count, 1)
502+
joint_type = model.joint_type.numpy()[0]
503+
self.assertEqual(joint_type, newton.JointType.FIXED)
504+
505+
# Verify the fixed joint has the correct parent_xform
506+
# In URDF, the xform is applied directly to the root body (no local transform)
507+
joint_X_p = model.joint_X_p.numpy()[0]
508+
509+
# Expected transform is just xform (URDF root links don't have position/orientation)
510+
expected_xform = xform
511+
512+
# Check position
513+
np.testing.assert_allclose(
514+
joint_X_p[:3],
515+
[expected_xform.p[0], expected_xform.p[1], expected_xform.p[2]],
516+
rtol=1e-5,
517+
atol=1e-5,
518+
err_msg="Fixed joint parent_xform position does not match expected xform",
519+
)
520+
521+
# Check quaternion (note: quaternions can be negated and still represent the same rotation)
522+
expected_quat = np.array([expected_xform.q[0], expected_xform.q[1], expected_xform.q[2], expected_xform.q[3]])
523+
actual_quat = joint_X_p[3:7]
524+
525+
# Check if quaternions match (accounting for q and -q representing the same rotation)
526+
quat_match = np.allclose(actual_quat, expected_quat, rtol=1e-5, atol=1e-5) or np.allclose(
527+
actual_quat, -expected_quat, rtol=1e-5, atol=1e-5
528+
)
529+
self.assertTrue(
530+
quat_match,
531+
f"Fixed joint parent_xform quaternion does not match expected xform.\n"
532+
f"Expected: {expected_quat}\nActual: {actual_quat}",
533+
)
534+
535+
# Verify body_q after eval_fk also matches the expected transform
536+
state = model.state()
537+
newton.eval_fk(model, model.joint_q, model.joint_qd, state)
538+
539+
body_q = state.body_q.numpy()[0]
540+
np.testing.assert_allclose(
541+
body_q[:3],
542+
[expected_xform.p[0], expected_xform.p[1], expected_xform.p[2]],
543+
rtol=1e-5,
544+
atol=1e-5,
545+
err_msg="Body position after eval_fk does not match expected xform",
546+
)
547+
548+
# Check body quaternion
549+
body_quat = body_q[3:7]
550+
quat_match = np.allclose(body_quat, expected_quat, rtol=1e-5, atol=1e-5) or np.allclose(
551+
body_quat, -expected_quat, rtol=1e-5, atol=1e-5
552+
)
553+
self.assertTrue(
554+
quat_match,
555+
f"Body quaternion after eval_fk does not match expected xform.\n"
556+
f"Expected: {expected_quat}\nActual: {body_quat}",
557+
)
558+
465559

466560
if __name__ == "__main__":
467561
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)