Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Release Versions
- feat: expose pinocchio::Data through getter (#238)
- feat: add functionality to create a robot model from string (#200)
- feat: add joint types & limit some uses to supported types only (#243)
- feat: restructure joint getters and support continuous joints (#245, #246)

## 9.2.0

Expand Down
2 changes: 1 addition & 1 deletion source/controllers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ auto ctrl = CartesianControllerFactory::create_controller(CONTROLLER_TYPE::IMPED
auto robot = robot_model::Model("my_robot", "/path/to/robot.urdf");

// create some state variables
auto current_joints = JointState::Random(robot.get_robot_name(), robot.get_number_of_joints());
auto current_joints = JointState::Random(robot.get_robot_name(), robot.get_configuration_dimension());
auto command_state = CartesianState::Random("command");
auto feedback_state = CartesianState::Random("feedback");

Expand Down
3 changes: 2 additions & 1 deletion source/controllers/src/ControllerFactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ std::shared_ptr<IController<JointState>> ControllerFactory<JointState>::create_c
CONTROLLER_TYPE type, const std::list<std::shared_ptr<ParameterInterface>>& parameters,
const robot_model::Model& robot_model
) {
auto ctrl = ControllerFactory<JointState>::create_controller(type, parameters, robot_model.get_number_of_joints());
auto ctrl =
Copy link
Member

Choose a reason for hiding this comment

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

Sorry I keep being confused about this, the configuration dimension is the number of variables necessary to describe the joint state, right? I'm not sure that our idea of controllers is compatible with anything else than revolute and prismatic joint or how would an impedance controller look like for a continuous joint? It doesn't work because the two numbers are coupled no?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes, it would fail. I only supported forward kinematics and velocities, so everything else is still incompatible (theoretically and in practice).

So perhaps we should completely fail exactly in this place, but indeed here it should also be number of joints, not configuration parameters

ControllerFactory<JointState>::create_controller(type, parameters, robot_model.get_configuration_dimension());
if (ctrl == nullptr) {
throw exceptions::InvalidControllerException("Cannot assign robot model to this controller!");
}
Expand Down
8 changes: 4 additions & 4 deletions source/controllers/test/tests/test_controller_factory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ TEST(ControllerFactoryTest, CreateControllerWithRobot) {

auto command_state = CartesianState::Identity(robot.get_frames().back(), robot.get_base_frame());
auto feedback_state = CartesianState::Identity(robot.get_frames().back(), robot.get_base_frame());
auto joint_state = JointState::Zero(robot_name, robot.get_number_of_joints());
auto joint_state = JointState::Zero(robot_name, robot.get_configuration_dimension());

EXPECT_THROW(
CartesianControllerFactory::create_controller(CONTROLLER_TYPE::NONE, robot),
Expand All @@ -203,7 +203,7 @@ TEST(ControllerFactoryTest, CreateControllerWithRobot) {
EXPECT_NO_THROW(joint_ctrl->compute_command(joint_state, joint_state));
EXPECT_EQ(
joint_ctrl->get_parameter_value<Eigen::MatrixXd>("stiffness").size(),
robot.get_number_of_joints() * robot.get_number_of_joints()
robot.get_configuration_dimension() * robot.get_configuration_dimension()
);
}

Expand All @@ -221,8 +221,8 @@ TEST(ControllerFactoryTest, CreateControllerWithRobotAndParams) {
ASSERT_NE(ctrl, nullptr);
EXPECT_EQ(
ctrl->get_parameter_value<Eigen::MatrixXd>("damping").size(),
robot.get_number_of_joints() * robot.get_number_of_joints()
robot.get_configuration_dimension() * robot.get_configuration_dimension()
);

EXPECT_EQ(ctrl->get_parameter_value<Eigen::MatrixXd>("damping").sum(), 5.0 * robot.get_number_of_joints());
EXPECT_EQ(ctrl->get_parameter_value<Eigen::MatrixXd>("damping").sum(), 5.0 * robot.get_configuration_dimension());
}
62 changes: 58 additions & 4 deletions source/robot_model/include/robot_model/Model.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ class Model {
*/
std::vector<pinocchio::CollisionPair> generate_joint_exclusion_list();

protected:
Eigen::VectorXd
expand_joint_positions_to_full_configuration(const state_representation::JointPositions& joint_positions) const;

public:
/**
* @brief Construct with robot name and path to URDF file
Expand Down Expand Up @@ -307,10 +311,22 @@ class Model {

/**
* @brief Getter of the number of joints
* @return the number of joints
* @return the number of joints (minus pinocchio's universe root joint and fixed joints)
*/
unsigned int get_number_of_joints() const;

/**
* @brief Getter of the degrees of freedom (dof) of the model
* @return the number of degrees of freedom (dof) of the model
*/
unsigned int get_dof() const;

/**
* @brief Getter of the configuration dimension of the model
* @return the number of configuration dimensions of the model
*/
unsigned int get_configuration_dimension() const;

/**
* @brief Getter of the joint frames from the model
* @return the joint frames
Expand All @@ -321,7 +337,7 @@ class Model {
* @brief Getter of the types of the current model's joints
* @return a vector of joint types
*/
std::vector<JointType> get_joint_types();
std::vector<JointType> get_joint_types() const;

/**
* @brief Getter of the frames from the model
Expand Down Expand Up @@ -651,7 +667,15 @@ inline std::optional<std::reference_wrapper<const std::string>> Model::get_urdf_
}

inline unsigned int Model::get_number_of_joints() const {
return this->robot_model_.nq;// ! todo: this is actually incorrect, that gives dofs
return this->robot_model_.njoints - 1;
}

inline unsigned int Model::get_dof() const {
return this->robot_model_.nv;
}

inline unsigned int Model::get_configuration_dimension() const {
return this->robot_model_.nq;
}

inline std::vector<std::string> Model::get_joint_frames() const {
Expand Down Expand Up @@ -726,7 +750,7 @@ struct JointTypeVisitor : public boost::static_visitor<JointType> {
}
};

inline std::vector<JointType> Model::get_joint_types() {
inline std::vector<JointType> Model::get_joint_types() const {
std::vector<JointType> joint_types;
JointTypeVisitor classifier;
for (int j = 1; j < this->robot_model_.njoints; ++j) {// skips the first joint (universe)
Expand All @@ -736,4 +760,34 @@ inline std::vector<JointType> Model::get_joint_types() {
}
return joint_types;
}

inline Eigen::VectorXd
Model::expand_joint_positions_to_full_configuration(const state_representation::JointPositions& joint_positions) const {
if (this->get_number_of_joints() == this->get_configuration_dimension()) {
return joint_positions.get_positions();
}
Eigen::VectorXd q(this->get_configuration_dimension());
const auto& joint_types = this->get_joint_types();
const Eigen::VectorXd& positions = joint_positions.get_positions();
std::size_t joint_input_idx = 0;
for (int i = 1; i < this->robot_model_.njoints; ++i) {
const auto& joint = this->robot_model_.joints[i];
std::size_t idx_q = joint.idx_q();
switch (joint_types[i - 1]) {
case JointType::CONTINUOUS: {
Copy link
Member

Choose a reason for hiding this comment

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

Assuming we have a URDF with floating or planar joints, does it construct? Is there a chance we get to this switch case with that URDF?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes it would construct. And with the condition I have in the constructor where I don't run the QP if we have limited support, we could indeed get here I think.

So in general there is still a question of where we should fail the construction of the model, or if we directly provide a separate model for mobile robots and fail everything else in this for example

Copy link
Member

Choose a reason for hiding this comment

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

I think it might make sense to have the current model throw on construction if there are joints that are not revolute or prismatic -> this way our controllers can stay the way they are. Then we make a derived class (we still prefer most of the logic to be in the current class, e.g. the getters are valid here) that can handle mobile robots which is just a different way of saying we support continuous joints as well but we should still throw on construction for planar and floating? What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah I think that's going to be cleaner (and addresses the performance concerns with the if-else). I'll create a commit for this so that we can see what this looks like

Copy link
Member Author

@bpapaspyros bpapaspyros Jul 1, 2025

Choose a reason for hiding this comment

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

we should also perhaps consider a robot model factory that controllers/hardware can use, but that may imply we need to make a cleaner interface. That's why in a sense I was also considering

                RobotModel
               /        \
      RobotArmModel   MobileRobotModel

Copy link
Member

Choose a reason for hiding this comment

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

A factory could also work even though I have a hard time seeing where we'd need that kind of inheritance since it feels like a controller is always just one or the other. But also an option

Copy link
Member

Choose a reason for hiding this comment

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

It might actually also be unfavorable to derive a MobileRobotModel from the current RobotModel because one could maybe mess up things through pointer injection? Idk

Copy link
Member Author

Choose a reason for hiding this comment

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

yup, all valid. My point is, before we have a pinocchio model somewhere we technically don't know what robot we have. But sure, a controller may explicitly have to use the correct class, but then technically we'd have to manage this in controllers that support both

double angle = positions[joint_input_idx++];
q[idx_q + 0] = std::cos(angle);
q[idx_q + 1] = std::sin(angle);
break;
}
default: {
for (int j = 0; j < joint.nq(); ++j) {
q[idx_q + j] = positions[joint_input_idx++];
}
break;
}
}
}
return q;
}
}// namespace robot_model
56 changes: 46 additions & 10 deletions source/robot_model/src/Model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -277,16 +277,50 @@ Model::compute_jacobian(const state_representation::JointPositions& joint_positi
throw exceptions::InvalidJointStateSizeException(joint_positions.get_size(), this->get_number_of_joints());
}
// compute the Jacobian from the joint state
pinocchio::Data::Matrix6x J(6, this->get_number_of_joints());
pinocchio::Data::Matrix6x J(6, this->get_configuration_dimension());
J.setZero();
pinocchio::computeFrameJacobian(
this->robot_model_, this->robot_data_, joint_positions.data(), frame_id, pinocchio::LOCAL_WORLD_ALIGNED, J
);
// the model does not have any reference frame
return state_representation::Jacobian(
this->get_robot_name(), this->get_joint_frames(), this->robot_model_.frames[frame_id].name, J,
this->get_base_frame()
);
if (this->get_configuration_dimension() != this->get_number_of_joints()) [[unlikely]] {
Eigen::VectorXd q = this->expand_joint_positions_to_full_configuration(joint_positions);
Eigen::MatrixXd P(this->robot_model_.nq, this->robot_model_.nv);
P.setZero();
static const auto& joint_types = this->get_joint_types();
for (int joint_input_idx = 0, i = 1; i < this->robot_model_.njoints; ++i) {
const auto& joint = this->robot_model_.joints[i];
std::size_t idx_q = joint.idx_q();
std::size_t idx_v = joint.idx_v();
switch (joint_types[i - 1]) {
case JointType::CONTINUOUS: {
double angle = joint_positions.get_positions()[joint_input_idx++];
P(idx_q, idx_v) = -std::sin(angle);
P(idx_q + 1, idx_v) = std::cos(angle);
break;
}
default: {
for (int j = 0; j < joint.nq(); ++j) {
P(idx_q + j, idx_v + j) = 1.0;
}
joint_input_idx += joint.nq();
break;
}
}
}
Eigen::MatrixXd Jv = J * P;
pinocchio::computeFrameJacobian(
this->robot_model_, this->robot_data_, q, frame_id, pinocchio::LOCAL_WORLD_ALIGNED, Jv
);
return state_representation::Jacobian(
this->get_robot_name(), this->get_joint_frames(), this->robot_model_.frames[frame_id].name, Jv,
this->get_base_frame()
);
} else [[likely]] {
pinocchio::computeFrameJacobian(
this->robot_model_, this->robot_data_, joint_positions.data(), frame_id, pinocchio::LOCAL_WORLD_ALIGNED, J
);
return state_representation::Jacobian(
this->get_robot_name(), this->get_joint_frames(), this->robot_model_.frames[frame_id].name, J,
this->get_base_frame()
);
}
}

state_representation::Jacobian
Expand Down Expand Up @@ -373,8 +407,10 @@ std::vector<state_representation::CartesianPose> Model::forward_kinematics(
if (joint_positions.get_size() != this->get_number_of_joints()) {
throw exceptions::InvalidJointStateSizeException(joint_positions.get_size(), this->get_number_of_joints());
}

Eigen::VectorXd q = this->expand_joint_positions_to_full_configuration(joint_positions);
std::vector<state_representation::CartesianPose> pose_vector;
pinocchio::forwardKinematics(this->robot_model_, this->robot_data_, joint_positions.data());
pinocchio::forwardKinematics(this->robot_model_, this->robot_data_, q);
for (unsigned int id : frame_ids) {
if (id >= static_cast<unsigned int>(this->robot_model_.nframes)) {
throw exceptions::FrameNotFoundException(std::to_string(id));
Expand Down
64 changes: 50 additions & 14 deletions source/robot_model/test/tests/test_joint_types.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "robot_model/Model.hpp"

#include <cassert>
#include <cstddef>
#include <state_representation/space/joint/JointState.hpp>

#include <gtest/gtest.h>
Expand All @@ -21,24 +22,59 @@ class JointTypeTesting : public testing::Test {

TEST_F(JointTypeTesting, CheckContinuousJoint) {
// clang-format off
auto urdf =
"<?xml version=\"1.0\" encoding=\"utf-8\"?>"
"<robot name=\"continuous\">"
"<link name=\"world\"/>"
"<joint name=\"base_joint\" type=\"continuous\">"
"<parent link=\"world\"/>"
"<child link=\"base\"/>"
"<axis xyz=\"0 0 1\"/>"
"</joint>"
"<link name=\"base\"/>"
"</robot>";
auto urdf =
"<?xml version=\"1.0\" encoding=\"utf-8\"?>"
"<robot name=\"continuous\">"
"<link name=\"world\"/>"
"<joint name=\"base_joint\" type=\"continuous\">"
"<parent link=\"world\"/>"
"<child link=\"base\"/>"
"<axis xyz=\"0 0 1\"/>"
"</joint>"
"<link name=\"base\"/>"
"</robot>";
// clang-format on

this->robot_ = std::make_unique<Model>("continuous", urdf);
EXPECT_STREQ(this->robot_->get_robot_name().c_str(), "continuous");
auto joint_types = this->robot_->get_joint_types();
EXPECT_EQ(joint_types.size(), 1);
EXPECT_EQ(joint_types.at(0), JointType::CONTINUOUS);
EXPECT_EQ(this->robot_->get_number_of_joints(), 2);
EXPECT_EQ(this->robot_->get_number_of_joints(), 1);
EXPECT_EQ(this->robot_->get_dof(), 1);
EXPECT_EQ(this->robot_->get_configuration_dimension(), 2);

auto& data = this->robot_->get_pinocchio_data();

const double angle = M_PI / 3.0;
Eigen::VectorXd q = Eigen::VectorXd::Zero(this->robot_->get_number_of_joints());
for (size_t i = 0; i < this->robot_->get_joint_types().size(); ++i) {
if (this->robot_->get_joint_types().at(i) == JointType::CONTINUOUS) {
q[i] = angle;
}
}
state_representation::JointState joint_state(this->robot_->get_robot_name(), this->robot_->get_joint_frames());
joint_state.set_positions(q);
this->robot_->forward_kinematics(joint_state);

const auto& M = data.oMi[1];
Eigen::Matrix3d R_exp = Eigen::AngleAxisd(angle, Eigen::Vector3d::UnitZ()).toRotationMatrix();
EXPECT_TRUE(M.rotation().isApprox(R_exp, 1e-9)) << "Rotation should be a pure Rz(pi/3)";

Eigen::VectorXd v(1);
v << 1.0;
joint_state.set_velocities(v);

auto twist = this->robot_->forward_velocity(joint_state);
EXPECT_EQ(twist.get_reference_frame(), this->robot_->get_base_frame());
EXPECT_EQ(twist.get_name(), "base");

Eigen::Vector3d expected_angular(0.0, 0.0, 1.0);
EXPECT_TRUE(twist.get_angular_velocity().isApprox(expected_angular, 1e-9))
<< "Expected angular velocity [0, 0, 1], got: " << twist.get_angular_velocity().transpose();

EXPECT_TRUE(twist.get_linear_velocity().isZero(1e-9))
<< "Expected zero linear velocity, got: " << twist.get_linear_velocity().transpose();
}

TEST_F(JointTypeTesting, CheckPrismaticJoint) {
Expand Down Expand Up @@ -84,7 +120,7 @@ TEST_F(JointTypeTesting, CheckFloatingJoint) {
// clang-format on
this->robot_ = std::make_unique<Model>("floating", urdf);
EXPECT_STREQ(this->robot_->get_robot_name().c_str(), "floating");
EXPECT_EQ(this->robot_->get_number_of_joints(), 7);
EXPECT_EQ(this->robot_->get_number_of_joints(), 1);
auto joint_types = this->robot_->get_joint_types();
EXPECT_EQ(joint_types.size(), 1);
EXPECT_EQ(joint_types.at(0), JointType::FLOATING);
Expand Down Expand Up @@ -128,7 +164,7 @@ TEST_F(JointTypeTesting, CheckPlanarJoint) {
// clang-format on
this->robot_ = std::make_unique<Model>("planar", urdf);
EXPECT_STREQ(this->robot_->get_robot_name().c_str(), "planar");
EXPECT_EQ(this->robot_->get_number_of_joints(), 4);
EXPECT_EQ(this->robot_->get_number_of_joints(), 1);
auto joint_types = this->robot_->get_joint_types();
EXPECT_EQ(joint_types.size(), 1);
EXPECT_EQ(joint_types.at(0), JointType::PLANAR);
Expand Down
Loading