Qontrol
Torque control using Generalized resolution

Introduction

This resolution strategy is based on the Generalized Hierarchical Control method published in this paper. Most existing techniques to handle strict task priorities in hierarchical control are based on null-space projectors or a sequence of quadratic programs; whereas non strict task priorities are usually handled by optimization based on a weighting strategy. This approach handles both strict and non-strict priorities of an arbitrary number of tasks, and achieves multiple priority rearrangements simultaneously. It relies on a generalized projector, which makes it possible to completely project a task into the null-space of a set of tasks, while partially projecting it into the null-space of some other tasks. Priority transitions are achieved by smooth variations of the generalized projector.

Formulation

The following example solves a qp problem expressed at the torque level such that:

\begin{equation} \begin{array}{l} X^T = [\ddot{q}_1^T, \ldots, \ddot{q}_{n_t}^T] \\ X^{opt} = \underset{X}{\mathrm{argmin}} \sum_{i=1}^{n_t} \left\| J_i S_{q_i} X + \dot{J}_i \dot{q} - a_i^* \right\|^2 + \left\| X \right\|^2 \\ \textrm{s.t.} \\ \quad 2 \left( \frac{q_{min} - q_k}{\Delta T^2} - \frac{\dot{q}_k}{\Delta T} \right) \leq PX \leq 2 \left( \frac{q_{max} - q_k}{\Delta T^2} - \frac{\dot{q}_k}{\Delta T} \right)\\ \quad \frac{\dot{q}_{min} - \dot{q}_k}{\Delta T} \leq PX \leq \frac{\dot{q}_{max} - \dot{q}_k}{\Delta T} \\ \quad \tau_{min} - n \leq BPX \leq \tau_{max} - n \\ P = [P_1, \ldots, P_{n_t}], \quad \ddot{q} = PX = \sum_i P_i \ddot{q}_i \\ \tau = BPX^{opt} + n \end{array} \end{equation}

Where P is the concatenation of the generalized projectors of each task.

For the i-th task we compute its projector \(P_i\) using its corresponding \(\alpha_i\) input: \( P_i(\alpha_i) = I_n − B_i(J_{s_i})^T \alpha_{i,r}^s(\alpha_i ,origin) B_i (J_{s_i} ) \)

Where \( J_{s_i}\) is a matrix made of the rows of J sorted in descending order with respect to the values of the diagonal elements in \(\alpha_i\) .

The values in \(\alpha_i\) are sorted accordingly, leading to \(\alpha_{i}^s\).

\(B_i(J_{s_i})\) is a \(r\) x \(n\) matrix , (where \(r = rank(J_{s_i} )\)), such that the rows of \(B_i(J_{s_i})\) form an orthonormal basis of the joint space.

Finally, \(\alpha_{i,r}^s\) is a diagonal matrix of degree r. The vector \(origin\) is a vector of the row indexes of \( J_{s_i}\) selected during the construction of the orthonormal basis \( B_i\)

The robot main tasks consists in keeping the end effector at a defined pose in Cartesian space. A second task is added to control the position on the y axis of the elbow of the robot. The mujoco library is used to simulate the robot behaviour.

Simulation

To run this example run the following command from the build/examples directory:

./torqueQontrol_GHC robot_name

where robot_name can be only panda

Full code

1 // Copyright 2021 DeepMind Technologies Limited
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 #include "mujoco/mujoco_sim.h"
16 #include "Qontrol/Qontrol.hpp"
17 #include "trajectory_generation/trajectory_generation.h"
18 
19 using namespace Qontrol;
20 
21 class MujocoQontrol : public MujocoSim
22 {
23 public:
24 //------------------------------------------- simulation -------------------------------------------
25 std::shared_ptr<Model::RobotModel<ModelImpl::PINOCCHIO>> model;
26 std::shared_ptr<JointTorqueProblem> torque_problem;
27 std::shared_ptr<Task::CartesianAcceleration<ControlOutput::JointTorque>> main_task;
28 std::shared_ptr<Task::CartesianAcceleration<ControlOutput::JointTorque>> elbow_task;
29 // std::shared_ptr<Qontrol::Task::GenericTask> main_task;
30 // std::shared_ptr<Qontrol::Task::GenericTask> elbow_task;
31 std::shared_ptr<Qontrol::Task::GenericTask> regularization_task;
32 
33 Qontrol::RobotState robot_state;
34 TrajectoryGeneration* traj;
35 std::string resource_path;
36 bool initialized = false;
37 double t = 0;
38 
39 void initController() override
40 {
41  model =
43 
44  torque_problem = std::make_shared<Qontrol::JointTorqueProblem>(model,ResolutionStrategy::Generalized);
45  //tasks (weights are useless)
46  main_task = torque_problem->task_set->add<Task::CartesianAcceleration>("MainTask");
47  elbow_task = torque_problem->task_set->add<Task::CartesianAcceleration>("CommunicationTask");
48  Eigen::MatrixXd selection_matrix = Eigen::MatrixXd::Zero(6,6);
49  selection_matrix(1,1) = 1.0; //y axis control only
50  elbow_task->setControledFrameName("panda_link4");
51  elbow_task->setReferenceFrame(ReferenceFrame::LOCAL_WORLD_ALIGNED);
52  elbow_task->setSelectionMatrix(selection_matrix);
53  regularization_task = torque_problem->task_set->add("regularizationTask",14,14,1.0); //regularisation dimension = n_dof*n_tasks (other than regularisation)
54  //constraints
55  auto joint_configuration_constraint = torque_problem->constraint_set->add<Constraint::JointConfiguration>("JointConfigurationConstraint");
56  auto joint_velocity_constraint = torque_problem->constraint_set->add<Constraint::JointVelocity>("JointVelocityConstraint");
57  auto joint_torque_constraint = torque_problem->constraint_set->add<Constraint::JointTorque>("JointTorqueConstraint");
58 
59  joint_configuration_constraint->setHorizon(30);
60  joint_velocity_constraint->setHorizon(30);
61 
62  //init() -> resizes properly some variables / to be called once the tasks and constraints are defined
63  torque_problem->init();
64  std::cout << "Model loaded with " << model->getNrOfDegreesOfFreedom() << " DoF" << std::endl;
65 
66  mju_copy(d->qpos, m->key_qpos, m->nu);
67  robot_state.joint_position.resize(model->getNrOfDegreesOfFreedom());
68  robot_state.joint_velocity.resize(model->getNrOfDegreesOfFreedom());
69 
70  traj = new TrajectoryGeneration(resource_path+"trajectory.csv", m->opt.timestep);
71 
72  initialized = true;
73 }
74 
75 void updateController() override
76 {
77 
78  while (!initialized)
79  {
80  // init_pose = model->getFramePose(model->getTipFrameName());
81  std::cout << "Waiting for initialization" << std::endl;
82  std::this_thread::sleep_for(std::chrono::milliseconds(100));
83  }
84 
85  for (int i=0; i<model->getNrOfDegreesOfFreedom() ; ++i)
86  {
87  robot_state.joint_position[i] = d->qpos[i];
88  robot_state.joint_velocity[i] = d->qvel[i];
89  }
90  model->setRobotState(robot_state);
91 
92  traj->update();
93  pinocchio::SE3 traj_pose(traj->pose.matrix());
94 
95  //get desired cartesian acceleration for main task
96  pinocchio::SE3 current_pose(model->getFramePose(model->getTipFrameName()).matrix());
97  const pinocchio::SE3 tipMdes = current_pose.actInv(traj_pose);
98  auto err = pinocchio::log6(tipMdes).toVector();
99 
100  Eigen::Matrix<double, 6, 1> p_gains;
101  p_gains << 1000, 1000, 1000, 1000, 1000, 1000;
102 
103  Eigen::Matrix<double, 6, 1> d_gains = 2.0 * p_gains.cwiseSqrt();
104  Eigen::Matrix<double, 6, 1> xdd_star =
105  p_gains.cwiseProduct(err) -
106  d_gains.cwiseProduct(model->getFrameVelocity(model->getTipFrameName())) ;
107 
108  double y_des_elbow =0.15;
109  t+=m->opt.timestep;
110  if(sin(0.5*t)<0){
111  y_des_elbow = -0.15;
112  }
113  double current_y_elbow_pose = model->getFramePose("panda_link4").matrix()(1,3);
114  double error_elbow_y = y_des_elbow-current_y_elbow_pose;
115  Eigen::VectorXd xdd_star_elbow = Eigen::VectorXd::Zero(6);
116  xdd_star_elbow[1]= 500.0 * error_elbow_y -100.0*model->getFrameVelocity("panda_link4").matrix()(1,3);
117 
118  Eigen::MatrixXd alpha_1 = Eigen::MatrixXd::Zero(7,7); //here absolute priority to main task
119  Eigen::MatrixXd alpha_2 = Eigen::MatrixXd::Identity(7,7); //activation of the elbow task but should not impac tthe main task
120  alpha_2(6,6)=0.0;
121  std::vector<Eigen::MatrixXd> alphas;
122  alphas.push_back(alpha_1);
123  alphas.push_back(alpha_2);
124 
125  //update the projectors of GHC
126  torque_problem->updateProjector(alphas);
127 
128  //TASKS UPDATE
129  main_task->setTargetAcceleration(xdd_star);
130  elbow_task->setTargetAcceleration(xdd_star_elbow);
131 
132  regularization_task->setE(0.00001*Eigen::MatrixXd::Identity(14,14)); //here 0.00001 is the weight of regularization task
133  regularization_task->setf(Eigen::VectorXd::Zero(14));
134 
135  //update torque problem
136  torque_problem->update(m->opt.timestep);
137 
138  if (torque_problem->solutionFound())
139  {
140  sendJointTorque(torque_problem->getJointTorqueCommand());
141  }
142 }
143 };
144 
145 
146 int main(int argc, const char** argv) {
147  MujocoQontrol mujoco_qontrol;
148  Qontrol::Log::Logger::parseArgv(argc, argv);
149 
150  mjvCamera cam;
151  mjv_defaultCamera(&cam);
152 
153  mjvOption opt;
154  mjv_defaultOption(&opt);
155 
156  mjvPerturb pert;
157  mjv_defaultPerturb(&pert);
158 
159  // simulate object encapsulates the UI
160  auto sim = std::make_unique<mj::Simulate>(
161  std::make_unique<mj::GlfwAdapter>(),
162  &cam, &opt, &pert, /* is_passive = */ false
163  );
164 
165  std::string robot = argv[1];
166  std::string mujoco_scene = "./resources/"+robot+"/scene.xml";
167  mujoco_qontrol.resource_path = "./resources/"+robot+"/";
168 
169  // start physics thread
170  std::thread physicsthreadhandle( &MujocoQontrol::PhysicsThread, mujoco_qontrol, sim.get(), mujoco_scene.c_str());
171 
172  // start simulation UI loop (blocking call)
173  sim->RenderLoop();
174  physicsthreadhandle.join();
175 
176  return 0;
177 }

Explanation of the code

Declaration

First we declare all the objects that will be used to define our problem.

0 std::shared_ptr<Model::RobotModel<ModelImpl::PINOCCHIO>> model;

We use pinocchio for our model library.

25 std::shared_ptr<JointTorqueProblem> torque_problem;

The output of our qp controller is at the torque level.

26 std::shared_ptr<Task::CartesianAcceleration<ControlOutput::JointTorque>> main_task;

We then declare tree tasks that will be updated every milliseconds.

The main task is expressed as a Cartesian Acceleration task.

27 std::shared_ptr<Task::CartesianAcceleration<ControlOutput::JointTorque>> elbow_task;

We add the secondary task to control the elbow along y axis.

28 std::shared_ptr<Qontrol::Task::GenericTask> regularization_task;

And we add a regularisation task (also at the torque level).

Initialization

39 void initController() override
40 {
41  model =
42  Model::RobotModel<ModelImpl::PINOCCHIO>::loadModelFromFile(resource_path+"robot.urdf");

During initialization we instantiate the model with the robot urdf.

We initialize the problem by giving it the model. By default, the qpmad library is used.

43 
44  torque_problem = std::make_shared<Qontrol::JointTorqueProblem>(model,ResolutionStrategy::Generalized);
45  //tasks (weights are useless)
46  main_task = torque_problem->task_set->add<Task::CartesianAcceleration>("MainTask");
47  elbow_task = torque_problem->task_set->add<Task::CartesianAcceleration>("CommunicationTask");
48  Eigen::MatrixXd selection_matrix = Eigen::MatrixXd::Zero(6,6);
49  selection_matrix(1,1) = 1.0; //y axis control only
50  elbow_task->setControledFrameName("panda_link4");
51  elbow_task->setReferenceFrame(ReferenceFrame::LOCAL_WORLD_ALIGNED);
52  elbow_task->setSelectionMatrix(selection_matrix);
53  regularization_task = torque_problem->task_set->add("regularizationTask",14,14,1.0); //regularisation dimension = n_dof*n_tasks (other than regularisation)

We then fill the task set of torque_problem with the main task, the elbow task and the regularisation task. We define a controlled frame for each task. By default it is the end effector of the robot. Additionnaly, it is possible to define the reference frame considered (local,world, local_world_aligned). The default value is LOCAL. Finally, if the cartesian task dimension is lower than 6, a selection matrix can be defined. Each tasks is given a name and a relative weight \( \omega \). This weight is not considered when we use the Generalized approach.

54  //constraints
55  auto joint_configuration_constraint = torque_problem->constraint_set->add<Constraint::JointConfiguration>("JointConfigurationConstraint");
56  auto joint_velocity_constraint = torque_problem->constraint_set->add<Constraint::JointVelocity>("JointVelocityConstraint");
57  auto joint_torque_constraint = torque_problem->constraint_set->add<Constraint::JointTorque>("JointTorqueConstraint");

We then define the constraint set of torque_problem. Constraints of the robot are already pre-implemented and can be added as shown in this example.

58 
59  joint_configuration_constraint->setHorizon(30);
60  joint_velocity_constraint->setHorizon(30);
61 
62  //init() -> resizes properly some variables / to be called once the tasks and constraints are defined
63  torque_problem->init();

Here we define that we are not using the default Weighting resolution strategy. It MUST be called after the tasks and constraints definition.

Update

76 void updateController() override
77 {
78 
79  while (!initialized)
80  {
81  // init_pose = model->getFramePose(model->getTipFrameName());
82  std::cout << "Waiting for initialization" << std::endl;
83  std::this_thread::sleep_for(std::chrono::milliseconds(100));
84  }
85 
86  for (int i=0; i<model->getNrOfDegreesOfFreedom() ; ++i)
87  {
88  robot_state.joint_position[i] = d->qpos[i];
89  robot_state.joint_velocity[i] = d->qvel[i];
90  }
91  model->setRobotState(robot_state);
92 
93  traj->update();

The update function is called every milliseconds. At the beginning of each update we fill the new robot state according to the simulated robot.

We also define the Cartesian goal for the end effector.

94  pinocchio::SE3 traj_pose(traj->pose.matrix());
95 
96  //get desired cartesian acceleration for main task
97  pinocchio::SE3 current_pose(model->getFramePose(model->getTipFrameName()).matrix());

We then compute the desired Cartesian acceleration using a simple PD controller. Pinocchio is used to compute the error between the desired Cartesian pose and the current Cartesian pose. This is done by the log6 function. The p_gains and d_gains variables are the gains of the PD controller.

Similarly to the previous desired Cartesian acceleration computation, we compute the desired acceleration of the elbow along the y axis. Here the reference is either 0.15m, either -0.15m depending of the simulation time.

Generalized Hierarchical Control is based on the projection P of the tasks on the other tasks' nullspace. In order to compute the overall projection P, we need to create a vector of \(\alpha_i\) matrices defined such that:

\(\alpha_i = diag(\alpha_{i1} I_{m_1}, \ldots, \alpha_{ij} I_{m_j}, \ldots, \alpha_{i_{n_t}} I_{m_{n_t}})\)

where \(\alpha_i\) is a diagonal matrix, the main diagonal blocks of which are square matrices: \(\alpha_{ij} I_{m_j}\) . \(I_{m_j}\) is the \(m_j\) x \(m_j\) identity matrix, and \(\alpha_{ij} \in [0, 1]\). By convention, the coefficient \(\alpha_{ij}\) indicates the priority of task j with respect to task i.

  • \(\alpha_{ij} = 0 \) corresponds to the case where task j has strict lower priority with respect to task i.
  • \(0 < \alpha_{ij} < 1\) corresponds to a soft (non-strict) priority between the two tasks: the greater the value of \(\alpha_{ij}\) , the higher the importance level of task j with respect to task i.
  • \(\alpha_{ij} = 1 \) corresponds to the case where task j has a strict higher priority with respect to task i.

There is a particular case induced by the proposed formulation and corresponding to the influence of task i on itself. Even though not intuitive, this self-influence has to be interpreted in terms of task existence, modulated by \(\alpha_{ii}\) . If \(\alpha_{ii} = 1\) then task i is projected into its own null-space, i.e. it is basically canceled out. Decreasing \(\alpha_{ii}\) continuously to 0 is a simple and elegant way to introduce the task in the set of tasks. Conversely, increasing \(\alpha_{ii}\) continuously from 0 to 1 provides with a proper task deletion procedure.

The updateProjector method update the projector with the alphas. Note that alphas values can be modified over time to smoothly change the hierarchy between the tasks.

Finally, we set the E and f for each task, including the regularization task. Unlike the other tasks,the regularization task is weighted. In this example, the weight is added in the E matrix.

Once we updated the necassary tasks and constraints we can update the whole problem. If a solution to the problem exist we can then get it and send it to the simulated robot.

Main function

The main function function fetches the robot name given in argv and starts the Mujoco simulation.

Qontrol::Task::CartesianAcceleration
Implementation of a Cartesian Acceleration task.
Definition: CartesianAcceleration.hpp:34
Qontrol::RobotState
Robot state.
Definition: GenericModel.hpp:35
Qontrol::Constraint::JointTorque
Implemtentation of a joint torque constraint.
Definition: JointTorque.hpp:34
Qontrol::Constraint::JointVelocity
Implemtentation of a joint velocity constraint.
Definition: JointVelocity.hpp:35
Qontrol::Model::RobotModel
Decalaration of a template specialization for the model library.
Definition: GenericModel.hpp:488
Qontrol::Constraint::JointConfiguration
Implemtentation of a joint configuration constraint.
Definition: JointConfiguration.hpp:35