ppovb5fc7/gazebo/test/integration/physics_friction.cc

727 lines
23 KiB
C++

/*
* Copyright (C) 2012 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 <cmath>
#include <string>
#include "gazebo/msgs/msgs.hh"
#include "gazebo/physics/physics.hh"
#ifdef HAVE_BULLET
#include "gazebo/physics/bullet/bullet_math_inc.h"
#endif
#include "gazebo/transport/transport.hh"
#include "gazebo/test/ServerFixture.hh"
#include "gazebo/test/helper_physics_generator.hh"
#include "gazebo/gazebo_config.h"
using namespace gazebo;
const double g_friction_tolerance = 1e-3;
class PhysicsFrictionTest : public ServerFixture,
public testing::WithParamInterface<const char*>
{
protected: PhysicsFrictionTest() : ServerFixture()
{
}
/// \brief Data structure to hold model pointer and friction parameter
/// for each test model in friction demo world.
class FrictionDemoBox
{
public: FrictionDemoBox(physics::WorldPtr _world, const std::string &_name)
: modelName(_name), world(_world), friction(0.0)
{
// Get the model pointer
model = world->GetModel(modelName);
// Get the friction coefficient
physics::LinkPtr link = model->GetLink();
physics::Collision_V collisions = link->GetCollisions();
physics::Collision_V::iterator iter = collisions.begin();
if (iter != collisions.end())
{
physics::SurfaceParamsPtr surf = (*iter)->GetSurface();
// Use the Secondary friction value,
// since gravity has a non-zero component in the y direction
this->friction = surf->FrictionPyramid()->MuSecondary();
}
}
public: ~FrictionDemoBox() {}
public: std::string modelName;
public: physics::WorldPtr world;
public: physics::ModelPtr model;
public: double friction;
};
/// \brief Class to hold parameters for spawning joints.
public: class SpawnFrictionBoxOptions
{
/// \brief Constructor.
public: SpawnFrictionBoxOptions() : mass(1.0),
friction1(1.0), friction2(1.0)
{
}
/// \brief Destructor.
public: ~SpawnFrictionBoxOptions()
{
}
/// \brief Size of box to spawn.
public: ignition::math::Vector3d size;
/// \brief Mass of box to spawn (inertia computed automatically).
public: double mass;
/// \brief Model pose.
public: ignition::math::Pose3d modelPose;
/// \brief Link pose.
public: ignition::math::Pose3d linkPose;
/// \brief Inertial pose.
public: ignition::math::Pose3d inertialPose;
/// \brief Collision pose.
public: ignition::math::Pose3d collisionPose;
/// \brief Friction coefficient in primary direction.
public: double friction1;
/// \brief Friction coefficient in secondary direction.
public: double friction2;
/// \brief Primary friction direction.
public: ignition::math::Vector3d direction1;
};
/// \brief Spawn a box with friction coefficients and direction.
/// \param[in] _opt Options for friction box.
public: physics::ModelPtr SpawnBox(const SpawnFrictionBoxOptions &_opt)
{
std::string modelName = this->GetUniqueString("box_model");
msgs::Model model;
model.set_name(modelName);
msgs::Set(model.mutable_pose(), _opt.modelPose);
msgs::AddBoxLink(model, _opt.mass, _opt.size);
auto link = model.mutable_link(0);
msgs::Set(link->mutable_pose(), _opt.linkPose);
{
auto inertial = link->mutable_inertial();
msgs::Set(inertial->mutable_pose(), _opt.inertialPose);
}
auto collision = link->mutable_collision(0);
msgs::Set(collision->mutable_pose(), _opt.collisionPose);
auto friction = collision->mutable_surface()->mutable_friction();
friction->set_mu(_opt.friction1);
friction->set_mu2(_opt.friction2);
msgs::Set(friction->mutable_fdir1(), _opt.direction1);
return ServerFixture::SpawnModel(model);
}
/// \brief Use the friction_demo world.
/// \param[in] _physicsEngine Physics engine to use.
public: void FrictionDemo(const std::string &_physicsEngine,
const std::string &_solverType="quick",
const std::string &_worldSolverType="ODE_DANTZIG");
/// \brief Friction test of maximum dissipation principle.
/// Basically test that friction force vector is aligned with
/// and opposes velocity vector.
/// \param[in] _physicsEngine Physics engine to use.
public: void MaximumDissipation(const std::string &_physicsEngine);
/// \brief Test friction directions for friction pyramid with boxes.
/// \param[in] _physicsEngine Physics engine to use.
public: void BoxDirectionRing(const std::string &_physicsEngine);
/// \brief Use frictionDirection parallel to normal to make sure
/// no NaN's are generated.
/// \param[in] _physicsEngine Physics engine to use.
public: void DirectionNaN(const std::string &_physicsEngine);
/// \brief Test ode slip parameter on models with 1-3 spheres
/// and varying mass.
/// Expect motion to depend on mass, slip, and number of contact points.
/// \param[in] _physicsEngine Physics engine to use.
public: void SphereSlip(const std::string &_physicsEngine);
};
class WorldStepFrictionTest : public PhysicsFrictionTest
{
};
/////////////////////////////////////////////////
// FrictionDemo test:
// Uses the test_friction world, which has a bunch of boxes on the ground
// with a gravity vector to simulate a 45-degree inclined plane. Each
// box has a different coefficient of friction. These friction coefficients
// are chosen to be close to the value that would prevent sliding according
// to the Coulomb model.
void PhysicsFrictionTest::FrictionDemo(const std::string &_physicsEngine,
const std::string &_solverType,
const std::string &_worldSolverType)
{
if (_physicsEngine == "simbody")
{
gzerr << "Aborting test since there's an issue with simbody's friction"
<< " parameters (#989)"
<< std::endl;
return;
}
Load("worlds/friction_demo.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();
// Custom gravity vector for this demo world.
EXPECT_DOUBLE_EQ(g.x, 0);
EXPECT_DOUBLE_EQ(g.y, -1.0);
EXPECT_DOUBLE_EQ(g.z, -1.0);
if (_physicsEngine == "ode")
{
// Set solver type
physics->SetParam("solver_type", _solverType);
if (_solverType == "world")
{
physics->SetParam("ode_quiet", true);
}
// Set world step solver type
physics->SetParam("world_step_solver", _worldSolverType);
}
std::vector<PhysicsFrictionTest::FrictionDemoBox> boxes;
std::vector<PhysicsFrictionTest::FrictionDemoBox>::iterator box;
boxes.push_back(PhysicsFrictionTest::FrictionDemoBox(world, "box_01_model"));
boxes.push_back(PhysicsFrictionTest::FrictionDemoBox(world, "box_02_model"));
boxes.push_back(PhysicsFrictionTest::FrictionDemoBox(world, "box_03_model"));
boxes.push_back(PhysicsFrictionTest::FrictionDemoBox(world, "box_04_model"));
boxes.push_back(PhysicsFrictionTest::FrictionDemoBox(world, "box_05_model"));
boxes.push_back(PhysicsFrictionTest::FrictionDemoBox(world, "box_06_model"));
// Verify box data structure
for (box = boxes.begin(); box != boxes.end(); ++box)
{
ASSERT_TRUE(box->model != NULL);
ASSERT_GT(box->friction, 0.0);
}
common::Time t = world->GetSimTime();
while (t.sec < 10)
{
world->Step(500);
t = world->GetSimTime();
double yTolerance = g_friction_tolerance;
if (_solverType == "world")
{
if (_worldSolverType == "DART_PGS")
yTolerance *= 2;
else if (_worldSolverType == "ODE_DANTZIG")
yTolerance = 0.84;
}
for (box = boxes.begin(); box != boxes.end(); ++box)
{
math::Vector3 vel = box->model->GetWorldLinearVel();
EXPECT_NEAR(vel.x, 0, g_friction_tolerance);
EXPECT_NEAR(vel.z, 0, yTolerance);
// Coulomb friction model
if (box->friction >= 1.0)
{
// Friction is large enough to prevent motion
EXPECT_NEAR(vel.y, 0, yTolerance);
}
else
{
// Friction is small enough to allow motion
// Expect velocity = acceleration * time
double vyTolerance = yTolerance;
#ifdef HAVE_BULLET
if (_physicsEngine == "bullet" && sizeof(btScalar) == 4)
{
vyTolerance *= 22;
}
#endif
EXPECT_NEAR(vel.y, (g.y + box->friction) * t.Double(),
vyTolerance);
}
}
}
for (box = boxes.begin(); box != boxes.end(); ++box)
{
ASSERT_TRUE(box->model != NULL);
}
}
/////////////////////////////////////////////////
// MaximumDissipation test:
// Start with friction_cone world, which has a circle of boxes,
// set box velocities to different angles,
// expect velocity unit vectors to stay constant while in motion.
void PhysicsFrictionTest::MaximumDissipation(const std::string &_physicsEngine)
{
// Load an empty world
Load("worlds/friction_cone.world", true, _physicsEngine);
physics::WorldPtr world = physics::get_world("default");
ASSERT_TRUE(world != NULL);
// Verify physics engine type
physics::PhysicsEnginePtr physics = world->GetPhysicsEngine();
ASSERT_TRUE(physics != NULL);
EXPECT_EQ(physics->GetType(), _physicsEngine);
// Expect friction cone model
{
std::string frictionModel;
EXPECT_NO_THROW(frictionModel = boost::any_cast<std::string>(
physics->GetParam("friction_model")));
EXPECT_EQ("cone_model", frictionModel);
}
// Get pointers to boxes and their polar coordinate angle
std::map<physics::ModelPtr, double> modelAngles;
auto models = world->GetModels();
for (auto model : models)
{
ASSERT_TRUE(model != nullptr);
auto name = model->GetName();
if (0 != name.compare(0, 4, "box_"))
{
continue;
}
auto pos = model->GetWorldPose().Ign().Pos();
double angle = std::atan2(pos.Y(), pos.X());
modelAngles[model] = angle;
// Expect radius of 9 m
pos.Z(0);
double radius = pos.Length();
EXPECT_NEAR(9.0, radius, 1e-5);
// Radial velocity should already be set
auto vel = model->GetWorldLinearVel().Ign();
EXPECT_GE(vel.Length(), radius*0.95);
EXPECT_NEAR(angle, atan2(vel.Y(), vel.X()), 1e-6);
}
EXPECT_EQ(modelAngles.size(), 32u);
world->Step(1500);
gzdbg << "Checking position of boxes" << std::endl;
std::map<physics::ModelPtr, double>::iterator iter;
for (iter = modelAngles.begin(); iter != modelAngles.end(); ++iter)
{
double angle = iter->second;
ignition::math::Vector3d pos = iter->first->GetWorldPose().Ign().Pos();
pos.Z(0);
double radius = pos.Length();
double polarAngle = atan2(pos.Y(), pos.X());
// expect polar angle to remain constant
EXPECT_NEAR(angle, polarAngle, 1e-2)
<< "model " << iter->first->GetScopedName()
<< std::endl;
// make sure the boxes are moving outward
EXPECT_GT(radius, 13)
<< "model " << iter->first->GetScopedName()
<< std::endl;
}
}
/////////////////////////////////////////////////
// BoxDirectionRing:
// Spawn several boxes with different friction direction parameters.
void PhysicsFrictionTest::BoxDirectionRing(const std::string &_physicsEngine)
{
if (_physicsEngine == "bullet")
{
gzerr << "Aborting test since there's an issue with bullet's friction"
<< " parameters (#1045)"
<< std::endl;
return;
}
if (_physicsEngine == "simbody")
{
gzerr << "Aborting test since there's an issue with simbody's friction"
<< " parameters (#989)"
<< std::endl;
return;
}
if (_physicsEngine == "dart")
{
gzerr << "Aborting test since there's an issue with dart's friction"
<< " parameters (#1000)"
<< std::endl;
return;
}
// Load an empty world
Load("worlds/friction_dir_test.world", true, _physicsEngine);
physics::WorldPtr world = physics::get_world("default");
ASSERT_TRUE(world != NULL);
// Verify physics engine type
physics::PhysicsEnginePtr physics = world->GetPhysicsEngine();
ASSERT_TRUE(physics != NULL);
EXPECT_EQ(physics->GetType(), _physicsEngine);
// set the gravity vector
ignition::math::Vector3d g(0.0, 1.0, -9.81);
world->SetGravity(g);
// Pointers and location of concentric semi-circles of boxes
std::map<physics::ModelPtr, double> modelAngles;
auto models = world->GetModels();
for (auto model : models)
{
ASSERT_TRUE(model != nullptr);
auto name = model->GetName();
if (0 != name.compare(0, 4, "box_"))
{
continue;
}
auto pos = model->GetWorldPose().Ign().Pos();
double angle = std::atan2(pos.Y(), pos.X());
modelAngles[model] = angle;
}
EXPECT_EQ(modelAngles.size(), 44u);
// Pointers to spheres model and its links
physics::ModelPtr spheres = world->GetModel("spheres");
ASSERT_TRUE(spheres != nullptr);
auto sphereLinks = spheres->GetLinks();
EXPECT_EQ(sphereLinks.size(), 2u);
for (auto link : sphereLinks)
{
ASSERT_TRUE(link != nullptr);
// spin spheres about vertical axis
link->SetAngularVel(ignition::math::Vector3d::UnitZ);
}
// Step forward
world->Step(1500);
double t = world->GetSimTime().Double();
gzdbg << "Checking velocity after " << t << " seconds" << std::endl;
std::map<physics::ModelPtr, double>::iterator iter;
for (iter = modelAngles.begin(); iter != modelAngles.end(); ++iter)
{
double cosAngle = cos(iter->second);
double sinAngle = sin(iter->second);
double velMag = g.Y() * sinAngle * t;
ignition::math::Vector3d vel = iter->first->GetWorldLinearVel().Ign();
EXPECT_NEAR(velMag*cosAngle, vel.X(), 5*g_friction_tolerance);
EXPECT_NEAR(velMag*sinAngle, vel.Y(), 5*g_friction_tolerance);
}
for (auto link : sphereLinks)
{
ASSERT_TRUE(link != nullptr);
// the friction direction should be in a body-fixed frame
// so spinning the spheres should cause them to start rolling
// check that spheres are spinning about the X axis
auto w = link->GetWorldAngularVel().Ign();
EXPECT_LT(w.X(), -4) << "Checking " << link->GetScopedName() << std::endl;
}
}
/////////////////////////////////////////////////
// DirectionNaN:
// Spawn box with vertical friction direction and make sure there's no NaN's
void PhysicsFrictionTest::DirectionNaN(const std::string &_physicsEngine)
{
if (_physicsEngine == "bullet")
{
gzerr << "Aborting test since there's an issue with bullet's friction"
<< " parameters (#1045)"
<< std::endl;
return;
}
if (_physicsEngine == "simbody")
{
gzerr << "Aborting test since there's an issue with simbody's friction"
<< " parameters (#989)"
<< std::endl;
return;
}
if (_physicsEngine == "dart")
{
gzerr << "Aborting test since there's an issue with dart's friction"
<< " parameters (#1000)"
<< std::endl;
return;
}
// Load an empty world
Load("worlds/empty.world", true, _physicsEngine);
physics::WorldPtr world = physics::get_world("default");
ASSERT_TRUE(world != NULL);
// Verify physics engine type
physics::PhysicsEnginePtr physics = world->GetPhysicsEngine();
ASSERT_TRUE(physics != NULL);
EXPECT_EQ(physics->GetType(), _physicsEngine);
// set the gravity vector
// small positive y component
math::Vector3 g(0.0, 1.5, -1.0);
physics->SetGravity(g);
// Spawn a single box
double dx = 0.5;
double dy = 0.5;
double dz = 0.2;
// Set box size and anisotropic friction
SpawnFrictionBoxOptions opt;
opt.size.Set(dx, dy, dz);
opt.direction1 = ignition::math::Vector3d(0.0, 0.0, 1.0);
opt.modelPose.Pos().Z(dz/2);
physics::ModelPtr model = SpawnBox(opt);
ASSERT_TRUE(model != NULL);
// Step forward
world->Step(1500);
double t = world->GetSimTime().Double();
gzdbg << "Checking velocity after " << t << " seconds" << std::endl;
double velMag = (g.y+g.z) * t;
math::Vector3 vel = model->GetWorldLinearVel();
EXPECT_NEAR(0.0, vel.x, g_friction_tolerance);
EXPECT_NEAR(velMag, vel.y, g_friction_tolerance);
}
/////////////////////////////////////////////////
void PhysicsFrictionTest::SphereSlip(const std::string &_physicsEngine)
{
if (_physicsEngine == "bullet")
{
gzerr << "Aborting test since there's an issue with bullet's friction"
<< " parameters (#1045)"
<< std::endl;
return;
}
if (_physicsEngine == "simbody")
{
gzerr << "Aborting test since there's an issue with simbody's friction"
<< " parameters (#989)"
<< std::endl;
return;
}
if (_physicsEngine == "dart")
{
gzerr << "Aborting test since there's an issue with dart's friction"
<< " parameters (#1000)"
<< std::endl;
return;
}
// Load an empty world
Load("worlds/friction_spheres.world", true, _physicsEngine);
physics::WorldPtr world = physics::get_world("default");
ASSERT_TRUE(world != NULL);
// check the gravity vector
// small positive y component
ignition::math::Vector3d grav = world->Gravity();
EXPECT_NEAR(grav.X(), 0, 1e-6);
EXPECT_NEAR(grav.Y(), 2, 1e-6);
EXPECT_NEAR(grav.Z(), -9.81, 1e-6);
// Get pointers to models and their mass-slip product
std::map<physics::ModelPtr, double> lowballMassSlip;
std::map<physics::ModelPtr, double> twoballMassSlip;
std::map<physics::ModelPtr, double> triballMassSlip;
auto models = world->GetModels();
for (auto model : models)
{
ASSERT_TRUE(model != nullptr);
auto name = model->GetName();
if (0 != name.compare(3, 5, "ball_"))
{
continue;
}
double massSlip = std::stod(name.substr(8, 3));
if (0 == name.compare(0, 3, "low"))
{
lowballMassSlip[model] = massSlip;
}
else if (0 == name.compare(0, 3, "two"))
{
twoballMassSlip[model] = massSlip;
}
else if (0 == name.compare(0, 3, "tri"))
{
triballMassSlip[model] = massSlip;
}
}
EXPECT_EQ(lowballMassSlip.size(), 6u);
EXPECT_EQ(twoballMassSlip.size(), 6u);
EXPECT_EQ(triballMassSlip.size(), 6u);
world->Step(5000);
// With constant lateral gravity and non-zero slip,
// expect a steady-state lateral velocity that is proportional
// to the product of slip and mass divided by the number of contact points.
//
// The contact points act like viscous dampers in parallel.
// The slip parameter is defined as:
// slip = lateral_velocity / (lateral_force / contact_points)
// and the velocity is then:
// lateral_velocity = lateral_force * slip / contact_points
// lateral_velocity = gravity * mass * slip / contact_points
gzdbg << "Checking velocity of lowball models" << std::endl;
for (auto lowball : lowballMassSlip)
{
auto model = lowball.first;
double massSlip = lowball.second;
auto vel = model->GetWorldLinearVel().Ign();
EXPECT_NEAR(vel.X(), 0, g_friction_tolerance);
EXPECT_NEAR(vel.Z(), 0, g_friction_tolerance);
double velExpected = grav.Y() * massSlip / 1.0;
EXPECT_NEAR(vel.Y(), velExpected, 0.015*velExpected)
<< "model " << lowball.first->GetScopedName()
<< std::endl;
}
gzdbg << "Checking velocity of twoball models" << std::endl;
for (auto twoball : twoballMassSlip)
{
auto model = twoball.first;
double massSlip = twoball.second;
auto vel = model->GetWorldLinearVel().Ign();
EXPECT_NEAR(vel.X(), 0, g_friction_tolerance);
EXPECT_NEAR(vel.Z(), 0, g_friction_tolerance);
double velExpected = grav.Y() * massSlip / 2.0;
EXPECT_NEAR(vel.Y(), velExpected, 0.015*velExpected)
<< "model " << twoball.first->GetScopedName()
<< std::endl;
}
gzdbg << "Checking velocity of triball models" << std::endl;
for (auto triball : triballMassSlip)
{
auto model = triball.first;
double massSlip = triball.second;
auto vel = model->GetWorldLinearVel().Ign();
EXPECT_NEAR(vel.X(), 0, g_friction_tolerance);
EXPECT_NEAR(vel.Z(), 0, g_friction_tolerance);
double velExpected = grav.Y() * massSlip / 3.0;
EXPECT_NEAR(vel.Y(), velExpected, 0.015*velExpected)
<< "model " << triball.first->GetScopedName()
<< std::endl;
}
}
/////////////////////////////////////////////////
TEST_P(PhysicsFrictionTest, FrictionDemo)
{
FrictionDemo(GetParam());
}
/////////////////////////////////////////////////
TEST_P(WorldStepFrictionTest, FrictionDemoWorldStep)
{
std::string worldStepSolver = GetParam();
if (worldStepSolver.compare("BULLET_PGS") == 0 ||
worldStepSolver.compare("BULLET_LEMKE") == 0)
{
gzerr << "Solver ["
<< worldStepSolver
<< "] doesn't yet work with this test."
<< std::endl;
return;
}
FrictionDemo("ode", "world", worldStepSolver);
}
/////////////////////////////////////////////////
TEST_P(PhysicsFrictionTest, MaximumDissipation)
{
if (std::string("ode").compare(GetParam()) == 0)
{
MaximumDissipation(GetParam());
}
else
{
gzerr << "Skipping test for physics engine "
<< GetParam()
<< std::endl;
}
}
/////////////////////////////////////////////////
TEST_P(PhysicsFrictionTest, BoxDirectionRing)
{
BoxDirectionRing(GetParam());
}
/////////////////////////////////////////////////
TEST_P(PhysicsFrictionTest, DirectionNaN)
{
DirectionNaN(GetParam());
}
/////////////////////////////////////////////////
TEST_P(PhysicsFrictionTest, SphereSlip)
{
if (std::string("ode").compare(GetParam()) == 0)
{
SphereSlip(GetParam());
}
else
{
gzerr << "Skipping test for physics engine "
<< GetParam()
<< std::endl;
}
}
INSTANTIATE_TEST_CASE_P(PhysicsEngines, PhysicsFrictionTest,
PHYSICS_ENGINE_VALUES);
INSTANTIATE_TEST_CASE_P(WorldStepSolvers, WorldStepFrictionTest,
WORLD_STEP_SOLVERS);
/////////////////////////////////////////////////
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}