Velocity Control GHC Interactive#

Introduction#

Velocity 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 &= [\dot{q}_1^T, \ldots, \dot{q}_{n_t}^T] \\ X^{opt} &= \underset{X}{\mathrm{argmin}} \sum_{i=1}^{n_t} \left\| J_i S_{q_i} X - v_i^* \right\|^2 + \left\| X \right\|^2 \\ &\textrm{s.t.} \\ &\quad \frac{q_{min} - q_k}{\Delta T} \leq PX \leq \frac{q_{max} - q_k}{\Delta T}\\ &\quad \dot{q}_{min} \leq PX \leq \dot{q}_{max} \\ P &= [P_1, \ldots, P_{n_t}], \quad \dot{q} = PX = \sum_i P_i \dot{q}_i \\ \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
 19
 20using namespace Qontrol;
 21
 22class MujocoQontrol : public MujocoSim 
 23{
 24public:
 25//------------------------------------------- simulation -------------------------------------------
 26std::shared_ptr<Qontrol::Model::RobotModel> model;
 27std::shared_ptr<Qontrol::JointVelocityProblem> velocity_problem;
 28std::shared_ptr<Qontrol::Task::CartesianVelocity<Qontrol::ControlOutput::JointVelocity>> main_task;
 29std::shared_ptr<Qontrol::Task::CartesianVelocity<Qontrol::ControlOutput::JointVelocity>> elbow_task;
 30std::shared_ptr<Qontrol::Task::GenericTask> regularization_task;
 31
 32
 33Qontrol::RobotState robot_state;
 34
 35TrajectoryGeneration* traj;
 36std::string resource_path;
 37bool initialized = false;
 38double t = 0;
 39
 40void initController( ) override
 41{
 42  model =
 43      Model::RobotModel::loadModelFromFile(resource_path+"robot.urdf");
 44  
 45  const int ndof = model->getNrOfDegreesOfFreedom();
 46
 47  velocity_problem = std::make_shared<Qontrol::JointVelocityProblem>(model,ResolutionStrategy::Generalized);  
 48  main_task = velocity_problem->task_set->add<Task::CartesianVelocity>("MainTask"); 
 49  elbow_task = velocity_problem->task_set->add<Task::CartesianVelocity>("CommunicationTask");
 50  Eigen::MatrixXd selection_matrix = Eigen::MatrixXd::Zero(6,6);
 51  selection_matrix(1,1) = 1.0; //y axis control only
 52  elbow_task->setControledFrameName("panda_link4");
 53  elbow_task->setReferenceFrame(ReferenceFrame::LOCAL_WORLD_ALIGNED);
 54  elbow_task->setSelectionMatrix(selection_matrix);
 55  regularization_task = velocity_problem->task_set->add("RegularisationTask",14,14,1e-5); 
 56
 57  auto joint_configuration_constraint = velocity_problem->constraint_set->add<Constraint::JointConfiguration>("JointConfigurationConstraint");
 58  auto joint_velocity_constraint = velocity_problem->constraint_set->add<Constraint::JointVelocity>("JointVelocityConstraint");
 59
 60  velocity_problem->init();
 61  
 62  mju_copy(d->qpos, m->key_qpos, m->nu);
 63  robot_state.joint_position.resize(ndof);
 64  robot_state.joint_velocity.resize(ndof);
 65
 66  traj = new TrajectoryGeneration(resource_path + "trajectory.csv",
 67                                  m->opt.timestep);
 68  initialized = true;
 69}
 70
 71void updateController() override
 72{
 73   while (!initialized)
 74  {
 75    // init_pose = model->getFramePose(model->getTipFrameName());
 76    std::cout << "Waiting for initialization" << std::endl;
 77    std::this_thread::sleep_for(std::chrono::milliseconds(100));
 78  }
 79
 80  const int ndof = model->getNrOfDegreesOfFreedom();
 81
 82  for (int i=0; i<ndof ; ++i)
 83  {
 84    robot_state.joint_position[i] = d->qpos[i];
 85    robot_state.joint_velocity[i] = d->qvel[i];
 86  }
 87  model->setRobotState(robot_state);
 88
 89  traj->update();
 90  pinocchio::SE3 traj_pose(traj->pose.matrix());
 91  
 92  pinocchio::SE3 current_pose(model->getFramePose(model->getTipFrameName()).matrix());
 93  const pinocchio::SE3 tipMdes = current_pose.actInv(traj_pose);
 94  auto err = pinocchio::log6(tipMdes).toVector();
 95  Eigen::Matrix<double,6,1> p_gains;
 96  p_gains << 10,10,10,10,10,10;
 97  Eigen::Matrix<double,6,1> xd_star = p_gains.cwiseProduct(err);
 98
 99  double y_des_elbow =0.15;
100  t+=m->opt.timestep;
101  if(sin(0.5*t)<0){
102    y_des_elbow = -0.15;
103  }
104  double current_y_elbow_pose = model->getFramePose("panda_link4").matrix()(1,3);
105  double error_elbow_y =  y_des_elbow-current_y_elbow_pose;
106  Eigen::VectorXd xd_star_elbow = Eigen::VectorXd::Zero(6);
107  xd_star_elbow[1]= 10.0 * error_elbow_y - 10.0*model->getFrameVelocity("panda_link4").matrix()(1,3);
108
109  Eigen::VectorXd alpha_1 = Eigen::VectorXd::Zero(7); //here absolute priority to main task
110  Eigen::VectorXd alpha_2 = Eigen::VectorXd::Ones(7); //activation of the elbow task but should not impact the main task
111  alpha_2[6] = 0.0;
112  std::vector<Eigen::VectorXd> alphas;
113  alphas.push_back(alpha_1);
114  alphas.push_back(alpha_2);
115
116  //update the projectors of GHC
117  velocity_problem->updateProjector(alphas);
118  
119  //TASKS UPDATE
120
121  main_task->setTargetVelocity(xd_star);
122  elbow_task->setTargetVelocity(xd_star_elbow);
123  regularization_task->setE(0.00001*Eigen::MatrixXd::Identity(14,14)); //here 0.00001 is the weight of regularization task
124  regularization_task->setf(Eigen::VectorXd::Zero(14));
125  
126  velocity_problem->update(m->opt.timestep);
127
128  if (velocity_problem->solutionFound())
129  {
130    sendJointVelocity(velocity_problem->getJointVelocityCommand());    
131  }
132}
133 
134};
135
136int main(int argc, const char** argv) {
137  MujocoQontrol mujoco_qontrol;
138  Qontrol::Log::Logger::parseArgv(argc, argv);
139
140  mjvCamera cam;
141  mjv_defaultCamera(&cam);
142
143  mjvOption opt;
144  mjv_defaultOption(&opt);
145
146  mjvPerturb pert;
147  mjv_defaultPerturb(&pert);
148
149  // simulate object encapsulates the UI
150  auto sim = std::make_unique<mj::Simulate>(
151      std::make_unique<mj::GlfwAdapter>(),
152      &cam, &opt, &pert, /* is_passive = */ false
153  );
154
155  std::string robot = argv[1];
156  std::string mujoco_scene = "./resources/"+robot+"/scene.xml";
157  mujoco_qontrol.resource_path = "./resources/"+robot+"/";
158
159  // start physics thread
160  std::thread physicsthreadhandle( &MujocoQontrol::PhysicsThread, mujoco_qontrol, sim.get(), mujoco_scene.c_str());
161
162  // start simulation UI loop (blocking call)
163  sim->RenderLoop();
164  physicsthreadhandle.join();
165
166  return 0;
167}

—

Explanation of the code

Declaration

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

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

We use pinocchio for our model library.

std::shared_ptr<Qontrol::JointVelocityProblem> velocity_problem;

The output of our qp controller is at the joint velocity level.

std::shared_ptr<Qontrol::Task::CartesianVelocity<Qontrol::ControlOutput::JointVelocity>> main_task;

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

The main task is expressed as a Cartesian Velocity task.

std::shared_ptr<Qontrol::Task::CartesianVelocity<Qontrol::ControlOutput::JointVelocity>> 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.

  velocity_problem = std::make_shared<Qontrol::JointVelocityProblem>(model,ResolutionStrategy::Generalized);  
  main_task = velocity_problem->task_set->add<Task::CartesianVelocity>("MainTask"); 
  elbow_task = velocity_problem->task_set->add<Task::CartesianVelocity>("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 = velocity_problem->task_set->add("RegularisationTask",14,14,1e-5); 

We then fill the task set of velocity_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 = velocity_problem->constraint_set->add<Constraint::JointConfiguration>("JointConfigurationConstraint");
  auto joint_velocity_constraint = velocity_problem->constraint_set->add<Constraint::JointVelocity>("JointVelocityConstraint");

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

  velocity_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));
  }

  const int ndof = model->getNrOfDegreesOfFreedom();

  for (int i=0; i<ndof ; ++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.

  traj->update();
  pinocchio::SE3 traj_pose(traj->pose.matrix());
  
  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 << 10,10,10,10,10,10;
  Eigen::Matrix<double,6,1> xd_star = p_gains.cwiseProduct(err);

  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 xd_star_elbow = Eigen::VectorXd::Zero(6);
  xd_star_elbow[1]= 10.0 * error_elbow_y - 10.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.

  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 xd_star_elbow = Eigen::VectorXd::Zero(6);
  xd_star_elbow[1]= 10.0 * error_elbow_y - 10.0*model->getFrameVelocity("panda_link4").matrix()(1,3);

  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 impact the 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
  velocity_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->setTargetVelocity(xd_star);
  elbow_task->setTargetVelocity(xd_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.

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

  if (velocity_problem->solutionFound())
  {
    sendJointVelocity(velocity_problem->getJointVelocityCommand());    

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 Velocity Control with MuJoCo and Qontrol (Clean Version)
  4
  5This example demonstrates:
  6- Loading a robot model in both MuJoCo and Qontrol
  7- Real-time velocity-level inverse kinematics
  8- Interactive mocap target control
  9- Cartesian velocity tracking with joint velocity commands
 10
 11This version uses the MujocoSimulator helper class to reduce boilerplate.
 12
 13Usage:
 14    python velocity_control_interactive_clean.py <robot_name>
 15    
 16Example:
 17    python velocity_control_interactive_clean.py panda
 18    
 19Controls:
 20- Use the MuJoCo viewer to move the mocap body (red sphere)
 21- The robot end-effector will track the mocap target
 22- Press ESC to exit
 23"""
 24
 25import numpy as np
 26import qontrol
 27import pinocchio as pin
 28import argparse
 29import os
 30import time
 31import traceback
 32from mujoco_helper import MujocoSimulator, compute_log6_error
 33
 34
 35class QontrolVelocityController:
 36    """Velocity controller using Qontrol (MuJoCo-agnostic)"""
 37    
 38    def __init__(self, robot_name: str, resource_path: str, mujoco_sim: MujocoSimulator):
 39        """
 40        Initialize the Qontrol velocity controller
 41        
 42        Args:
 43            robot_name: Name of the robot (e.g., 'panda')
 44            resource_path: Path to robot resources folder
 45            mujoco_sim: MuJoCo simulator instance
 46        """
 47        self.robot_name = robot_name
 48        self.resource_path = resource_path
 49        self.sim = mujoco_sim
 50        
 51        # =================================================================
 52        # Qontrol Setup - Robot Model and QP Problem
 53        # =================================================================
 54        urdf_path = os.path.join(resource_path, "robot.urdf")
 55        if not os.path.exists(urdf_path):
 56            raise FileNotFoundError(f"URDF file not found: {urdf_path}")
 57        
 58        print(f"Loading Qontrol model from: {urdf_path}")
 59        self.qontrol_model = qontrol.RobotModel.load_from_file(urdf_path)
 60        self.ndof = self.qontrol_model.get_nr_of_degrees_of_freedom()
 61        print(f"Robot has {self.ndof} degrees of freedom")
 62        
 63        # Create QP solver
 64        self.solver = qontrol.create_qpmad_solver()
 65        
 66        # Create velocity-level problem
 67        self.velocity_problem = qontrol.JointVelocityProblem(self.qontrol_model, self.solver)
 68        
 69        # Add Cartesian velocity task for end-effector tracking
 70        self.main_task = self.velocity_problem.task_set.add_cartesian_velocity("MainTask", 1.0)
 71        
 72        # Add joint velocity regularization (minimize joint motion)
 73        self.regularization_task = self.velocity_problem.task_set.add_joint_velocity("RegularizationTask", 1e-5)
 74        
 75        # Add constraints
 76        self.joint_config_constraint = self.velocity_problem.constraint_set.add_joint_configuration("JointConfigurationConstraint")
 77        self.joint_vel_constraint = self.velocity_problem.constraint_set.add_joint_velocity("JointVelocityConstraint")
 78        self.joint_config_constraint.set_horizon(15)
 79        # Robot state object
 80        self.robot_state = qontrol.RobotState()
 81        self.robot_state.resize(self.ndof)
 82        
 83        # Get tip frame name
 84        self.tip_frame = self.qontrol_model.get_tip_frame_name()
 85        print(f"Controlling frame: {self.tip_frame}")
 86
 87        # =================================================================
 88        # Control Parameters
 89        # =================================================================
 90        self.p_gains = np.array([10.0, 10.0, 10.0, 10.0, 10.0, 10.0])  # [position, orientation] gains
 91    
 92    def update(self):
 93        """Main control update: compute and apply joint torques"""
 94        # 1. Update Qontrol model with current state from simulation
 95        qpos, qvel = self.sim.get_joint_state()
 96        self.robot_state.joint_position = qpos
 97        self.robot_state.joint_velocity = qvel
 98        self.qontrol_model.set_robot_state(self.robot_state)
 99        
100        # 2. Get target and current poses
101        target_se3 = self.sim.get_mocap_pose_se3()
102        current_se3 = self.sim.get_frame_pose_se3(self.tip_frame)
103        
104        # 3. Compute SE(3) log6 error
105        cartesian_error = compute_log6_error(target_se3, current_se3)
106        
107        # 4. Solve QP for joint velocities
108        desired_velocity = self.p_gains * cartesian_error
109        self.main_task.set_target_velocity(desired_velocity)
110        self.regularization_task.set_target_velocity(np.zeros(self.ndof))
111        
112        start_time = time.perf_counter()
113        self.velocity_problem.update(self.sim.dt)
114        
115        # 5. Apply joint velocities directly to simulation
116        if self.velocity_problem.solution_found():
117            joint_velocities = self.velocity_problem.get_joint_velocity_command()
118            
119            # Directly set joint velocities in simulation
120            self.sim.set_joint_velocities(joint_velocities)
121            
122        else:
123            print("Warning: No QP solution found!")
124            # Fallback: set velocities to zero
125            self.sim.set_joint_velocities(np.zeros(self.ndof))
126
127def main():
128    """Main entry point"""
129    parser = argparse.ArgumentParser(
130        description="Interactive velocity control with MuJoCo and Qontrol (Clean Version)",
131        formatter_class=argparse.RawDescriptionHelpFormatter,
132        epilog="""
133Examples:
134  %(prog)s panda
135  %(prog)s ur5
136  
137The program expects the following files in examples/resources/<robot>/:
138  - scene_interactive.xml (MuJoCo scene with mocap body)
139  - robot.urdf (Robot description for Qontrol)
140"""
141    )
142    
143    parser.add_argument(
144        "robot",
145        type=str,
146        help="Robot name (e.g., 'panda', 'ur5')"
147    )
148    
149    parser.add_argument(
150        "--resources",
151        type=str,
152        default=None,
153        help="Path to resources directory (default: ../examples/resources/<robot>/)"
154    )
155    
156    args = parser.parse_args()
157    
158    # Determine resource path
159    if args.resources:
160        resource_path = args.resources
161    else:
162        # Default: ../../../examples/resources/<robot>/
163        # (from bindings/python/examples to examples/resources)
164        script_dir = os.path.dirname(os.path.abspath(__file__))
165        resource_path = os.path.join(script_dir, "..", "..", "..", "examples", "resources", args.robot)
166    
167    resource_path = os.path.abspath(resource_path)
168    
169    if not os.path.exists(resource_path):
170        print(f"Error: Resource path not found: {resource_path}")
171        print(f"\nExpected directory structure:")
172        print(f"  {resource_path}/")
173        print(f"    ├── scene_interactive.xml")
174        print(f"    └── robot.urdf")
175        return 1
176    
177    try:
178        # Create MuJoCo simulator
179        sim = MujocoSimulator(args.robot, resource_path)
180        
181        # Create Qontrol controller
182        controller = QontrolVelocityController(args.robot, resource_path, sim)
183        
184        # Initialize mocap to robot's end-effector pose
185        sim.init_mocap_to_frame(controller.tip_frame)
186        
187        # Display initial mocap pose
188        mocap_pose = sim.get_mocap_pose_se3()
189        print(f"\nInitial mocap pose (at {controller.tip_frame}):")
190        print(f"  Position: {mocap_pose.translation}")
191        quat = pin.Quaternion(mocap_pose.rotation)
192        print(f"  Orientation (quaternion xyzw): [{quat.x:.6f}, {quat.y:.6f}, {quat.z:.6f}, {quat.w:.6f}]")
193        print(f"  Orientation (quaternion wxyz): [{quat.w:.6f}, {quat.x:.6f}, {quat.y:.6f}, {quat.z:.6f}]")
194        
195        # Run interactive simulation
196        sim.run_interactive(
197            controller_callback=controller.update,
198            instructions="""Controls:
199  - Drag the red sphere (mocap body) to move the target
200  - The robot will track the mocap target
201  - Press ESC or close window to exit
202  - Double-click to select mocap body"""
203        )
204        
205    except KeyboardInterrupt:
206        print("\nInterrupted by user")
207        return 0
208    except Exception as e:
209        print(f"\nError: {e}")
210        traceback.print_exc()
211        return 1
212    
213    return 0
214
215
216if __name__ == "__main__":
217    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()
        print(f"Robot has {self.ndof} degrees of freedom")
        
        # Create QP solver
        self.solver = qontrol.create_qpmad_solver()
        
        # Create velocity-level problem
        self.velocity_problem = qontrol.JointVelocityProblem(self.qontrol_model, self.solver)

The output of our qp controller is at the joint velocity level.

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

The main task is expressed as a Cartesian Velocity task.

        self.main_task = self.velocity_problem.task_set.add_cartesian_velocity("MainTask", 1.0)

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

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

        self.regularization_task = self.velocity_problem.task_set.add_joint_velocity("RegularizationTask", 1e-5)

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 velocity_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.

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

        self.joint_config_constraint = self.velocity_problem.constraint_set.add_joint_configuration("JointConfigurationConstraint")
        self.joint_vel_constraint = self.velocity_problem.constraint_set.add_joint_velocity("JointVelocityConstraint")

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

Update

    def update(self):
        """Main control update: compute and apply joint torques"""
        # 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)

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.

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.

        desired_velocity = self.p_gains * cartesian_error
        self.main_task.set_target_velocity(desired_velocity)

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.

        self.regularization_task.set_target_velocity(np.zeros(self.ndof))

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.velocity_problem.update(self.sim.dt)

Main function

def main():
    """Main entry point"""
    parser = argparse.ArgumentParser(
        description="Interactive velocity control with MuJoCo and Qontrol (Clean Version)",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  %(prog)s panda
  %(prog)s ur5
  
The program expects the following files in examples/resources/<robot>/:
  - scene_interactive.xml (MuJoCo scene with mocap body)
  - robot.urdf (Robot description for Qontrol)
"""
    )
    
    parser.add_argument(
        "robot",
        type=str,
        help="Robot name (e.g., 'panda', 'ur5')"
    )
    
    parser.add_argument(
        "--resources",
        type=str,
        default=None,
        help="Path to resources directory (default: ../examples/resources/<robot>/)"
    )
    
    args = parser.parse_args()
    
    # Determine resource path
    if args.resources:
        resource_path = args.resources
    else:
        # Default: ../../../examples/resources/<robot>/
        # (from bindings/python/examples to examples/resources)
        script_dir = os.path.dirname(os.path.abspath(__file__))
        resource_path = os.path.join(script_dir, "..", "..", "..", "examples", "resources", args.robot)
    
    resource_path = os.path.abspath(resource_path)
    
    if not os.path.exists(resource_path):
        print(f"Error: Resource path not found: {resource_path}")
        print(f"\nExpected directory structure:")
        print(f"  {resource_path}/")
        print(f"    ├── scene_interactive.xml")
        print(f"    └── robot.urdf")
        return 1
    
    try:
        # Create MuJoCo simulator
        sim = MujocoSimulator(args.robot, resource_path)
        
        # Create Qontrol controller
        controller = QontrolVelocityController(args.robot, resource_path, sim)
        
        # Initialize mocap to robot's end-effector pose
        sim.init_mocap_to_frame(controller.tip_frame)
        
        # Display initial mocap pose
        mocap_pose = sim.get_mocap_pose_se3()
        print(f"\nInitial mocap pose (at {controller.tip_frame}):")
        print(f"  Position: {mocap_pose.translation}")
        quat = pin.Quaternion(mocap_pose.rotation)
        print(f"  Orientation (quaternion xyzw): [{quat.x:.6f}, {quat.y:.6f}, {quat.z:.6f}, {quat.w:.6f}]")
        print(f"  Orientation (quaternion wxyz): [{quat.w:.6f}, {quat.x:.6f}, {quat.y:.6f}, {quat.z:.6f}]")
        
        # Run interactive simulation
        sim.run_interactive(
            controller_callback=controller.update,
            instructions="""Controls:
  - Drag the red sphere (mocap body) to move the target
  - The robot will track the mocap target
  - Press ESC or close window to exit
  - Double-click to select mocap body"""
        )
        
    except KeyboardInterrupt:
        print("\nInterrupted by user")
        return 0
    except Exception as e:
        print(f"\nError: {e}")
        traceback.print_exc()
        return 1
    
    return 0


if __name__ == "__main__":
    exit(main())

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