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