Skip to content

Commit ce14e40

Browse files
aclegg3joannetruong
authored andcommitted
[BE] - fix inconsistent base_rot property (#2085)
* add humnaoid data to CI download to activate the test_humanoid.py pytest * Fix the inconsistency between base_rot getter and setter. Add unit tests. Activate and fix skipped humanoid unit tests. * remove the precise test for pi since that is the singularity wrap point and will often produce -pi
1 parent 952a28a commit ce14e40

File tree

5 files changed

+212
-8
lines changed

5 files changed

+212
-8
lines changed

.circleci/config.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ jobs:
211211
. activate habitat
212212
git lfs install
213213
conda install -y gitpython git-lfs
214-
python -m habitat_sim.utils.datasets_download --uids ci_test_assets franka_panda hab_spot_arm hab3_bench_assets ycb rearrange_dataset_v2 --data-path habitat-sim/data/ --no-replace --no-prune
214+
python -m habitat_sim.utils.datasets_download --uids ci_test_assets franka_panda hab_spot_arm hab3_bench_assets habitat_humanoids ycb rearrange_dataset_v2 --data-path habitat-sim/data/ --no-replace --no-prune
215215
- run:
216216
name: Run sim benchmark
217217
command: |

habitat-lab/habitat/articulated_agents/articulated_agent_base.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,28 @@ def base_pos(self, position: mn.Vector3):
176176

177177
@property
178178
def base_rot(self) -> float:
179-
return float(self.sim_obj.rotation.angle())
179+
"""
180+
Returns scalar rotation angle of the agent around the Y axis.
181+
Within range (-pi,pi) consistency with setter is tested. Outside that range, an equivalent but distinct rotation angle may be returned (e.g. 2pi == -2pi == 0).
182+
"""
183+
angle = float(self.sim_obj.rotation.angle())
184+
# NOTE: if the quaternion axis is inverted (-Y) then the angle will be negated
185+
if self.sim_obj.rotation.axis()[1] < 0:
186+
angle = -1 * angle
187+
# NOTE: This offsetting gives us guarantees of consistency in the (-pi, pi) range.
188+
if angle > mn.math.pi:
189+
angle -= mn.math.pi * 2
190+
elif angle <= -mn.math.pi:
191+
angle += mn.math.pi * 2
192+
# NOTE: This final fmod ensures that large angles are mapped back into the -2pi, 2pi range.
193+
angle = mn.math.fmod(angle, 2 * mn.math.pi)
194+
return angle
180195

181196
@base_rot.setter
182197
def base_rot(self, rotation_y_rad: float):
198+
"""
199+
Set the scalar rotation angle of the agent around the Y axis.
200+
"""
183201
if self._base_type == "mobile" or self._base_type == "leg":
184202
self.sim_obj.rotation = mn.Quaternion.rotation(
185203
mn.Rad(rotation_y_rad), mn.Vector3(0, 1, 0)

habitat-lab/habitat/articulated_agents/humanoids/kinematic_humanoid.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,31 @@ def base_pos(self, position: mn.Vector3):
145145

146146
@property
147147
def base_rot(self) -> float:
148-
return float(self.sim_obj.rotation.angle() + mn.Rad(self.offset_rot))
148+
"""
149+
Returns scalar rotation angle of the humanoid around the Y axis.
150+
Within range (-pi,pi) consistency with setter is tested. Outside that range, an equivalent but distinct rotation angle may be returned (e.g. 2pi == -2pi == 0).
151+
NOTE: for humanoid, an additional offset_rot is considered between the model and this wrapper class.
152+
"""
153+
angle = float(self.sim_obj.rotation.angle())
154+
# NOTE: if the quaternion axis is inverted (-Y) then the angle will be negated
155+
if self.sim_obj.rotation.axis()[1] < 0:
156+
angle = -1 * angle
157+
angle += float(mn.Rad(self.offset_rot))
158+
# NOTE: This offsetting gives us guarantees of consistency in the (-pi, pi) range.
159+
if angle > mn.math.pi:
160+
angle -= mn.math.pi * 2
161+
elif angle < -mn.math.pi:
162+
angle += mn.math.pi * 2
163+
# NOTE: This final fmod ensures that large angles are mapped back into the -2pi, 2pi range.
164+
angle = mn.math.fmod(angle, 2 * mn.math.pi)
165+
return angle
149166

150167
@base_rot.setter
151168
def base_rot(self, rotation_y_rad: float):
169+
"""
170+
Set the scalar rotation angle of the humanoid around the Y axis.
171+
NOTE: for humanoid, an additional offset_rot is considered between the model and this wrapper class.
172+
"""
152173
if self._base_type == "mobile" or self._base_type == "leg":
153174
angle_rot = -self.offset_rot
154175
self.sim_obj.rotation = mn.Quaternion.rotation(

test/test_humanoid.py

+92-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# This source code is licensed under the MIT license found in the
55
# LICENSE file in the root directory of this source tree.
66

7+
import math
78
from os import path as osp
89

910
import gym
@@ -202,14 +203,17 @@ def test_humanoid_controller(humanoid_name):
202203
{
203204
"articulated_agent_urdf": humanoid_path,
204205
"motion_data_path": walk_pose_path,
206+
"auto_update_sensor_transform": True,
205207
}
206208
)
207209
if not osp.exists(humanoid_path):
208210
pytest.skip(f"No humanoid file {humanoid_path}")
209211
kin_humanoid = kinematic_humanoid.KinematicHumanoid(agent_config, sim)
210212
kin_humanoid.reconfigure()
211213
kin_humanoid.update()
212-
assert kin_humanoid.get_robot_sim_id() == 1 # 0 is the ground plane
214+
assert (
215+
kin_humanoid.get_robot_sim_id() == 2
216+
) # 0 is the stage and 1 is the ground plane
213217
print(kin_humanoid.get_link_and_joint_names())
214218
observations += simulate(sim, 1.0, produce_debug_video)
215219

@@ -272,6 +276,89 @@ def test_humanoid_controller(humanoid_name):
272276
)
273277

274278

279+
@pytest.mark.skipif(
280+
not osp.exists("data/humanoids/humanoid_data/female_2/female_2.urdf"),
281+
reason="Test requires female 2 humanoid URDF and assets.",
282+
)
283+
@pytest.mark.skipif(
284+
not habitat_sim.built_with_bullet,
285+
reason="Robot wrapper API requires Bullet physics.",
286+
)
287+
def test_base_rot():
288+
# set this to output test results as video for easy investigation
289+
produce_debug_video = False
290+
observations = []
291+
cfg_settings = default_sim_settings.copy()
292+
cfg_settings["scene"] = "NONE"
293+
cfg_settings["enable_physics"] = True
294+
295+
# loading the physical scene
296+
hab_cfg = make_cfg(cfg_settings)
297+
298+
with habitat_sim.Simulator(hab_cfg) as sim:
299+
# setup the camera for debug video (looking at 0,0,0)
300+
sim.agents[0].scene_node.translation = [0.0, -1.0, 2.0]
301+
302+
# add the humanoid to the world via the wrapper
303+
humanoid_path = "data/humanoids/humanoid_data/female_2/female_2.urdf"
304+
walk_pose_path = "data/humanoids/humanoid_data/female_2/female_2_motion_data_smplx.pkl"
305+
306+
agent_config = DictConfig(
307+
{
308+
"articulated_agent_urdf": humanoid_path,
309+
"motion_data_path": walk_pose_path,
310+
"auto_update_sensor_transform": True,
311+
}
312+
)
313+
if not osp.exists(humanoid_path):
314+
pytest.skip(f"No humanoid file {humanoid_path}")
315+
kin_humanoid = kinematic_humanoid.KinematicHumanoid(agent_config, sim)
316+
kin_humanoid.reconfigure()
317+
kin_humanoid.update()
318+
319+
# check that for range (-pi, pi) getter and setter are consistent
320+
num_samples = 100
321+
for i in range(1, num_samples):
322+
angle = -math.pi + (math.pi * 2 * i) / num_samples
323+
kin_humanoid.base_rot = angle
324+
assert np.allclose(angle, kin_humanoid.base_rot, atol=1e-5)
325+
if produce_debug_video:
326+
observations.append(sim.get_sensor_observations())
327+
328+
# check that for range [-2pi, 2pi] increment is accurate
329+
kin_humanoid.base_rot = -math.pi * 2
330+
inc = (math.pi * 4) / num_samples
331+
rot_check = -math.pi * 2
332+
for _ in range(0, num_samples):
333+
kin_humanoid.base_rot = kin_humanoid.base_rot + inc
334+
rot_check += inc
335+
# NOTE: here we check that the angle is accurate (allow an offset of one full rotation to cover redundant options)
336+
accurate_angle = False
337+
for offset in [0, math.pi * 2, math.pi * -2]:
338+
if np.allclose(
339+
rot_check, kin_humanoid.base_rot + offset, atol=1e-5
340+
):
341+
accurate_angle = True
342+
break
343+
assert (
344+
accurate_angle
345+
), f"should be {rot_check}, but was {kin_humanoid.base_rot}."
346+
if produce_debug_video:
347+
observations.append(sim.get_sensor_observations())
348+
349+
# produce some test debug video
350+
if produce_debug_video:
351+
from habitat_sim.utils import viz_utils as vut
352+
353+
vut.make_video(
354+
observations,
355+
"color_sensor",
356+
"color",
357+
"test_base_rot",
358+
open_vid=True,
359+
)
360+
361+
275362
@pytest.mark.skipif(
276363
not osp.exists("data/humanoids/humanoid_data/female_2/female_2.urdf"),
277364
reason="Test requires a human armature.",
@@ -369,14 +456,17 @@ def test_humanoid_seqpose_controller(humanoid_name):
369456
{
370457
"articulated_agent_urdf": humanoid_path,
371458
"motion_data_path": walk_pose_path,
459+
"auto_update_sensor_transform": True,
372460
}
373461
)
374462
if not osp.exists(humanoid_path):
375463
pytest.skip(f"No humanoid file {humanoid_path}")
376464
kin_humanoid = kinematic_humanoid.KinematicHumanoid(agent_config, sim)
377465
kin_humanoid.reconfigure()
378466
kin_humanoid.update()
379-
assert kin_humanoid.get_robot_sim_id() == 1 # 0 is the ground plane
467+
assert (
468+
kin_humanoid.get_robot_sim_id() == 2
469+
) # 0 is the stage and 1 is the ground plane
380470

381471
# set base ground position from navmesh
382472
# NOTE: because the navmesh floats above the collision geometry we should see a pop/settle with dynamics and no fixed base

test/test_robot_wrapper.py

+78-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# This source code is licensed under the MIT license found in the
55
# LICENSE file in the root directory of this source tree.
66

7+
import math
78
from os import path as osp
89

910
import numpy as np
@@ -353,7 +354,8 @@ def test_fetch_robot_wrapper(fixed_base):
353354

354355
# set base ground position using object transformation approach
355356
target_base_pos = sim.pathfinder.snap_point(fetch.sim_obj.translation)
356-
target_base_rots = [0.0, np.pi * 0.25, np.pi * 0.50, np.pi]
357+
# Note, don't test equivalency of pi, because that is the wrap point and is often negated.
358+
target_base_rots = [0.0, np.pi * 0.25, np.pi * 0.50, np.pi * 0.99]
357359
for target_base_rot in target_base_rots:
358360
set_agent_base_via_obj_trans(
359361
target_base_pos, target_base_rot, fetch
@@ -614,7 +616,8 @@ def test_spot_robot_wrapper(fixed_base):
614616

615617
# set base ground position using object transformation approach
616618
target_base_pos = sim.pathfinder.snap_point(spot.sim_obj.translation)
617-
target_base_rots = [0.0, np.pi * 0.25, np.pi * 0.50, np.pi]
619+
# Note, don't test equivalency of pi, because that is the wrap point and is often negated.
620+
target_base_rots = [0.0, np.pi * 0.25, np.pi * 0.50, np.pi * 0.99]
618621
for target_base_rot in target_base_rots:
619622
set_agent_base_via_obj_trans(
620623
target_base_pos, target_base_rot, spot
@@ -705,6 +708,77 @@ def test_spot_robot_wrapper(fixed_base):
705708
)
706709

707710

711+
@pytest.mark.skipif(
712+
not osp.exists("data/robots/hab_spot_arm"),
713+
reason="Test requires Spot w/ arm robot URDF and assets.",
714+
)
715+
@pytest.mark.skipif(
716+
not habitat_sim.built_with_bullet,
717+
reason="Robot wrapper API requires Bullet physics.",
718+
)
719+
def test_base_rot():
720+
# set this to output test results as video for easy investigation
721+
produce_debug_video = False
722+
observations = []
723+
cfg_settings = default_sim_settings.copy()
724+
cfg_settings["scene"] = "NONE"
725+
cfg_settings["enable_physics"] = True
726+
727+
# loading the physical scene
728+
hab_cfg = make_cfg(cfg_settings)
729+
730+
with habitat_sim.Simulator(hab_cfg) as sim:
731+
# setup the camera for debug video (looking at 0,0,0)
732+
sim.agents[0].scene_node.translation = [0.0, -1.0, 2.0]
733+
734+
# add the robot to the world via the wrapper
735+
robot_path = "data/robots/hab_spot_arm/urdf/hab_spot_arm.urdf"
736+
agent_config = DictConfig({"articulated_agent_urdf": robot_path})
737+
spot = spot_robot.SpotRobot(agent_config, sim)
738+
spot.reconfigure()
739+
spot.update()
740+
741+
# check that for range (-pi, pi) getter and setter are consistent
742+
num_samples = 100
743+
for i in range(1, num_samples):
744+
angle = -math.pi + (math.pi * 2 * i) / num_samples
745+
spot.base_rot = angle
746+
assert np.allclose(angle, spot.base_rot, atol=1e-5)
747+
if produce_debug_video:
748+
observations.append(sim.get_sensor_observations())
749+
750+
# check that for range [-2pi, 2pi] increment is accurate
751+
spot.base_rot = -math.pi * 2
752+
inc = (math.pi * 4) / num_samples
753+
rot_check = -math.pi * 2
754+
for _ in range(0, num_samples):
755+
spot.base_rot = spot.base_rot + inc
756+
rot_check += inc
757+
# NOTE: here we check that the angle is accurate (allow an offset of one full rotation to cover redundant options)
758+
accurate_angle = False
759+
for offset in [0, math.pi * 2, math.pi * -2]:
760+
if np.allclose(rot_check, spot.base_rot + offset, atol=1e-5):
761+
accurate_angle = True
762+
break
763+
assert (
764+
accurate_angle
765+
), f"should be {rot_check}, but was {spot.base_rot}."
766+
if produce_debug_video:
767+
observations.append(sim.get_sensor_observations())
768+
769+
# produce some test debug video
770+
if produce_debug_video:
771+
from habitat_sim.utils import viz_utils as vut
772+
773+
vut.make_video(
774+
observations,
775+
"color_sensor",
776+
"color",
777+
"test_base_rot",
778+
open_vid=True,
779+
)
780+
781+
708782
@pytest.mark.skipif(
709783
not osp.exists("data/robots/hab_stretch"),
710784
reason="Test requires Stretch w/ robot URDF and assets.",
@@ -763,7 +837,8 @@ def test_stretch_robot_wrapper(fixed_base):
763837
target_base_pos = sim.pathfinder.snap_point(
764838
stretch.sim_obj.translation
765839
)
766-
target_base_rots = [0.0, np.pi * 0.25, np.pi * 0.50, np.pi]
840+
# Note, don't test equivalency of pi, because that is the wrap point and is often negated.
841+
target_base_rots = [0.0, np.pi * 0.25, np.pi * 0.50, np.pi * 0.99]
767842
for target_base_rot in target_base_rots:
768843
set_agent_base_via_obj_trans(
769844
target_base_pos, target_base_rot, stretch

0 commit comments

Comments
 (0)