def test_initialization(gazebo: scenario.GazeboSimulator):

    ok = gazebo.initialize()

    rtf = gazebo.real_time_factor()
    step_size = gazebo.step_size()
    iterations = gazebo.steps_per_run()

    if step_size <= 0:
        assert not ok
        assert not gazebo.initialized()
        assert not gazebo.run()

    if rtf <= 0:
        assert not ok
        assert not gazebo.initialized()
        assert not gazebo.run()

    if iterations <= 0:
        assert not ok
        assert not gazebo.initialized()
        assert not gazebo.run()

    if rtf > 0 and iterations > 0 and step_size > 0:
        assert ok
        assert gazebo.initialized()
示例#2
0
def test_model_base_pose(gazebo: scenario.GazeboSimulator):

    assert gazebo.initialize()

    gym_ignition_model_name = "pendulum"
    model = get_model(gazebo, gym_ignition_model_name)
    assert gym_ignition_model_name in gazebo.get_world().model_names()

    assert model.base_frame() == "support"
    # assert model.set_base_frame("support")  # TODO: Not yet supported
    # assert model.set_base_frame("pendulum")  # TODO: Not yet supported

    # Check that the pose is the identical
    gazebo.run(paused=True)
    assert model.base_position() == pytest.approx([0, 0, 0])
    assert model.base_orientation() == pytest.approx([1, 0, 0, 0])

    # Reset the base pose
    new_base_pose = dict(position=[5, 5, 0], orientation=[0, 1, 0, 0])
    assert model.reset_base_pose(new_base_pose["position"],
                                 new_base_pose["orientation"])

    # Before stepping the simulation the pose should be the initial one
    assert model.base_position() == pytest.approx([0, 0, 0])
    assert model.base_orientation() == pytest.approx([1, 0, 0, 0])

    # Step the simulator and check that the pose changes
    gazebo.run(paused=True)
    assert model.base_position() == pytest.approx(new_base_pose["position"])
    assert model.base_orientation() == pytest.approx(
        new_base_pose["orientation"])
示例#3
0
def test_cube_contact(gazebo: scenario.GazeboSimulator,
                      get_model_str: Callable):

    assert gazebo.initialize()
    world = gazebo.get_world().to_gazebo()

    # Insert the Physics system
    assert world.set_physics_engine(scenario.PhysicsEngine_dart)

    # Insert the ground plane
    assert world.insert_model(gym_ignition_models.get_model_file("ground_plane"))
    assert len(world.model_names()) == 1

    # Insert the cube
    cube_urdf = misc.string_to_file(get_model_str())
    assert world.insert_model(cube_urdf,
                              core.Pose([0, 0, 0.15], [1., 0, 0, 0]),
                              "cube")
    assert len(world.model_names()) == 2

    # Get the cube
    cube = world.get_model("cube")

    # Enable contact detection
    assert not cube.contacts_enabled()
    assert cube.enable_contacts(enable=True)
    assert cube.contacts_enabled()

    # The cube was inserted floating in the air with a 5cm gap wrt to ground.
    # There should be no contacts.
    gazebo.run(paused=True)
    assert not cube.get_link("cube").in_contact()
    assert len(cube.contacts()) == 0

    # Make the cube fall for 150ms
    for _ in range(150):
        gazebo.run()

    # There should be only one contact, between ground and the cube
    assert cube.get_link("cube").in_contact()
    assert len(cube.contacts()) == 1

    # Get the contact
    contact_with_ground = cube.contacts()[0]

    assert contact_with_ground.body_a == "cube::cube"
    assert contact_with_ground.body_b == "ground_plane::link"

    # Check contact points
    for point in contact_with_ground.points:

        assert point.normal == pytest.approx([0, 0, 1])

    # Check that the contact force matches the weight of the cube
    z_forces = [point.force[2] for point in contact_with_ground.points]
    assert np.sum(z_forces) == pytest.approx(-5 * world.gravity()[2], abs=0.1)

    # Forces of all contact points are combined by the following method
    assert cube.get_link("cube").contact_wrench() == \
        pytest.approx([0, 0, np.sum(z_forces), 0, 0, 0])
def test_paused_step(gazebo: scenario.GazeboSimulator):

    assert gazebo.initialize()

    world = gazebo.get_world().to_gazebo()
    assert world.insert_model(
        gym_ignition_models.get_model_file("ground_plane"))
    assert "ground_plane" in world.model_names()
    gazebo.run(paused=True)

    world.remove_model("ground_plane")

    assert "ground_plane" in world.model_names()
    gazebo.run(paused=True)
    assert "ground_plane" not in world.model_names()
    assert world.time() == 0.0
示例#5
0
    def visualise_spawn_volume(self,
                               task: SupportedTasks,
                               gazebo: scenario.GazeboSimulator,
                               color: Tuple[float, float, float,
                                            float] = (0, 0, 1, 0.8),
                               color_with_height: Tuple[float, float, float,
                                                        float] = (1, 0, 1,
                                                                  0.7)):

        # Insert translucent boxes visible only in simulation with no physical interactions
        models.Box(world=task.world,
                   name="object_spawn_volume",
                   position=task._object_spawn_centre,
                   orientation=(0, 0, 0, 1),
                   size=task._object_spawn_volume,
                   collision=False,
                   visual=True,
                   gui_only=True,
                   static=True,
                   color=color)
        models.Box(world=task.world,
                   name="object_spawn_volume_with_height",
                   position=task._object_spawn_centre,
                   orientation=(0, 0, 0, 1),
                   size=task._object_spawn_volume,
                   collision=False,
                   visual=True,
                   gui_only=True,
                   static=True,
                   color=color_with_height)
        # Execute a paused run to process model insertion
        if not gazebo.run(paused=True):
            raise RuntimeError("Failed to execute a paused Gazebo run")
示例#6
0
    def add_robot(self, task: SupportedTasks,
                  gazebo: scenario.GazeboSimulator):

        robot = None
        if 'panda' == task._robot_model:
            robot = models.Panda(
                world=task.world,
                position=task._robot_position,
                orientation=conversions.Quaternion.to_wxyz(
                    task._robot_quat_xyzw),
                arm_collision=task._robot_arm_collision,
                hand_collision=task._robot_hand_collision,
                initial_joint_positions=task._robot_initial_joint_positions)
        task.robot_name = robot.name()
        task.robot_base_link_name = robot.get_base_link_name()
        task.robot_ee_link_name = robot.get_ee_link_name()
        task.robot_gripper_link_names = robot.get_gripper_link_names()

        # TODO (low priority): TF2 - Move this to task
        robot_base_frame_id = robot.link_names()[0]
        self._tf2_broadcaster.broadcast_tf(translation=task._robot_position,
                                           rotation=task._robot_quat_xyzw,
                                           xyzw=True,
                                           child_frame_id=robot_base_frame_id)

        # Execute a paused run to process model insertion
        if not gazebo.run(paused=True):
            raise RuntimeError("Failed to execute a paused Gazebo run")
def test_pause(gazebo: scenario.GazeboSimulator):

    assert gazebo.initialize()
    assert not gazebo.running()
    assert gazebo.pause()

    assert gazebo.initialize()

    assert not gazebo.running()
    assert gazebo.pause()

    assert gazebo.run(paused=True)
    assert not gazebo.running()
    assert gazebo.pause()

    assert gazebo.run(paused=False)
    assert not gazebo.running()
    assert gazebo.pause()
示例#8
0
def test_fuel_world(gazebo: scenario.GazeboSimulator):
    # (setup) load a world that includes a fuel model
    worlds_folder = Path(__file__) / ".." / ".." / "assets" / "worlds"
    world_file = worlds_folder / "fuel_support.sdf"
    assert gazebo.insert_world_from_sdf(str(world_file.resolve()))
    assert gazebo.initialize()
    assert gazebo.run(paused=True)

    # the actual test
    assert "ground_plane" in gazebo.get_world().model_names()
示例#9
0
def test_world_api(gazebo: scenario.GazeboSimulator):

    assert gazebo.initialize()
    world = gazebo.get_world()

    gravity = [0, 0, 10.0]
    assert world.set_gravity(gravity)
    assert world.gravity() == pytest.approx(gravity)

    assert len(world.model_names()) == 0

    # Insert a model from an non existent file
    assert not world.insert_model("")

    # Insert first cube with default name and pose
    cube_urdf = utils.get_cube_urdf()
    assert world.insert_model(cube_urdf)
    assert len(world.model_names()) == 1

    default_model_name = scenario.get_model_name_from_sdf(cube_urdf, 0)
    assert default_model_name in world.model_names()
    cube1 = world.get_model(default_model_name)
    # TODO: assert cube1

    assert cube1.name() == default_model_name
    assert cube1.base_position() == pytest.approx([0, 0, 0])
    assert cube1.base_orientation() == pytest.approx([1, 0, 0, 0])

    # Inserting a model with the same name should fail
    assert not world.insert_model(cube_urdf)
    assert len(world.model_names()) == 1

    # Insert second cube with custom name and pose
    custom_model_name = "other_cube"
    assert custom_model_name != default_model_name
    custom_model_pose = core.Pose([1, 1, 0], [0, 0, 0, 1])

    assert world.insert_model(cube_urdf, custom_model_pose, custom_model_name)
    assert custom_model_name in world.model_names()
    assert len(world.model_names()) == 2

    cube2 = world.get_model(custom_model_name)
    assert cube1 != cube2
    # TODO: assert cube2

    assert cube2.name() == custom_model_name
    assert cube2.base_position() == pytest.approx(custom_model_pose.position)
    assert cube2.base_orientation() == pytest.approx(custom_model_pose.orientation)

    # Remove the first model (requires either a paused or unpaused step)
    assert world.remove_model(default_model_name)
    assert len(world.model_names()) == 2
    gazebo.run(paused=True)
    assert len(world.model_names()) == 1

    # Without the physics system, the time should not increase
    gazebo.run()
    gazebo.run()
    gazebo.run()
    assert world.time() == 0.0
示例#10
0
    def add_default_ground(self, task: SupportedTasks,
                           gazebo: scenario.GazeboSimulator):

        ground = models.Ground(world=task.world,
                               position=task._ground_position,
                               orientation=conversions.Quaternion.to_wxyz(
                                   task._ground_quat_xyzw),
                               size=task._ground_size)
        task.ground_name = ground.name()

        # Execute a paused run to process model insertion
        if not gazebo.run(paused=True):
            raise RuntimeError("Failed to execute a paused Gazebo run")
示例#11
0
    def add_invisible_world_bottom_collision_plane(
            self, task: SupportedTasks, gazebo: scenario.GazeboSimulator):

        models.Plane(world=task.world,
                     position=(0.0, 0.0, -1.0),
                     orientation=(1.0, 0.0, 0.0, 0.0),
                     direction=(0.0, 0.0, 1.0),
                     collision=True,
                     friction=10.0,
                     visual=False)

        # Execute a paused run to process model insertion
        if not gazebo.run(paused=True):
            raise RuntimeError("Failed to execute a paused Gazebo run")
def get_cube(gazebo: scenario.GazeboSimulator, world: scenario.World) -> core.Model:

    quaternion = to_wxyz(Rotation.from_euler("x", 45, degrees=True).as_quat())
    initial_pose = core.Pose([0, 0, 0.5], quaternion.tolist())

    cube_urdf = utils.get_cube_urdf()
    assert world.insert_model(cube_urdf, initial_pose)
    assert "cube_robot" in world.model_names()

    cube = world.get_model("cube_robot")

    assert cube.to_gazebo().reset_base_world_linear_velocity([0.1, -0.2, -0.3])
    assert cube.to_gazebo().reset_base_world_angular_velocity([-0.1, 2.0, 0.3])

    assert gazebo.run(paused=True)
    return cube
示例#13
0
def test_model_joints(gazebo: scenario.GazeboSimulator):

    assert gazebo.initialize()

    gym_ignition_model_name = "panda"
    model = get_model(gazebo, gym_ignition_model_name)

    q = model.joint_positions()
    assert pytest.approx(q) == [0.0] * model.dofs()

    dq = model.joint_velocities()
    assert pytest.approx(dq) == [0.0] * model.dofs()

    assert model.reset_joint_positions([0.05] * model.dofs())
    assert model.joint_positions() == pytest.approx([0.0] * model.dofs())
    gazebo.run(paused=True)
    assert model.joint_positions() == pytest.approx([0.05] * model.dofs())
    assert model.joint_velocities() == pytest.approx([0.0] * model.dofs())

    gazebo.run(paused=False)
    assert model.joint_velocities() != pytest.approx([0.0] * model.dofs())

    assert model.reset_joint_velocities([-0.1] * model.dofs())
    assert model.joint_velocities() != pytest.approx([-0.1] * model.dofs())
    gazebo.run(paused=True)
    assert model.joint_velocities() == pytest.approx([-0.1] * model.dofs())

    assert model.reset_joint_positions([0.0] * model.dofs())
    assert model.reset_joint_velocities([0.0] * model.dofs())

    joint_subset = model.joint_names()[0:4]
    assert model.reset_joint_positions([-0.4] * len(joint_subset),
                                       joint_subset)
    assert model.reset_joint_velocities([3.0] * len(joint_subset),
                                        joint_subset)
    gazebo.run(paused=True)
    assert model.joint_positions(joint_subset) == pytest.approx(
        [-0.4] * len(joint_subset))
    assert model.joint_velocities(joint_subset) == pytest.approx(
        [3.0] * len(joint_subset))
    assert model.joint_positions() == pytest.approx(
        [-0.4] * len(joint_subset) + [0.0] *
        (model.dofs() - len(joint_subset)))
    assert model.joint_velocities() == pytest.approx(
        [3.0] * len(joint_subset) + [0.0] * (model.dofs() - len(joint_subset)))
示例#14
0
    def add_default_object(self, task: SupportedTasks,
                           gazebo: scenario.GazeboSimulator):

        object_model = None
        if 'box' == task._object_type:
            object_model = models.Box(
                world=task.world,
                position=task._object_spawn_centre,
                orientation=conversions.Quaternion.to_wxyz(
                    task._object_quat_xyzw),
                size=task._object_dimensions,
                mass=task._object_mass,
                collision=task._object_collision,
                visual=task._object_visual,
                static=task._object_static,
                color=task._object_color)
        elif 'sphere' == task._object_type:
            object_model = models.Sphere(
                world=task.world,
                position=task._object_spawn_centre,
                orientation=conversions.Quaternion.to_wxyz(
                    task._object_quat_xyzw),
                radius=task._object_dimensions[0],
                mass=task._object_mass,
                collision=task._object_collision,
                visual=task._object_visual,
                static=task._object_static,
                color=task._object_color)
        elif 'cylinder' == task._object_type:
            object_model = models.Cylinder(
                world=task.world,
                position=task._object_spawn_centre,
                orientation=conversions.Quaternion.to_wxyz(
                    task._object_quat_xyzw),
                radius=task._object_dimensions[0],
                length=task._object_dimensions[1],
                mass=task._object_mass,
                collision=task._object_collision,
                visual=task._object_visual,
                static=task._object_static,
                color=task._object_color)
        task.object_names.append(object_model.name())

        # Execute a paused run to process model insertion
        if not gazebo.run(paused=True):
            raise RuntimeError("Failed to execute a paused Gazebo run")
示例#15
0
def test_world_physics_plugin(gazebo: scenario.GazeboSimulator):

    assert gazebo.initialize()
    world = gazebo.get_world()

    dt = gazebo.step_size()

    assert world.time() == 0

    # Insert a cube
    cube_urdf = utils.get_cube_urdf()
    cube_name = "my_cube"
    cube_pose = core.Pose([0, 0, 1.0], [1, 0, 0, 0])
    assert world.insert_model(cube_urdf, cube_pose, cube_name)
    assert cube_name in world.model_names()

    cube = world.get_model(cube_name)

    # There's no physics, the cube should not move
    for _ in range(10):
        gazebo.run()
        assert cube.base_position() == cube_pose.position

    assert world.time() == 0

    # Insert the Physics system
    assert world.set_physics_engine(scenario.PhysicsEngine_dart)

    # After the first step, the physics catches up with time
    gazebo.run()
    assert world.time() == pytest.approx(11 * dt)

    # The cube should start falling
    assert cube.base_position()[2] < cube_pose.position[2]

    gazebo.run()
    assert world.time() == pytest.approx(12 * dt)

    # Paused steps do not increase the time
    gazebo.run(paused=True)
    assert world.time() == pytest.approx(12 * dt)
示例#16
0
    def add_camera(self,
                   task: SupportedTasks,
                   gazebo: scenario.GazeboSimulator):

        # First add a sensor plugin to the world to enable
        task.world.to_gazebo().insert_world_plugin("libignition-gazebo-sensors-system.so",
                                                   "ignition::gazebo::systems::Sensors",
                                                   f"""
                                                    <sdf version="1.7">
                                                        <render_engine>{task._camera_render_engine}</render_engine>
                                                    </sdf>
                                                    """)

        # Create camera
        camera = models.Camera(world=task.world,
                               position=task._camera_position,
                               orientation=conversions.Quaternion.to_wxyz(
                                   task._camera_quat_xyzw),
                               camera_type=task._camera_type,
                               width=task._camera_width,
                               height=task._camera_height,
                               update_rate=task._camera_update_rate,
                               horizontal_fov=task._camera_horizontal_fov,
                               vertical_fov=task._camera_vertical_fov,
                               clip_color=task._camera_clip_color,
                               clip_depth=task._camera_clip_depth,
                               noise_mean=self._camera_noise_mean,
                               noise_stddev=self._camera_noise_stddev,
                               ros2_bridge_color=task._camera_ros2_bridge_color,
                               ros2_bridge_depth=task._camera_ros2_bridge_depth,
                               ros2_bridge_points=task._camera_ros2_bridge_points)
        task.camera_name = camera.name()

        # TODO (low priority): TF2 - Move this to task
        camera_base_frame_id = camera.frame_id()
        self._tf2_broadcaster.broadcast_tf(translation=task._camera_position,
                                           rotation=task._camera_quat_xyzw,
                                           xyzw=True,
                                           child_frame_id=camera_base_frame_id)

        # Execute a paused run to process model insertion
        if not gazebo.run(paused=True):
            raise RuntimeError("Failed to execute a paused Gazebo run")
示例#17
0
def get_random_panda(gazebo: scenario.GazeboSimulator,
                     world: scenario.World) -> core.Model:

    panda_urdf = gym_ignition_models.get_model_file("panda")
    assert world.insert_model(panda_urdf)
    assert "panda" in world.model_names()

    panda = world.get_model("panda")

    joint_space = get_joint_positions_space(model=panda)
    joint_space.seed(10)

    q = joint_space.sample()
    dq = joint_space.np_random.uniform(low=-1.0, high=1.0, size=q.shape)

    assert panda.to_gazebo().reset_joint_positions(q.tolist())
    assert panda.to_gazebo().reset_joint_velocities(dq.tolist())

    assert gazebo.run(paused=True)
    return panda
示例#18
0
def test_sim_time_starts_from_zero(gazebo: scenario.GazeboSimulator):

    assert gazebo.initialize()
    world = gazebo.get_world()

    dt = gazebo.step_size()

    assert world.time() == 0
    assert world.set_physics_engine(scenario.PhysicsEngine_dart)
    assert world.time() == 0

    gazebo.run(paused=True)
    assert world.time() == 0

    gazebo.run(paused=False)
    assert world.time() == dt

    gazebo.run(paused=False)
    assert world.time() == 2 * dt

    gazebo.run(paused=False)
    assert world.time() == pytest.approx(3 * dt)
示例#19
0
    def randomize_models(self,
                         task: SupportedTasks,
                         gazebo: scenario.GazeboSimulator):
        """
        Randomize models if needed.
        """

        # Randomize robot joint positions if needed, else reset
        if self.robot_joint_position_randomizer_enabled():
            self.robot_random_joint_positions(task=task)
        else:
            self.reset_robot_joint_positions(task=task)

        # Randomize ground plane if needed
        if task._ground_enable and self.ground_model_expired():
            self.randomize_ground(task=task)

        # Randomize camera if needed
        if task._camera_enable and self.camera_pose_expired():
            self.randomize_camera_pose(task=task)

        # Randomize objects if needed
        # Note: No need to randomize pose of new models because they are already spawned randomly
        self.__object_positions.clear()
        if task._object_enable:
            if self.object_models_expired():
                if self._object_random_use_mesh_models:
                    self.randomize_object_models(task=task)
                else:
                    self.randomize_object_primitives(task=task)
            elif self.object_poses_randomizer_enabled():
                self.object_random_pose(task=task)
            elif not self.object_models_randomizer_enabled():
                self.reset_default_object_pose(task=task)

        # Execute a paused run to process these randomization operations
        if not gazebo.run(paused=True):
            raise RuntimeError("Failed to execute a paused Gazebo run")
示例#20
0
def test_download_model_from_fuel(gazebo: scenario.GazeboSimulator):

    assert gazebo.initialize()

    # Get the default world
    world = gazebo.get_world()

    # Download a model from Fuel (testing a name with spaces)
    model_name = "Electrical Box"
    model_sdf = scenario.get_model_file_from_fuel(
        f"https://fuel.ignitionrobotics.org/openrobotics/models/{model_name}", False
    )
    assert model_sdf

    assert world.insert_model(model_sdf, core.Pose_identity())
    assert model_name in world.model_names()

    # Insert another model changing its name
    other_model_name = "my_box"
    other_model_pose = core.Pose([3.0, 0.0, 0.0], [1.0, 0, 0, 0])
    assert world.insert_model(model_sdf, other_model_pose, other_model_name)
    assert other_model_name in world.model_names()

    assert gazebo.run()
def test_run(gazebo: scenario.GazeboSimulator):

    assert gazebo.initialize()
    assert gazebo.run(paused=True)
    assert gazebo.run()
def test_computed_torque_fixed_base(gazebo: scenario.GazeboSimulator):

    assert gazebo.initialize()
    step_size = gazebo.step_size()

    # Get the default world
    world = gazebo.get_world()

    # Insert the physics
    assert world.set_physics_engine(scenario.PhysicsEngine_dart)

    # Get the panda urdf
    panda_urdf = gym_ignition_models.get_model_file("panda")

    # Insert the panda arm
    model_name = "panda"
    assert world.insert_model(panda_urdf, core.Pose_identity(), model_name)

    # import time
    # gazebo.gui()
    # time.sleep(3)
    # gazebo.run(paused=True)

    # Get the model
    panda = world.get_model(model_name).to_gazebo()

    # Set the controller period
    panda.set_controller_period(step_size)

    # Insert the controller
    assert panda.insert_model_plugin(*controllers.ComputedTorqueFixedBase(
        kp=[10.0] * panda.dofs(),
        ki=[0.0] * panda.dofs(),
        kd=[3.0] * panda.dofs(),
        urdf=panda_urdf,
        joints=panda.joint_names(),
    ).args())

    # Set the references
    assert panda.set_joint_position_targets([0.0] * panda.dofs())
    assert panda.set_joint_velocity_targets([0.0] * panda.dofs())
    assert panda.set_joint_acceleration_targets([0.0] * panda.dofs())

    joints_no_fingers = [
        j for j in panda.joint_names() if j.startswith("panda_joint")
    ]
    nr_of_joints = len(joints_no_fingers)
    assert nr_of_joints > 0

    # Reset the joints state
    q0 = [np.deg2rad(45)] * nr_of_joints
    dq0 = [0.1] * nr_of_joints
    assert panda.reset_joint_positions(q0, joints_no_fingers)
    assert panda.reset_joint_velocities(dq0, joints_no_fingers)

    assert gazebo.run(True)
    assert panda.joint_positions(joints_no_fingers) == pytest.approx(q0)
    assert panda.joint_velocities(joints_no_fingers) == pytest.approx(dq0)

    # Step the simulator for a couple of seconds
    for _ in range(3000):
        gazebo.run()

    # Check that the the references have been reached
    assert panda.joint_positions() == pytest.approx(
        panda.joint_position_targets(), abs=np.deg2rad(1))
    assert panda.joint_velocities() == pytest.approx(
        panda.joint_velocity_targets(), abs=0.05)

    # Apply an external force
    assert (panda.get_link("panda_link4").to_gazebo().apply_world_force(
        [100.0, 0, 0], 0.5))

    for _ in range(4000):
        assert gazebo.run()

    # Check that the the references have been reached
    assert panda.joint_positions() == pytest.approx(
        panda.joint_position_targets(), abs=np.deg2rad(1))
    assert panda.joint_velocities() == pytest.approx(
        panda.joint_velocity_targets(), abs=0.05)
示例#23
0
def test_cube_multiple_contacts(gazebo: scenario.GazeboSimulator,
                                get_model_str: Callable):

    assert gazebo.initialize()
    world = gazebo.get_world().to_gazebo()

    # Insert the Physics system
    assert world.set_physics_engine(scenario.PhysicsEngine_dart)

    # Insert the ground plane
    assert world.insert_model(gym_ignition_models.get_model_file("ground_plane"))
    assert len(world.model_names()) == 1

    # Insert two cubes side to side with a 10cm gap
    cube_urdf = misc.string_to_file(get_model_str())
    assert world.insert_model(cube_urdf,
                              core.Pose([0, -0.15, 0.101], [1., 0, 0, 0]),
                              "cube1")
    assert world.insert_model(cube_urdf,
                              core.Pose([0, 0.15, 0.101], [1., 0, 0, 0]),
                              "cube2")
    assert len(world.model_names()) == 3

    # Get the cubes
    cube1 = world.get_model("cube1")
    cube2 = world.get_model("cube2")

    # Enable contact detection
    assert not cube1.contacts_enabled()
    assert cube1.enable_contacts(enable=True)
    assert cube1.contacts_enabled()

    # Enable contact detection
    assert not cube2.contacts_enabled()
    assert cube2.enable_contacts(enable=True)
    assert cube2.contacts_enabled()

    # The cubes were inserted floating in the air with a 1mm gap wrt to ground.
    # There should be no contacts.
    gazebo.run(paused=True)
    assert not cube1.get_link("cube").in_contact()
    assert not cube2.get_link("cube").in_contact()
    assert len(cube1.contacts()) == 0
    assert len(cube2.contacts()) == 0

    # Make the cubes fall for 50ms
    for _ in range(50):
        gazebo.run()

    assert cube1.get_link("cube").in_contact()
    assert cube2.get_link("cube").in_contact()
    assert len(cube1.contacts()) == 1
    assert len(cube2.contacts()) == 1

    # Now we make another cube fall above the gap. It will touch both cubes.
    assert world.insert_model(cube_urdf,
                              core.Pose([0, 0, 0.301], [1., 0, 0, 0]),
                              "cube3")
    assert len(world.model_names()) == 4

    cube3 = world.get_model("cube3")
    assert not cube3.contacts_enabled()
    assert cube3.enable_contacts(enable=True)
    assert cube3.contacts_enabled()

    gazebo.run(paused=True)
    assert not cube3.get_link("cube").in_contact()
    assert len(cube3.contacts()) == 0

    # Make the cube fall for 50ms
    for _ in range(50):
        gazebo.run()

    # There will be two contacts, respectively with cube1 and cube2.
    # Contacts are related to the link, not the collision elements. In the case of two
    # collisions elements associated to the same link, contacts are merged together.
    assert cube3.get_link("cube").in_contact()
    assert len(cube3.contacts()) == 2

    contact1 = cube3.contacts()[0]
    contact2 = cube3.contacts()[1]

    assert contact1.body_a == "cube3::cube"
    assert contact2.body_a == "cube3::cube"

    assert contact1.body_b == "cube1::cube"
    assert contact2.body_b == "cube2::cube"

    # Calculate the total contact wrench of cube3
    assert cube3.get_link("cube").contact_wrench() == pytest.approx([0, 0, 50, 0, 0, 0],
                                                                    abs=1.1)

    # Calculate the total contact force of the cubes below.
    # They will have 1.5 their weight from below and -0.5 from above.
    assert cube1.get_link("cube").contact_wrench()[2] == pytest.approx(50, abs=1.1)
    assert cube1.get_link("cube").contact_wrench()[2] == pytest.approx(50, abs=1.1)

    # Check the normals and the sign of the forces
    for contact in cube2.contacts():
        if contact.body_b == "cube3::cube":
            for point in contact.points:
                assert point.force[2] < 0
                assert point.normal == pytest.approx([0, 0, -1], abs=0.001)
        if contact.body_b == "ground_plane::link":
            for point in contact.points:
                assert point.force[2] > 0
                assert point.normal == pytest.approx([0, 0, 1], abs=0.001)