505 lines
15 KiB
C++
505 lines
15 KiB
C++
|
/*
|
||
|
* Copyright (C) 2015 Open Source Robotics Foundation
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*
|
||
|
*/
|
||
|
#include <string.h>
|
||
|
|
||
|
#include "gazebo/msgs/msgs.hh"
|
||
|
#include "gazebo/physics/physics.hh"
|
||
|
#include "gazebo/physics/ode/ODESurfaceParams.hh"
|
||
|
#include "gazebo/transport/transport.hh"
|
||
|
#include "gazebo/test/ServerFixture.hh"
|
||
|
#include "gazebo/test/helper_physics_generator.hh"
|
||
|
|
||
|
using namespace gazebo;
|
||
|
|
||
|
const double g_friction_tolerance = 1e-3;
|
||
|
|
||
|
class PhysicsTorsionalFrictionTest : public ServerFixture,
|
||
|
public testing::WithParamInterface<const char*>
|
||
|
{
|
||
|
protected: PhysicsTorsionalFrictionTest() : ServerFixture()
|
||
|
{
|
||
|
}
|
||
|
|
||
|
/// \brief Data structure to hold model pointer and friction parameter
|
||
|
/// for each test model in friction demo world.
|
||
|
class SphereData
|
||
|
{
|
||
|
/// \brief Constructor.
|
||
|
/// \param[in] _world World pointer.
|
||
|
/// \param[in] _name Model name.
|
||
|
public: SphereData(physics::WorldPtr _world,
|
||
|
const std::string &_name)
|
||
|
{
|
||
|
// Get the model pointer
|
||
|
this->model = _world->GetModel(_name);
|
||
|
|
||
|
// Get the link pointer
|
||
|
this->link = this->model->GetLink();
|
||
|
|
||
|
// Get surface params
|
||
|
physics::Collision_V collisions = this->link->GetCollisions();
|
||
|
auto iter = collisions.begin();
|
||
|
EXPECT_TRUE(iter != collisions.end());
|
||
|
|
||
|
auto surf = (*iter)->GetSurface();
|
||
|
EXPECT_TRUE(surf != NULL);
|
||
|
|
||
|
auto surfODE =
|
||
|
boost::static_pointer_cast<physics::ODESurfaceParams>(surf);
|
||
|
|
||
|
this->coefficient = surf->FrictionPyramid()->MuTorsion();
|
||
|
this->patch = surf->FrictionPyramid()->PatchRadius();
|
||
|
this->radius = surf->FrictionPyramid()->SurfaceRadius();
|
||
|
this->kp = surfODE->kp;
|
||
|
|
||
|
// Get inertial params
|
||
|
auto inertial = link->GetInertial();
|
||
|
EXPECT_TRUE(inertial != NULL);
|
||
|
|
||
|
this->mass = inertial->GetMass();
|
||
|
this->izz = inertial->GetIZZ();
|
||
|
|
||
|
this->error.InsertStatistic("maxAbs");
|
||
|
}
|
||
|
|
||
|
/// \brief Pointer to the model
|
||
|
public: physics::ModelPtr model;
|
||
|
|
||
|
/// \brief Pointer to the link
|
||
|
public: physics::LinkPtr link;
|
||
|
|
||
|
/// \brief Torsional friction
|
||
|
public: double coefficient;
|
||
|
|
||
|
/// \brief Spring constant equivalents of a contact.
|
||
|
public: double kp;
|
||
|
|
||
|
/// \brief Mass
|
||
|
public: double mass;
|
||
|
|
||
|
/// \brief Inertia Izz component
|
||
|
public: double izz;
|
||
|
|
||
|
/// \brief Patch
|
||
|
public: double patch;
|
||
|
|
||
|
/// \brief Sphere radius
|
||
|
public: double radius;
|
||
|
|
||
|
/// \brief Computing signal stats error
|
||
|
public: ignition::math::SignalStats error;
|
||
|
};
|
||
|
|
||
|
/// \brief Use the torsional_friction_test world to test spheres with
|
||
|
/// different coefficients of torsional friction and different patch radii.
|
||
|
/// \param[in] _physicsEngine Physics engine to use.
|
||
|
public: void CoefficientTest(const std::string &_physicsEngine);
|
||
|
|
||
|
/// \brief Use the torsional_friction_test world to test spheres with
|
||
|
/// different surface radii.
|
||
|
/// \param[in] _physicsEngine Physics engine to use.
|
||
|
public: void RadiusTest(const std::string &_physicsEngine);
|
||
|
|
||
|
/// \brief Use the torsional_friction_test world to test spheres with
|
||
|
/// different contact depths. It checks if the contact depth is correct for
|
||
|
/// those spheres.
|
||
|
/// \param[in] _physicsEngine Physics engine to use.
|
||
|
public: void DepthTest(const std::string &_physicsEngine);
|
||
|
|
||
|
/// \brief Callback for contact subscribers in depth test.
|
||
|
/// \param[in] _msg Contact message
|
||
|
private: void Callback(const ConstContactsPtr &_msg);
|
||
|
|
||
|
/// \brief Message to be filled with the latest contacts message.
|
||
|
private: msgs::Contacts contactsMsg;
|
||
|
|
||
|
/// \brief Mutex to protect reads and writes to contactsMsg.
|
||
|
public: mutable boost::mutex mutex;
|
||
|
};
|
||
|
|
||
|
/////////////////////////////////////////////////
|
||
|
void PhysicsTorsionalFrictionTest::Callback(const ConstContactsPtr &_msg)
|
||
|
{
|
||
|
boost::mutex::scoped_lock lock(this->mutex);
|
||
|
this->contactsMsg = *_msg;
|
||
|
}
|
||
|
|
||
|
/////////////////////////////////////////////////
|
||
|
void PhysicsTorsionalFrictionTest::DepthTest(
|
||
|
const std::string &_physicsEngine)
|
||
|
{
|
||
|
if (_physicsEngine != "ode")
|
||
|
{
|
||
|
gzerr << "Torsional friction only works with ODE (issues #1681 #1682 #1683)"
|
||
|
<< std::endl;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
Load("test/worlds/torsional_friction_test.world", true, _physicsEngine);
|
||
|
physics::WorldPtr world = physics::get_world("default");
|
||
|
ASSERT_TRUE(world != NULL);
|
||
|
|
||
|
// Step so contacts begin
|
||
|
world->Step(1);
|
||
|
|
||
|
// check the gravity vector
|
||
|
physics::PhysicsEnginePtr physics = world->GetPhysicsEngine();
|
||
|
ASSERT_TRUE(physics != NULL);
|
||
|
EXPECT_EQ(physics->GetType(), _physicsEngine);
|
||
|
math::Vector3 g = physics->GetGravity();
|
||
|
EXPECT_DOUBLE_EQ(g.x, 0);
|
||
|
EXPECT_DOUBLE_EQ(g.y, 0);
|
||
|
EXPECT_DOUBLE_EQ(g.z, -9.8);
|
||
|
|
||
|
// Sleep to ensure transport topics are all advertised
|
||
|
common::Time::MSleep(100);
|
||
|
std::list<std::string> topics =
|
||
|
transport::getAdvertisedTopics("gazebo.msgs.Contacts");
|
||
|
topics.sort();
|
||
|
EXPECT_FALSE(topics.empty());
|
||
|
EXPECT_EQ(topics.size(), 1u);
|
||
|
|
||
|
auto topic = topics.front();
|
||
|
|
||
|
gzdbg << "Listening to " << topic << std::endl;
|
||
|
transport::SubscriberPtr sub = this->node->Subscribe(topic,
|
||
|
&PhysicsTorsionalFrictionTest::Callback, this);
|
||
|
|
||
|
// Step for spheres to sink and rest at the final depth
|
||
|
world->Step(1000);
|
||
|
|
||
|
// Wait for contact messages to be received
|
||
|
int maxSleep = 30;
|
||
|
int sleep = 0;
|
||
|
while (this->contactsMsg.contact().size() < 20 && sleep < maxSleep)
|
||
|
{
|
||
|
common::Time::MSleep(100);
|
||
|
sleep++;
|
||
|
}
|
||
|
|
||
|
ASSERT_EQ(this->contactsMsg.contact().size(), 20);
|
||
|
|
||
|
// Copy message to local variable
|
||
|
msgs::Contacts contacts;
|
||
|
{
|
||
|
boost::mutex::scoped_lock lock(this->mutex);
|
||
|
contacts = this->contactsMsg;
|
||
|
}
|
||
|
|
||
|
// Load the spheres
|
||
|
std::vector<PhysicsTorsionalFrictionTest::SphereData>
|
||
|
spheres;
|
||
|
spheres.push_back(
|
||
|
PhysicsTorsionalFrictionTest::SphereData(world, "sphere_mass_1"));
|
||
|
spheres.push_back(
|
||
|
PhysicsTorsionalFrictionTest::SphereData(world, "sphere_mass_2"));
|
||
|
spheres.push_back(
|
||
|
PhysicsTorsionalFrictionTest::SphereData(world, "sphere_mass_3"));
|
||
|
spheres.push_back(
|
||
|
PhysicsTorsionalFrictionTest::SphereData(world, "sphere_mass_4"));
|
||
|
spheres.push_back(
|
||
|
PhysicsTorsionalFrictionTest::SphereData(world, "sphere_mass_5"));
|
||
|
|
||
|
// Verify sphere data structure
|
||
|
for (auto sphere : spheres)
|
||
|
{
|
||
|
ASSERT_TRUE(sphere.model != NULL);
|
||
|
}
|
||
|
|
||
|
// Check relevant contacts from message
|
||
|
std::vector<bool> contactsChecked = {false, false, false, false, false};
|
||
|
ASSERT_EQ(contacts.contact().size(), 20);
|
||
|
for (auto contact : contacts.contact())
|
||
|
{
|
||
|
// Check if the contact has a sphere for this test
|
||
|
int number = 0;
|
||
|
if (contact.has_collision1() &&
|
||
|
contact.collision1().find("sphere_mass") == 0)
|
||
|
{
|
||
|
number = std::stoi(contact.collision1().substr(12, 1));
|
||
|
}
|
||
|
else if (contact.has_collision2() &&
|
||
|
contact.collision2().find("sphere_mass") == 0)
|
||
|
{
|
||
|
number = std::stoi(contact.collision2().substr(12, 1));
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Get corresponding sphere
|
||
|
ASSERT_GE(number, 1);
|
||
|
ASSERT_LE(number, 5);
|
||
|
contactsChecked[number-1] = true;
|
||
|
auto sphere = spheres[number-1];
|
||
|
|
||
|
// Check that contact normal is in the positive Z direction
|
||
|
ignition::math::Vector3d normal = msgs::ConvertIgn(contact.normal(0));
|
||
|
EXPECT_EQ(normal, ignition::math::Vector3d::UnitZ);
|
||
|
|
||
|
// Check that contact depth is:
|
||
|
// normal force = kp * depth
|
||
|
double expectedDepth = sphere.mass * -g.z / sphere.kp;
|
||
|
double relativeError =
|
||
|
std::abs(contact.depth(0) - expectedDepth)/expectedDepth;
|
||
|
gzdbg << "sphere_mass_" << number
|
||
|
<< " expected " << expectedDepth
|
||
|
<< " actual " << contact.depth(0)
|
||
|
<< std::endl;
|
||
|
|
||
|
// Less than 1% error
|
||
|
EXPECT_LT(relativeError, 0.01);
|
||
|
}
|
||
|
|
||
|
// Verify that each sphere was checked
|
||
|
for (bool contactChecked : contactsChecked)
|
||
|
{
|
||
|
EXPECT_TRUE(contactChecked);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/////////////////////////////////////////////////
|
||
|
void PhysicsTorsionalFrictionTest::CoefficientTest(
|
||
|
const std::string &_physicsEngine)
|
||
|
{
|
||
|
if (_physicsEngine != "ode")
|
||
|
{
|
||
|
gzerr << "Torsional friction only works with ODE (issues #1681 #1682 #1683)"
|
||
|
<< std::endl;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
Load("worlds/torsional_friction_test.world", true, _physicsEngine);
|
||
|
physics::WorldPtr world = physics::get_world("default");
|
||
|
ASSERT_TRUE(world != NULL);
|
||
|
|
||
|
// check the gravity vector
|
||
|
physics::PhysicsEnginePtr physics = world->GetPhysicsEngine();
|
||
|
ASSERT_TRUE(physics != NULL);
|
||
|
EXPECT_EQ(physics->GetType(), _physicsEngine);
|
||
|
math::Vector3 g = physics->GetGravity();
|
||
|
EXPECT_DOUBLE_EQ(g.x, 0);
|
||
|
EXPECT_DOUBLE_EQ(g.y, 0);
|
||
|
EXPECT_DOUBLE_EQ(g.z, -9.8);
|
||
|
|
||
|
// Load the spheres
|
||
|
std::vector<PhysicsTorsionalFrictionTest::SphereData>
|
||
|
spheres;
|
||
|
spheres.push_back(
|
||
|
PhysicsTorsionalFrictionTest::SphereData(world, "sphere_coefficient_1"));
|
||
|
spheres.push_back(
|
||
|
PhysicsTorsionalFrictionTest::SphereData(world, "sphere_coefficient_2"));
|
||
|
spheres.push_back(
|
||
|
PhysicsTorsionalFrictionTest::SphereData(world, "sphere_coefficient_3"));
|
||
|
spheres.push_back(
|
||
|
PhysicsTorsionalFrictionTest::SphereData(world, "sphere_coefficient_4"));
|
||
|
spheres.push_back(
|
||
|
PhysicsTorsionalFrictionTest::SphereData(world, "sphere_coefficient_5"));
|
||
|
spheres.push_back(
|
||
|
PhysicsTorsionalFrictionTest::SphereData(world, "sphere_patch_1"));
|
||
|
spheres.push_back(
|
||
|
PhysicsTorsionalFrictionTest::SphereData(world, "sphere_patch_2"));
|
||
|
spheres.push_back(
|
||
|
PhysicsTorsionalFrictionTest::SphereData(world, "sphere_patch_3"));
|
||
|
spheres.push_back(
|
||
|
PhysicsTorsionalFrictionTest::SphereData(world, "sphere_patch_4"));
|
||
|
spheres.push_back(
|
||
|
PhysicsTorsionalFrictionTest::SphereData(world, "sphere_patch_5"));
|
||
|
|
||
|
// Verify sphere data structure
|
||
|
for (auto sphere : spheres)
|
||
|
{
|
||
|
ASSERT_TRUE(sphere.model != NULL);
|
||
|
ASSERT_TRUE(sphere.link != NULL);
|
||
|
}
|
||
|
|
||
|
// Step so spheres settle in the equilibrium depth
|
||
|
world->Step(1000);
|
||
|
|
||
|
int maxSteps = 10;
|
||
|
int step = 0;
|
||
|
while (step < maxSteps)
|
||
|
{
|
||
|
// Apply a torque about the vertical axis
|
||
|
double appliedTorque = 1000;
|
||
|
for (auto sphere : spheres)
|
||
|
{
|
||
|
sphere.link->AddRelativeTorque(math::Vector3(0, 0, appliedTorque));
|
||
|
}
|
||
|
|
||
|
world->Step(1);
|
||
|
step++;
|
||
|
|
||
|
for (auto sphere : spheres)
|
||
|
{
|
||
|
// Get angular acceleration
|
||
|
math::Vector3 acc = sphere.model->GetWorldAngularAccel();
|
||
|
EXPECT_NEAR(acc.x, 0, g_friction_tolerance);
|
||
|
EXPECT_NEAR(acc.y, 0, g_friction_tolerance);
|
||
|
|
||
|
// Calculate torque due to friction
|
||
|
double normalZ = -sphere.mass * g.z;
|
||
|
double frictionCoef = sphere.coefficient * 3*M_PI/16 *
|
||
|
sphere.patch;
|
||
|
double frictionTorque = normalZ * frictionCoef;
|
||
|
double frictionAcc = (appliedTorque - frictionTorque) / sphere.izz;
|
||
|
// Friction is large enough to prevent motion
|
||
|
if (appliedTorque <= frictionTorque)
|
||
|
{
|
||
|
EXPECT_NEAR(acc.z, 0, g_friction_tolerance);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
sphere.error.InsertData(acc.z - frictionAcc);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Check error separately to reduce console spam
|
||
|
for (auto sphere : spheres)
|
||
|
{
|
||
|
gzdbg << "Model " << sphere.model->GetName() << std::endl;
|
||
|
EXPECT_NEAR(sphere.error.Map()["maxAbs"], 0.0, g_friction_tolerance);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/////////////////////////////////////////////////
|
||
|
void PhysicsTorsionalFrictionTest::RadiusTest(
|
||
|
const std::string &_physicsEngine)
|
||
|
{
|
||
|
if (_physicsEngine != "ode")
|
||
|
{
|
||
|
gzerr << "Torsional friction only works with ODE (issues #1681 #1682 #1683)"
|
||
|
<< std::endl;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
Load("worlds/torsional_friction_test.world", true, _physicsEngine);
|
||
|
physics::WorldPtr world = physics::get_world("default");
|
||
|
ASSERT_TRUE(world != NULL);
|
||
|
|
||
|
// check the gravity vector
|
||
|
physics::PhysicsEnginePtr physics = world->GetPhysicsEngine();
|
||
|
ASSERT_TRUE(physics != NULL);
|
||
|
EXPECT_EQ(physics->GetType(), _physicsEngine);
|
||
|
math::Vector3 g = physics->GetGravity();
|
||
|
EXPECT_DOUBLE_EQ(g.x, 0);
|
||
|
EXPECT_DOUBLE_EQ(g.y, 0);
|
||
|
EXPECT_DOUBLE_EQ(g.z, -9.8);
|
||
|
|
||
|
// Load the spheres
|
||
|
std::vector<PhysicsTorsionalFrictionTest::SphereData>
|
||
|
spheres;
|
||
|
spheres.push_back(
|
||
|
PhysicsTorsionalFrictionTest::SphereData(world, "sphere_radius_1"));
|
||
|
spheres.push_back(
|
||
|
PhysicsTorsionalFrictionTest::SphereData(world, "sphere_radius_2"));
|
||
|
spheres.push_back(
|
||
|
PhysicsTorsionalFrictionTest::SphereData(world, "sphere_radius_3"));
|
||
|
spheres.push_back(
|
||
|
PhysicsTorsionalFrictionTest::SphereData(world, "sphere_radius_4"));
|
||
|
spheres.push_back(
|
||
|
PhysicsTorsionalFrictionTest::SphereData(world, "sphere_radius_5"));
|
||
|
|
||
|
// Verify sphere data structure
|
||
|
for (auto sphere : spheres)
|
||
|
{
|
||
|
ASSERT_TRUE(sphere.model != NULL);
|
||
|
ASSERT_TRUE(sphere.link != NULL);
|
||
|
}
|
||
|
|
||
|
// Step so spheres settle in the equilibrium depth
|
||
|
world->Step(1000);
|
||
|
|
||
|
int maxSteps = 10;
|
||
|
int step = 0;
|
||
|
while (step < maxSteps)
|
||
|
{
|
||
|
// Apply a torque about the vertical axis
|
||
|
double appliedTorque = 1000;
|
||
|
for (auto sphere : spheres)
|
||
|
{
|
||
|
sphere.link->AddRelativeTorque(math::Vector3(0, 0, appliedTorque));
|
||
|
}
|
||
|
|
||
|
world->Step(1);
|
||
|
step++;
|
||
|
gzdbg << "world time: " << world->GetSimTime().Double() << std::endl;
|
||
|
|
||
|
for (auto sphere : spheres)
|
||
|
{
|
||
|
// Get angular acceleration
|
||
|
math::Vector3 acc = sphere.model->GetWorldAngularAccel();
|
||
|
EXPECT_NEAR(acc.x, 0, g_friction_tolerance);
|
||
|
EXPECT_NEAR(acc.y, 0, g_friction_tolerance);
|
||
|
|
||
|
// Calculate torque due to friction
|
||
|
double depthAtEquilibrium = sphere.mass * -g.z / sphere.kp;
|
||
|
double patch = sqrt(sphere.radius * depthAtEquilibrium);
|
||
|
double normalZ = -sphere.mass * g.z;
|
||
|
double frictionTorque =
|
||
|
normalZ * sphere.coefficient * 3 * M_PI * patch / 16;
|
||
|
gzdbg << sphere.model->GetName()
|
||
|
<< " frictionTorque " << frictionTorque
|
||
|
<< std::endl;
|
||
|
|
||
|
// Friction is large enough to prevent motion
|
||
|
if (appliedTorque <= frictionTorque)
|
||
|
{
|
||
|
EXPECT_NEAR(acc.z, 0, g_friction_tolerance);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
double expectedAcc = (appliedTorque - frictionTorque) / sphere.izz;
|
||
|
double relativeError = std::abs(acc.z - expectedAcc) / expectedAcc;
|
||
|
|
||
|
// Less than 1% error
|
||
|
EXPECT_LT(relativeError, 0.01);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/////////////////////////////////////////////////
|
||
|
TEST_P(PhysicsTorsionalFrictionTest, CoefficientTest)
|
||
|
{
|
||
|
CoefficientTest(GetParam());
|
||
|
}
|
||
|
|
||
|
/////////////////////////////////////////////////
|
||
|
TEST_P(PhysicsTorsionalFrictionTest, RadiusTest)
|
||
|
{
|
||
|
RadiusTest(GetParam());
|
||
|
}
|
||
|
|
||
|
/////////////////////////////////////////////////
|
||
|
TEST_P(PhysicsTorsionalFrictionTest, DepthTest)
|
||
|
{
|
||
|
DepthTest(GetParam());
|
||
|
}
|
||
|
|
||
|
INSTANTIATE_TEST_CASE_P(PhysicsEngines, PhysicsTorsionalFrictionTest,
|
||
|
PHYSICS_ENGINE_VALUES);
|
||
|
|
||
|
/////////////////////////////////////////////////
|
||
|
int main(int argc, char **argv)
|
||
|
{
|
||
|
::testing::InitGoogleTest(&argc, argv);
|
||
|
return RUN_ALL_TESTS();
|
||
|
}
|