Torque Control GHC Interactive#
Introduction#
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:
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.