Torque Control GHC Interactive#

Introduction#

Torque GHC Interactive Example

In this interactive example, the robot end-effector tracks a mocap target while the elbow oscillates along the Y-axis. The Generalized Hierarchical Control (GHC) strategy ensures that the secondary elbow task is projected into the null-space of the main end-effector task, allowing the elbow to move without disturbing the tracking accuracy of the robot tip.

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{split}\begin{aligned} 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{aligned}\end{split}\]

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.

Full Code:

You can find the source code of this example here.

  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
 19using namespace Qontrol;
 20
 21class MujocoQontrol : public MujocoSim 
 22{
 23public:
 24//------------------------------------------- simulation -------------------------------------------
 25std::shared_ptr<Model::RobotModel> model;
 26std::shared_ptr<JointTorqueProblem> torque_problem;
 27std::shared_ptr<Task::CartesianAcceleration<ControlOutput::JointTorque>> main_task;
 28std::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;
 31std::shared_ptr<Qontrol::Task::GenericTask> regularization_task;
 32
 33Qontrol::RobotState robot_state;
 34TrajectoryGeneration* traj;
 35std::string resource_path;
 36bool initialized = false;
 37double t = 0;
 38
 39void initController() override
 40{
 41  model =
 42      Model::RobotModel::loadModelFromFile(resource_path+"robot.urdf");
 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
 75void 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 -2*std::sqrt(500.0)*model->getFrameVelocity("panda_link4").matrix()(1,3);
117
118  Eigen::VectorXd alpha_1 = Eigen::VectorXd::Zero(7); //here absolute priority to main task
119  Eigen::VectorXd alpha_2 = Eigen::VectorXd::Ones(7); //activation of the elbow task but should not impac tthe main task
120  alpha_2[6] = 0.0;
121  std::vector<Eigen::VectorXd> 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
146int 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.

std::shared_ptr<Model::RobotModel> model;

We use pinocchio for our model library.

std::shared_ptr<JointTorqueProblem> torque_problem;

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

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.

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

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

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

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

Initialization

void initController() override
{
  model =
      Model::RobotModel::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.

  torque_problem = std::make_shared<Qontrol::JointTorqueProblem>(model,ResolutionStrategy::Generalized);
  //tasks  (weights are useless)
  main_task = torque_problem->task_set->add<Task::CartesianAcceleration>("MainTask"); 
  elbow_task = torque_problem->task_set->add<Task::CartesianAcceleration>("CommunicationTask");
  Eigen::MatrixXd selection_matrix = Eigen::MatrixXd::Zero(6,6);
  selection_matrix(1,1) = 1.0; //y axis control only
  elbow_task->setControledFrameName("panda_link4");
  elbow_task->setReferenceFrame(ReferenceFrame::LOCAL_WORLD_ALIGNED);
  elbow_task->setSelectionMatrix(selection_matrix);
  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.

  auto joint_configuration_constraint = torque_problem->constraint_set->add<Constraint::JointConfiguration>("JointConfigurationConstraint");
  auto joint_velocity_constraint = torque_problem->constraint_set->add<Constraint::JointVelocity>("JointVelocityConstraint");
  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.

  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

void updateController() override
{
  
  while (!initialized)
  {
    // init_pose = model->getFramePose(model->getTipFrameName());
    std::cout << "Waiting for initialization" << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
  }

  for (int i=0; i<model->getNrOfDegreesOfFreedom() ; ++i)
  {
    robot_state.joint_position[i] = d->qpos[i];
    robot_state.joint_velocity[i] = d->qvel[i];
  }
  model->setRobotState(robot_state);

  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.

  pinocchio::SE3 traj_pose(traj->pose.matrix());

  //get desired cartesian acceleration for main task
  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.

  const pinocchio::SE3 tipMdes = current_pose.actInv(traj_pose);
  auto err = pinocchio::log6(tipMdes).toVector();

  Eigen::Matrix<double, 6, 1> p_gains;
  p_gains << 1000, 1000, 1000, 1000, 1000, 1000;

  Eigen::Matrix<double, 6, 1> d_gains = 2.0 * p_gains.cwiseSqrt();
  Eigen::Matrix<double, 6, 1> xdd_star =
      p_gains.cwiseProduct(err) -
      d_gains.cwiseProduct(model->getFrameVelocity(model->getTipFrameName())) ;
  
  double y_des_elbow =0.15;
  t+=m->opt.timestep;
  if(sin(0.5*t)<0){
    y_des_elbow = -0.15;
  }
  double current_y_elbow_pose = model->getFramePose("panda_link4").matrix()(1,3);
  double error_elbow_y =  y_des_elbow-current_y_elbow_pose;
  Eigen::VectorXd xdd_star_elbow = Eigen::VectorXd::Zero(6);
  xdd_star_elbow[1]= 500.0 * error_elbow_y -2*std::sqrt(500.0)*model->getFrameVelocity("panda_link4").matrix()(1,3);

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.

  Eigen::VectorXd alpha_1 = Eigen::VectorXd::Zero(7); //here absolute priority to main task
  Eigen::VectorXd alpha_2 = Eigen::VectorXd::Ones(7); //activation of the elbow task but should not impac tthe main task
  alpha_2[6] = 0.0;
  std::vector<Eigen::VectorXd> alphas;
  alphas.push_back(alpha_1);
  alphas.push_back(alpha_2);

  //update the projectors of GHC
  torque_problem->updateProjector(alphas);

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.

  main_task->setTargetAcceleration(xdd_star);
  elbow_task->setTargetAcceleration(xdd_star_elbow);

  regularization_task->setE(0.00001*Eigen::MatrixXd::Identity(14,14)); //here 0.00001 is the weight of regularization task
  regularization_task->setf(Eigen::VectorXd::Zero(14));

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.

  torque_problem->update(m->opt.timestep);

  if (torque_problem->solutionFound())
  {
    sendJointTorque(torque_problem->getJointTorqueCommand());
  }
} 
};

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

int main(int argc, const char** argv) {
  MujocoQontrol mujoco_qontrol;
  Qontrol::Log::Logger::parseArgv(argc, argv);

  mjvCamera cam;
  mjv_defaultCamera(&cam);

  mjvOption opt;
  mjv_defaultOption(&opt);

  mjvPerturb pert;
  mjv_defaultPerturb(&pert);

  // simulate object encapsulates the UI
  auto sim = std::make_unique<mj::Simulate>(
      std::make_unique<mj::GlfwAdapter>(),
      &cam, &opt, &pert, /* is_passive = */ false
  );

  std::string robot = argv[1];
  std::string mujoco_scene = "./resources/"+robot+"/scene.xml";
  mujoco_qontrol.resource_path = "./resources/"+robot+"/";

  // start physics thread
  std::thread physicsthreadhandle( &MujocoQontrol::PhysicsThread, mujoco_qontrol, sim.get(), mujoco_scene.c_str());

  // start simulation UI loop (blocking call)
  sim->RenderLoop();
  physicsthreadhandle.join();

  return 0;

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

Full Code:

You can find the source code of this example here.

  1#!/usr/bin/env python3
  2"""
  3Interactive Torque Control with GHC (Generalized Hierarchical Control)
  4
  5This example demonstrates:
  6- Hierarchical task control using GHC projectors
  7- Multiple tasks with different priorities
  8- Main task: End-effector Cartesian tracking (highest priority)
  9- Secondary task: Elbow Y-position oscillation (projected in main task null space)
 10- Real-time torque-level inverse dynamics
 11- Interactive mocap target control
 12
 13Mirrors the C++ example: torqueQontrol_GHC.cpp (adapted for interactive mode)
 14
 15Usage:
 16    python torque_control_ghc_interactive.py <robot_name>
 17    
 18Example:
 19    python torque_control_ghc_interactive.py panda
 20    python torque_control_ghc_interactive.py panda --verbose
 21    
 22Controls:
 23- Use the MuJoCo viewer to move the mocap body (red sphere)
 24- The robot end-effector will track the mocap target
 25- The elbow oscillates in Y-direction in the null space of the main task
 26- Press ESC to exit
 27"""
 28
 29import numpy as np
 30import qontrol
 31import pinocchio as pin
 32import argparse
 33import os
 34import time
 35from mujoco_helper import MujocoSimulator, compute_log6_error
 36
 37
 38class QontrolTorqueGHCController:
 39    """GHC Torque controller using Qontrol (MuJoCo-agnostic)"""
 40    
 41    def __init__(self, robot_name: str, resource_path: str, mujoco_sim: MujocoSimulator):
 42        """
 43        Initialize the Qontrol GHC torque controller
 44        
 45        Args:
 46            robot_name: Name of the robot (e.g., 'panda')
 47            resource_path: Path to robot resources folder
 48            mujoco_sim: MuJoCo simulator instance
 49        """
 50        self.robot_name = robot_name
 51        self.resource_path = resource_path
 52        self.sim = mujoco_sim
 53        
 54        # =================================================================
 55        # Qontrol Setup - Robot Model and QP Problem
 56        # =================================================================
 57        urdf_path = os.path.join(resource_path, "robot.urdf")
 58        if not os.path.exists(urdf_path):
 59            raise FileNotFoundError(f"URDF file not found: {urdf_path}")
 60        
 61        print(f"Loading Qontrol model from: {urdf_path}")
 62        self.qontrol_model = qontrol.RobotModel.load_from_file(urdf_path)
 63        self.ndof = self.qontrol_model.get_nr_of_degrees_of_freedom()
 64        print(f"Robot has {self.ndof} degrees of freedom")
 65        
 66        # Create QP solver
 67        self.solver = qontrol.create_qpmad_solver()
 68        
 69        # Create torque-level problem with Generalized resolution strategy (for GHC)
 70        print("Creating GHC torque problem with Generalized resolution strategy")
 71        self.torque_problem = qontrol.JointTorqueProblem(
 72            self.qontrol_model, 
 73            self.solver,
 74            qontrol.ResolutionStrategy.Generalized
 75        )
 76        
 77        # =================================================================
 78        # Task Setup with Hierarchical Control
 79        # =================================================================
 80        
 81        # Task 1: Main task - Cartesian acceleration for end-effector tracking
 82        # Weight is not used with GHC (priorities defined by alpha matrices)
 83        self.main_task = self.torque_problem.task_set.add_cartesian_acceleration("MainTask", 1.0)
 84        
 85        # Task 2: Elbow task - Control elbow position in Y direction only
 86        self.elbow_task = self.torque_problem.task_set.add_cartesian_acceleration("ElbowTask", 1.0)
 87        
 88        # Configure elbow task to control only Y axis in LOCAL_WORLD_ALIGNED frame
 89        selection_matrix = np.zeros((6, 6))
 90        selection_matrix[1, 1] = 1.0  # Y axis control only
 91        self.elbow_task.set_selection_matrix(selection_matrix)
 92        self.elbow_task.set_controlled_frame_name("panda_link4")  # Elbow link
 93        self.elbow_task.set_reference_frame(qontrol.ReferenceFrame.LOCAL_WORLD_ALIGNED)
 94        print("Elbow task configured: panda_link4, Y-axis control only")
 95        
 96        # Task 3: Regularization task - For GHC mode, dimension = ndof * n_tasks (excluding regularization)
 97        # n_tasks = 2 (main + elbow), so regularization_dim = 7 * 2 = 14
 98        regularization_dim = self.ndof * 2
 99        self.regularization_task = self.torque_problem.task_set.add(
100            "RegularizationTask", 
101            regularization_dim, 
102            regularization_dim,
103            1.0  # Weight set during update
104        )
105        print(f"Regularization task dimension: {regularization_dim}")
106        
107        # =================================================================
108        # Constraint Setup
109        # =================================================================
110        self.joint_config_constraint = self.torque_problem.constraint_set.add_joint_configuration("JointConfigurationConstraint")
111        self.joint_vel_constraint = self.torque_problem.constraint_set.add_joint_velocity("JointVelocityConstraint")
112        self.joint_torque_constraint = self.torque_problem.constraint_set.add_joint_torque("JointTorqueConstraint")
113        
114        # Set prediction horizons for constraints (like C++ example)
115        self.joint_config_constraint.set_horizon(30)
116        self.joint_vel_constraint.set_horizon(30)
117        print("Constraint horizons set to 30")
118        
119        # Initialize problem (resizes tasks/constraints for GHC mode)
120        print("Initializing GHC problem...")
121        self.torque_problem.init()
122        print("GHC problem initialized successfully")
123        
124        # =================================================================
125        # Robot State
126        # =================================================================
127        self.robot_state = qontrol.RobotState()
128        self.robot_state.resize(self.ndof)
129        
130        # Get initial joint state and update model
131        qpos, qvel = self.sim.get_joint_state()
132        self.robot_state.joint_position = qpos
133        self.robot_state.joint_velocity = qvel
134        self.qontrol_model.set_robot_state(self.robot_state)
135        
136        # Get tip frame name
137        self.tip_frame = self.qontrol_model.get_tip_frame_name()
138        print(f"Controlling frame: {self.tip_frame}")
139        
140        # Initialize mocap to current end-effector pose
141        self.sim.init_mocap_to_frame(self.tip_frame)
142        
143        print("dt:", self.sim.dt)
144       
145        # =================================================================
146        # Control Parameters
147        # =================================================================
148        # PD gains for main Cartesian task
149        self.main_p_gains = np.array([1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0])
150        self.main_d_gains = 2.0 * np.sqrt(self.main_p_gains)  # Critical damping
151        
152        # PD gains for elbow task (oscillation)
153        self.elbow_p_gain = 1000.0
154        self.elbow_d_gain = 2.0 * np.sqrt(self.elbow_p_gain)
155        
156        # Time counter for elbow oscillation
157        self.t = 0.0
158    
159    def update(self):
160        """Main control update: compute and apply joint torques"""
161        # Update time
162        self.t += self.sim.dt
163        
164        # 1. Update Qontrol model with current state from simulation
165        qpos, qvel = self.sim.get_joint_state()
166        self.robot_state.joint_position = qpos
167        self.robot_state.joint_velocity = qvel
168        self.qontrol_model.set_robot_state(self.robot_state)
169        
170        # 2. Get target and current poses for main task
171        target_se3 = self.sim.get_mocap_pose_se3()
172        current_ee_matrix = self.qontrol_model.get_frame_pose(self.tip_frame)
173        current_se3 = pin.SE3(current_ee_matrix[:3, :3], current_ee_matrix[:3, 3])
174        
175        # 3. Compute SE(3) log6 error for main task
176        cartesian_error = compute_log6_error(target_se3, current_se3)
177        error_norm = np.linalg.norm(cartesian_error)
178        
179        # 4. Get current end-effector velocity
180        ee_velocity = self.qontrol_model.get_frame_velocity(self.tip_frame)
181        
182        # 5. Compute desired Cartesian acceleration for main task (PD control)
183        xdd_star = self.main_p_gains * cartesian_error - self.main_d_gains * ee_velocity
184        
185        # Set main task target
186        self.main_task.set_target_acceleration(xdd_star)
187        
188        # 6. Compute elbow task (Y-axis oscillation)
189        # Desired Y position oscillates: y_des = 0.15 * sign(sin(0.5*t))
190        if np.sin(0.5 * self.t) < 0:
191            y_des_elbow = -0.15
192        else:
193            y_des_elbow = 0.15
194        
195        # Get current elbow pose and velocity
196        elbow_pose = self.qontrol_model.get_frame_pose("panda_link4")
197        current_y_elbow = elbow_pose[1, 3]  # Y position
198        
199        elbow_velocity = self.qontrol_model.get_frame_velocity("panda_link4")
200        
201        # Compute error
202        error_elbow_y = y_des_elbow - current_y_elbow
203        
204        # Compute desired acceleration (PD control, only Y axis)
205        xdd_star_elbow = np.zeros(6)
206        xdd_star_elbow[1] = (self.elbow_p_gain * error_elbow_y - 
207                             self.elbow_d_gain * elbow_velocity[1])
208        
209        # Set elbow task target
210        self.elbow_task.set_target_acceleration(xdd_star_elbow)
211        
212        # 7. Update GHC projectors with alpha matrices
213        # Alpha for main task: absolute priority (all zeros)
214        alpha_1 = np.zeros(self.ndof)
215        
216        # Alpha for elbow task: activated but should not impact main task
217        alpha_2 = np.ones(self.ndof)
218        alpha_2[6] = 0.0  # Last joint (gripper) not involved
219        
220        alphas = [alpha_1, alpha_2]
221        self.torque_problem.update_projector(alphas)
222        
223        # 8. Set regularization task
224        # Weight of regularization is 0.00001 (very small)
225        reg_weight = 0.00001
226        self.regularization_task.set_E(reg_weight * np.eye(self.ndof * 2))
227        self.regularization_task.set_f(np.zeros(self.ndof * 2))
228        
229        # 9. Solve QP for joint torques
230        t_qp_start = time.perf_counter()
231        self.torque_problem.update(self.sim.dt)
232        t_qp = time.perf_counter() - t_qp_start
233        
234        # 10. Apply torques to simulation
235        if self.torque_problem.solution_found():
236            joint_torques = self.torque_problem.get_joint_torque_command()
237            self.sim.apply_torques(joint_torques)
238        else:
239            print("Warning: No QP solution found!")
240            self.sim.apply_torques(np.zeros(self.ndof))
241
242
243def main():
244    """Main entry point"""
245    parser = argparse.ArgumentParser(
246        description="Interactive GHC torque control with MuJoCo and Qontrol",
247        formatter_class=argparse.RawDescriptionHelpFormatter,
248        epilog="""
249Examples:
250  %(prog)s panda
251  %(prog)s ur5
252        """
253    )
254    parser.add_argument("robot", type=str, help="Robot name (e.g., panda)")
255    
256    args = parser.parse_args()
257    
258    # Get resource path
259    script_dir = os.path.dirname(os.path.abspath(__file__))
260    resource_path = os.path.join(script_dir, "..", "..", "..", "examples", "resources", args.robot)
261    
262    if not os.path.exists(resource_path):
263        print(f"Error: Resource path not found: {resource_path}")
264        return 1
265    
266    # MuJoCo scene path
267    scene_path = os.path.join(resource_path, "scene_interactive.xml")
268    if not os.path.exists(scene_path):
269        print(f"Error: Scene file not found: {scene_path}")
270        return 1
271    
272    print(f"Loading MuJoCo scene from: {scene_path}")
273    
274    # Initialize MuJoCo simulator
275    sim = MujocoSimulator(args.robot, resource_path)
276    
277    # Initialize controller
278    controller = QontrolTorqueGHCController(args.robot, resource_path, sim)
279    
280    print("\n" + "="*60)
281    print("Starting GHC torque control simulation")
282    print("Main task: End-effector tracking (highest priority)")
283    print("Secondary task: Elbow Y-oscillation (null space)")
284    print("Move the red mocap target to control the robot")
285    print("Press ESC to exit")
286    print("="*60 + "\n")
287    
288    try:
289        # Run simulation with controller
290        sim.run_interactive(controller.update)
291    except KeyboardInterrupt:
292        print("\nInterrupted by user")
293    except Exception as e:
294        print(f"\nError during simulation: {e}")
295        import traceback
296        traceback.print_exc()
297        return 1
298    
299    return 0
300
301
302if __name__ == "__main__":
303    exit(main())

—

Explanation of the code

Note: The following explanation refers to the logic implemented in C++, but the Python API directly mirrors this structure.

Declaration

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

We use pinocchio for our model library.

        self.qontrol_model = qontrol.RobotModel.load_from_file(urdf_path)
        self.ndof = self.qontrol_model.get_nr_of_degrees_of_freedom()

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

        self.torque_problem = qontrol.JointTorqueProblem(
            self.qontrol_model, 
            self.solver,
            qontrol.ResolutionStrategy.Generalized

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

The main task is expressed as a Cartesian Acceleration task.

        self.main_task = self.torque_problem.task_set.add_cartesian_acceleration("MainTask", 1.0)

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

        self.elbow_task = self.torque_problem.task_set.add_cartesian_acceleration("ElbowTask", 1.0)

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

        self.regularization_task = self.torque_problem.task_set.add(
            "RegularizationTask", 
            regularization_dim, 
            regularization_dim,
            1.0  # Weight set during update

Initialization

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.

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.

        selection_matrix = np.zeros((6, 6))
        selection_matrix[1, 1] = 1.0  # Y axis control only
        self.elbow_task.set_selection_matrix(selection_matrix)
        self.elbow_task.set_controlled_frame_name("panda_link4")  # Elbow link
        self.elbow_task.set_reference_frame(qontrol.ReferenceFrame.LOCAL_WORLD_ALIGNED)

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.

        self.joint_config_constraint = self.torque_problem.constraint_set.add_joint_configuration("JointConfigurationConstraint")
        self.joint_vel_constraint = self.torque_problem.constraint_set.add_joint_velocity("JointVelocityConstraint")
        self.joint_torque_constraint = self.torque_problem.constraint_set.add_joint_torque("JointTorqueConstraint")
        
        # Set prediction horizons for constraints (like C++ example)
        self.joint_config_constraint.set_horizon(30)
        self.joint_vel_constraint.set_horizon(30)

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

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.

    def update(self):
        """Main control update: compute and apply joint torques"""
        # Update time
        self.t += self.sim.dt
        
        # 1. Update Qontrol model with current state from simulation
        qpos, qvel = self.sim.get_joint_state()
        self.robot_state.joint_position = qpos
        self.robot_state.joint_velocity = qvel
        self.qontrol_model.set_robot_state(self.robot_state)

We also define the Cartesian goal for the end effector.

        target_se3 = self.sim.get_mocap_pose_se3()
        current_ee_matrix = self.qontrol_model.get_frame_pose(self.tip_frame)
        current_se3 = pin.SE3(current_ee_matrix[:3, :3], current_ee_matrix[:3, 3])

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.

        cartesian_error = compute_log6_error(target_se3, current_se3)
        error_norm = np.linalg.norm(cartesian_error)
        
        # 4. Get current end-effector velocity
        ee_velocity = self.qontrol_model.get_frame_velocity(self.tip_frame)
        
        # 5. Compute desired Cartesian acceleration for main task (PD control)
        xdd_star = self.main_p_gains * cartesian_error - self.main_d_gains * ee_velocity
        
        # Set main task target
        self.main_task.set_target_acceleration(xdd_star)

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.

        if np.sin(0.5 * self.t) < 0:
            y_des_elbow = -0.15
        else:
            y_des_elbow = 0.15
        
        # Get current elbow pose and velocity
        elbow_pose = self.qontrol_model.get_frame_pose("panda_link4")
        current_y_elbow = elbow_pose[1, 3]  # Y position
        
        elbow_velocity = self.qontrol_model.get_frame_velocity("panda_link4")
        
        # Compute error
        error_elbow_y = y_des_elbow - current_y_elbow
        
        # Compute desired acceleration (PD control, only Y axis)
        xdd_star_elbow = np.zeros(6)
        xdd_star_elbow[1] = (self.elbow_p_gain * error_elbow_y - 
                             self.elbow_d_gain * elbow_velocity[1])
        
        # Set elbow task target
        self.elbow_task.set_target_acceleration(xdd_star_elbow)

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.
        alpha_1 = np.zeros(self.ndof)
        
        # Alpha for elbow task: activated but should not impact main task
        alpha_2 = np.ones(self.ndof)
        alpha_2[6] = 0.0  # Last joint (gripper) not involved
        
        alphas = [alpha_1, alpha_2]
        self.torque_problem.update_projector(alphas)

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.

        reg_weight = 0.00001
        self.regularization_task.set_E(reg_weight * np.eye(self.ndof * 2))
        self.regularization_task.set_f(np.zeros(self.ndof * 2))

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.

        self.torque_problem.update(self.sim.dt)
        t_qp = time.perf_counter() - t_qp_start
        
        # 10. Apply torques to simulation
        if self.torque_problem.solution_found():
            joint_torques = self.torque_problem.get_joint_torque_command()
            self.sim.apply_torques(joint_torques)
        else:
            print("Warning: No QP solution found!")
            self.sim.apply_torques(np.zeros(self.ndof))

Main function

def main():
    """Main entry point"""
    parser = argparse.ArgumentParser(
        description="Interactive GHC torque control with MuJoCo and Qontrol",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  %(prog)s panda
  %(prog)s ur5
        """
    )
    parser.add_argument("robot", type=str, help="Robot name (e.g., panda)")
    
    args = parser.parse_args()
    
    # Get resource path
    script_dir = os.path.dirname(os.path.abspath(__file__))
    resource_path = os.path.join(script_dir, "..", "..", "..", "examples", "resources", args.robot)
    
    if not os.path.exists(resource_path):
        print(f"Error: Resource path not found: {resource_path}")
        return 1
    
    # MuJoCo scene path
    scene_path = os.path.join(resource_path, "scene_interactive.xml")
    if not os.path.exists(scene_path):
        print(f"Error: Scene file not found: {scene_path}")
        return 1
    
    print(f"Loading MuJoCo scene from: {scene_path}")
    
    # Initialize MuJoCo simulator
    sim = MujocoSimulator(args.robot, resource_path)
    
    # Initialize controller
    controller = QontrolTorqueGHCController(args.robot, resource_path, sim)
    
    print("\n" + "="*60)
    print("Starting GHC torque control simulation")
    print("Main task: End-effector tracking (highest priority)")
    print("Secondary task: Elbow Y-oscillation (null space)")
    print("Move the red mocap target to control the robot")
    print("Press ESC to exit")
    print("="*60 + "\n")
    
    try:
        # Run simulation with controller
        sim.run_interactive(controller.update)
    except KeyboardInterrupt:
        print("\nInterrupted by user")
    except Exception as e:
        print(f"\nError during simulation: {e}")
        import traceback
        traceback.print_exc()
        return 1
    
    return 0

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