Skip to content
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
024e0d6
Add AddLinkForce function
Vedant87 Mar 7, 2025
94ad125
Add AddLinkForce function using AddWorld force method
Vedant87 Mar 7, 2025
f2cd174
Update .gitignore
Vedant87 Mar 7, 2025
9869266
Fixed lint issues by removing trailing whitespaces and limiting lines…
Vedant87 Mar 7, 2025
9660d0c
Fixed lint issues by removing trailing whitespaces and limiting lines…
Vedant87 Mar 7, 2025
a4d3ee3
Add Unit test
Vedant87 Mar 12, 2025
2aa9794
Add Unit test
Vedant87 Mar 12, 2025
d0d498b
Fix formatting for unit test function
Vedant87 Mar 12, 2025
f7c09b7
Fix errors and typos
Vedant87 Mar 12, 2025
1282177
Fix naming convention, added Python bindings.
Vedant87 Mar 14, 2025
03cbfba
Fix errors and limit lines to 80 characters
Vedant87 Mar 14, 2025
334aea2
Changes after review, add handcalculations to Unit test function.
Vedant87 Mar 18, 2025
ee1a690
Fix errors.
Vedant87 Mar 18, 2025
b107a65
Fix errors.
Vedant87 Mar 18, 2025
85bdea9
Correct Expected_torque_with_offset calculations.
Vedant87 Mar 18, 2025
c5ceda6
Merge branch 'main' into add_link_force_api
Vedant87 Mar 18, 2025
e9ba7cc
Merge branch 'main' into add_link_force_api
Vedant87 Mar 31, 2025
53b8b28
Merge branch 'main' into add_link_force_api
Vedant87 Apr 5, 2025
e7eb0ed
Merge branch 'main' into add_link_force_api
Vedant87 Apr 7, 2025
f77af75
Merge branch 'main' into add_link_force_api
Vedant87 Apr 16, 2025
fcbe12d
Merge branch 'main' into add_link_force_api
Vedant87 Apr 18, 2025
6d5067f
Merge branch 'main' into add_link_force_api
Vedant87 Apr 22, 2025
54e4c5d
Merge branch 'main' into add_link_force_api
Vedant87 May 2, 2025
06f6ee8
Merge branch 'main' into add_link_force_api
scpeters May 6, 2025
3371d9f
Update src/Link.cc
Vedant87 May 9, 2025
431d151
Update src/Link.cc
Vedant87 May 9, 2025
eb0233d
Update src/Link.cc
Vedant87 May 9, 2025
10e1b9d
Update src/Link.cc
Vedant87 May 9, 2025
352cffc
Update src/Link.cc
Vedant87 May 9, 2025
3b4180c
Update test/integration/link.cc
Vedant87 May 9, 2025
6c228b0
Update test/integration/link.cc
Vedant87 May 9, 2025
cb4eea4
Update test/integration/link.cc
Vedant87 May 9, 2025
0a9e696
Update test/integration/link.cc
Vedant87 May 9, 2025
9aa3d10
Update Offset test
Vedant87 May 9, 2025
cb4cf99
Update calculations
Vedant87 May 10, 2025
d5e24a1
FIx errors
Vedant87 May 10, 2025
a38852c
Update comments
Vedant87 May 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions include/gz/sim/Link.hh
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,26 @@ namespace gz
const math::Vector3d &_force,
const math::Vector3d &_position) const;

/// \brief Add a force expressed in link's inertial frame,
/// and applied at the link's inertial frame.
/// \param[in] _ecm Mutable Entity-component manager.
/// \param[in] _force Force to be applied expressed in link's
/// inertial frame.
public: void AddForceInInertialFrame(EntityComponentManager &_ecm,
const math::Vector3d &_force) const;

/// \brief Add a force expressed in link's inertial frame,
/// and applied at an offset from the from the link's inertial frame,
/// the offset is expressed in link's inertial frame.
/// \param[in] _ecm Mutable Entity-component manager.
/// \param[in] _force Force to be applied expressed in link's inertial
/// frame.
/// \param[in] _position The point of application of the force expressed
/// in the link's inertial frame.
public: void AddForceInInertialFrame(EntityComponentManager &_ecm,
const math::Vector3d &_force,
const math::Vector3d &_position) const;

/// \brief Add a wrench expressed in world coordinates and applied to
/// the link at the link's origin. This wrench is applied for one
/// simulation step.
Expand Down
18 changes: 18 additions & 0 deletions python/src/gz/sim/Link.cc
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,24 @@ void defineSimLink(py::object module)
py::arg("position"),
"Add a force expressed in world coordinates and applied at "
"an offset from the center of mass of the link.")
.def("add_force_in_inertial_frame",
py::overload_cast<EntityComponentManager &, const math::Vector3d &>
(&gz::sim::Link::AddForceInInertialFrame, py::const_),
py::arg("ecm"),
py::arg("force"),
"Add a force expressed in link's inertial frame, and applied at"
" the link's inertial frame.")
.def("add_force_in_inertial_frame",
py::overload_cast<EntityComponentManager &,
const math::Vector3d &,
const math::Vector3d &>
(&gz::sim::Link::AddForceInInertialFrame, py::const_),
py::arg("ecm"),
py::arg("force"),
py::arg("position"),
"Add a force expressed in link's inertial frame,"
" and applied at an offset from the from the link's inertial frame,"
"the offset is expressed in link's inertial frame.")
.def("add_world_wrench",
py::overload_cast<EntityComponentManager &,
const math::Vector3d &,
Expand Down
58 changes: 58 additions & 0 deletions src/Link.cc
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,64 @@ void Link::AddWorldForce(EntityComponentManager &_ecm,
this->AddWorldWrench(_ecm, _force, torque);
}

//////////////////////////////////////////////////
void Link::AddForceInInertialFrame(EntityComponentManager &_ecm,
const math::Vector3d &_force) const
{
auto inertial = _ecm.Component<components::Inertial>(this->dataPtr->id);
auto worldPose = _ecm.ComponentData<components::WorldPose>(this->dataPtr->id)
.value_or(sim::worldPose(this->dataPtr->id, _ecm));

// Can't apply force if the inertial's pose is not found
if (!inertial)
return;

// The force is expressed in terms of the link's inertial coordinate frame,
// We'll first convert this to force expressed in terms of the link's
// coordinate frame
math::Vector3d linkForce = inertial->Data().Pose().Rot(
).RotateVector(_force);

// AddWorldForce applies the force expressed in world coordinates
// so we need to compute the force expressed in world coordinates
math::Vector3d worldForce = worldPose.Rot().RotateVector(linkForce);

// Apply Force using AddWorldForce method
this->AddWorldForce(_ecm, worldForce);
}

//////////////////////////////////////////////////
void Link::AddForceInInertialFrame(EntityComponentManager &_ecm,
const math::Vector3d &_force,
const math::Vector3d &_position) const
{
auto inertial = _ecm.Component<components::Inertial>(this->dataPtr->id);
auto worldPose = _ecm.ComponentData<components::WorldPose>(this->dataPtr->id)
.value_or(sim::worldPose(this->dataPtr->id, _ecm));

// Can't apply force if the inertial's pose is not found
if (!inertial)
return;

// The force is expressed in terms of the link's inertial coordinate frame,
// We'll first convert this to force expressed in terms of the link's
// coordinate frame
math::Vector3d linkForce =
inertial->Data().Pose().Rot().RotateVector(_force);

// ApplyWorldForce applies the force expressed in world coordinates
// so we need to compute the force expressed in world coordinates
math::Vector3d worldForce = worldPose.Rot().RotateVector(linkForce);

// ApplyWorldForce applies the force at a position expressed in terms
// Rotate offset from inertial frame to link frame.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// ApplyWorldForce applies the force at a position expressed in terms
// Rotate offset from inertial frame to link frame.
// ApplyWorldForce applies the force at a position relative to the
// center of mass and expressed in coordinates of the link frame.
// Since _position is relative to the center of mass in coordinates
// of the inertial frame, it just needs to be rotated to be expressed
// in coordiantes of the link frame.

math::Vector3d positionInLinkFrame =
inertial->Data().Pose().Rot().RotateVector(_position);

// Apply Force using AddWorldForce method
this->AddWorldForce(_ecm, worldForce, positionInLinkFrame);
}

//////////////////////////////////////////////////
void Link::AddWorldWrench(EntityComponentManager &_ecm,
const math::Vector3d &_force,
Expand Down
224 changes: 224 additions & 0 deletions test/integration/link.cc
Original file line number Diff line number Diff line change
Expand Up @@ -698,3 +698,227 @@ TEST_F(LinkIntegrationTest, LinkAddWorldForce)
EXPECT_EQ(math::Vector3d::Zero, math::Vector3d(
wrenchMsg.torque().x(), wrenchMsg.torque().y(), wrenchMsg.torque().z()));
}

TEST_F(LinkIntegrationTest, LinkAddForceInInertialFrame)
{
EntityComponentManager ecm;
EventManager eventMgr;
SdfEntityCreator creator(ecm, eventMgr);

auto eLink = ecm.CreateEntity();
ecm.CreateComponent(eLink, components::Link());

Link link(eLink);
EXPECT_EQ(eLink, link.Entity());

ASSERT_TRUE(link.Valid(ecm));

// No ExternalWorldWrenchCmd should exist by default
EXPECT_EQ(nullptr, ecm.Component<components::ExternalWorldWrenchCmd>(eLink));

// Add force in Inertial Frame
math::Vector3d force(0, 0, 1.0);
link.AddForceInInertialFrame(ecm, force);

// No WorldPose or Inertial component exists so command should not work
EXPECT_EQ(nullptr, ecm.Component<components::ExternalWorldWrenchCmd>(eLink));

// create WorldPose and Inertial component and try adding force again
math::Pose3d linkWorldPose;
linkWorldPose.Set(1.0, 0.0, 0.0, 0, 0, GZ_PI_4);
math::Pose3d inertiaPose;
inertiaPose.Set(1.0, 2.0, 3.0, 0, GZ_PI_2, 0);
math::Inertiald linkInertial;
linkInertial.SetPose(inertiaPose);
ecm.CreateComponent(eLink, components::WorldPose(linkWorldPose));
ecm.CreateComponent(eLink, components::Inertial(linkInertial));
link.AddForceInInertialFrame(ecm, force);

// ExternalWorldWrenchCmd component should now be created
auto wrenchComp = ecm.Component<components::ExternalWorldWrenchCmd>(eLink);
EXPECT_NE(nullptr, wrenchComp);

// Summary of dynamic state at this time in the test:
// - The link has a world pose with position (1, 0, 0)
// and orientation (0, 0, π/4).
// - The link has an inertial pose with position (1, 2, 3)
// and orientation (0, π/2, 0).
// - A force of (0, 0, 1) N is applied in the inertial frame.
// Original force vector in inertial frame
// force = [0, 0, 1]
// Rotation matrix for inertiaPose (rotation about Y-axis by π/2 radians)
// Ry = [ cos(π/2) 0 sin(π/2)
// 0 1 0
// -sin(π/2) 0 cos(π/2) ]
// Ry = [ 0 0 1
// 0 1 0
// -1 0 0 ]
//
// Calculate linkForce = Ry * force:
// linkForce_x = (0 * 0) + (0 * 0) + (1 * 1) = 1
// linkForce_y = (0 * 0) + (1 * 0) + (0 * 1) = 0
// linkForce_z = (-1 * 0) + (0 * 0) + (0 * 1) = 0
//
// Thus, linkForce = [1, 0, 0]
//
// Rotation matrix for linkWorldPose (rotation about Z-axis by π/4 radians)
// Rz = [ cos(π/4) -sin(π/4) 0
// sin(π/4) cos(π/4) 0
// 0 0 1 ]
//
// Rz ≈ [ √2/2 -√2/2 0
// √2/2 √2/2 0
// 0 0 1 ]
//
// Calculate worldForce = Rz * linkForce:
// worldForce_x = (√2/2)*1 + (-√2/2)*0 + (0)*0 = √2/2 ≈ 0.7071
// worldForce_y = (√2/2)*1 + (√2/2)*0 + (0)*0 = √2/2 ≈ 0.7071
// worldForce_z = (0)*1 + (0)*0 + (1)*0 = 0
//
// Thus, worldForce ≈ [√2/2, √2/2, 0] ≈ [0.7071, 0.7071, 0]
//
// Expected torque in world frame
// First, rotate the inertia pose position to the world frame
// using the link's world pose rotation.
// Inertia pose position: (1, 2, 3)
// Link world pose rotation matrix (around Z-axis by π/4 radians):
// R_world = [ √2/2 -√2/2 0
// √2/2 √2/2 0
// 0 0 1]
//
// Rotate inertia pose position to world frame:
// r_world = R_world * [1, 2, 3] = [-√2/2, 3√2/2, 3]
//
// Then, calculate the cross product with the world force
// to get the expected torque.
// World force: (√2/2, √2/2, 0)
// Cross product:
// expectedTorque = r_world × F_world
// expectedTorque = (-3√2/2, 3√2/2, -2)
//
// After the calculations the expected wrench is
const double expectedForceX = std::sqrt(2) / 2;
const double expectedForceY = std::sqrt(2) / 2;
const double expectedForceZ = 0;
const double expectedTorqueX = -3 * std::sqrt(2)/2;
const double expectedTorqueY = 3 * std::sqrt(2)/2;
const double expectedTorqueZ = -2;

// verify Wrench values
auto wrenchMsg = wrenchComp->Data();

// Looser tolerances are needed for the nonzero terms
EXPECT_NEAR(expectedForceX, wrenchMsg.force().x(), 1e-2);
EXPECT_NEAR(expectedForceY, wrenchMsg.force().y(), 1e-2);
EXPECT_NEAR(expectedForceZ, wrenchMsg.force().z(), 1e-6);
EXPECT_NEAR(expectedTorqueX, wrenchMsg.torque().x(), 1e-2);
EXPECT_NEAR(expectedTorqueY, wrenchMsg.torque().y(), 1e-2);
EXPECT_NEAR(expectedTorqueZ, wrenchMsg.torque().z(), 1e-2);

// apply opposite force. Since the cmd is not processed yet, this should
// cancel out the existing wrench cmd
link.AddForceInInertialFrame(ecm, -force);
wrenchComp = ecm.Component<components::ExternalWorldWrenchCmd>(eLink);
EXPECT_NE(nullptr, wrenchComp);
wrenchMsg = wrenchComp->Data();

EXPECT_EQ(math::Vector3d::Zero, math::Vector3d(
wrenchMsg.force().x(), wrenchMsg.force().y(), wrenchMsg.force().z()));
EXPECT_EQ(math::Vector3d::Zero, math::Vector3d(
wrenchMsg.torque().x(), wrenchMsg.torque().y(), wrenchMsg.torque().z()));

// Add force in Inertial Frame at an offset
math::Vector3d offset{1.0, 0.0, 0.0};
link.AddForceInInertialFrame(ecm, force, offset);

wrenchComp = ecm.Component<components::ExternalWorldWrenchCmd>(eLink);
EXPECT_NE(nullptr, wrenchComp);

// Summary of dynamic state at this time in the test:
// - The link has a world pose with position (1, 0, 0)
// and orientation (0, 0, π/4).
// - The link has an inertial pose with position (1, 2, 3)
// and orientation (0, π/2, 0).
// - A force of (0, 0, 1) N is applied at an offset (1, 0, 0),
// both force and offset are expressed in terms of the inertial frame.
// Calculate the offset expressed in terms of link's coordinate frame
// First, create a pose for the offset in the inertial frame.
// offset = (1, 0, 0)
// forceApplicationRelativeToInertialFrame =
// Pose3d(offset, Quaterniond::Identity)
// This pose represents a translation by the
// offset vector without any rotation.
// Transform this pose by the inertial pose to get the
// offset in the link's frame.
// Inertial pose: (1, 2, 3) with orientation (0, π/2, 0)
// Inertial rotation matrix (around Y-axis by π/2 radians):
// Ry = [ 0 0 1
// 0 1 0
// -1 0 0 ]
//
// Apply this rotation to the offset pose:
// offsetInLinkFrame = Ry * [1, 0, 0]^T
// = [0 0 1] * [1]
// [0 1 0] [0]
// [-1 0 0] [0]
// = [0, 0, -1]
// offsetInLInkFrame = [0, 0, -1]
//
// Calculate the force expressed in link's coordinate frame with offset
// First, rotate the force by the inertia pose's rotation.
// Inertia pose rotation matrix (around Y-axis by π/2 radians):
// Ry = [ 0 0 1
// 0 1 0
// -1 0 0 ]
// Force in inertial frame: (0, 0, 1)
// linkForceWithOffset = Ry * force = [1, 0, 0]
// Then, rotate this force by the link's world pose
// rotation to get the world force.
// Link world pose rotation matrix (around Z-axis by π/4 radians):
// Rz = [ √2/2 -√2/2 0
// √2/2 √2/2 0
// 0 0 1 ]
// worldForceWithOffset = Rz * linkForceWithOffset = [√2/2, √2/2, 0]
//
// Calculate the expected torque with offset
// First, calculate the effective position vector in the world frame.
// offsetInLinkFrame = [0, 0, -1]
// inertiaPose.Pos() = [1, 2, 3]
// effectivePosition = offsetInLinkFrame + inertiaPose.Pos() = [1, 2, 2]
// Rotate this effective position by the link's world pose rotation:
// effectivePositionWorld = Rz * effectivePosition
// = [-√2/2, 3√2/2, 2]
// Calculate the cross product with the world force to get the torque.
// worldForceWithOffset = [√2/2, √2/2, 0]
// expectedTorqueWithOffset = effectivePositionWorld × worlapdForceWithOffset
// expectedTorqueWithOffset = (-√2, √2, -2)
// After the calculations the expected wrench is
const double expectedForceWithOffsetX = std::sqrt(2) / 2;
const double expectedForceWithOffsetY = std::sqrt(2) / 2;
const double expectedForceWithOffsetZ = 0;
const double expectedTorqueWithOffsetX = -1 * std::sqrt(2);
const double expectedTorqueWithOffsetY = std::sqrt(2);
const double expectedTorqueWithOffsetZ = -2;

wrenchMsg = wrenchComp->Data();

// Looser tolerances are needed for the nonzero terms
EXPECT_NEAR(expectedForceWithOffsetX, wrenchMsg.force().x(), 1e-2);
EXPECT_NEAR(expectedForceWithOffsetY, wrenchMsg.force().y(), 1e-2);
EXPECT_NEAR(expectedForceWithOffsetZ, wrenchMsg.force().z(), 1e-6);
EXPECT_NEAR(expectedTorqueWithOffsetX, wrenchMsg.torque().x(), 1e-2);
EXPECT_NEAR(expectedTorqueWithOffsetY, wrenchMsg.torque().y(), 1e-2);
EXPECT_NEAR(expectedTorqueWithOffsetZ, wrenchMsg.torque().z(), 1e-2);

// apply opposite force again and verify the resulting wrench values are zero
link.AddForceInInertialFrame(ecm, -force, offset);
wrenchComp = ecm.Component<components::ExternalWorldWrenchCmd>(eLink);
EXPECT_NE(nullptr, wrenchComp);
wrenchMsg = wrenchComp->Data();

EXPECT_EQ(math::Vector3d::Zero, math::Vector3d(
wrenchMsg.force().x(), wrenchMsg.force().y(), wrenchMsg.force().z()));
EXPECT_EQ(math::Vector3d::Zero, math::Vector3d(
wrenchMsg.torque().x(), wrenchMsg.torque().y(), wrenchMsg.torque().z()));

}