Torque Qontrol#
# Torque control
The following example solves a qp problem expressed at the torque level such that:
.
The robot main tasks consists in following a simple trajectory defined in Cartesian space. 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::JointTorque<ControlOutput::JointTorque>> regularisation_task;
29
30Qontrol::RobotState robot_state;
31TrajectoryGeneration* traj;
32std::string resource_path;
33
34void initController() override
35{
36 model =
37 Model::RobotModel::loadModelFromFile(resource_path+"robot.urdf");
38
39 const int ndof = model->getNrOfDegreesOfFreedom();
40
41 torque_problem = std::make_shared<Qontrol::JointTorqueProblem>(model);
42 main_task = torque_problem->task_set->add<Task::CartesianAcceleration>("MainTask");
43 regularisation_task = torque_problem->task_set->add<Task::JointTorque>("RegularisationTask",1e-5);
44
45 auto joint_configuration_constraint = torque_problem->constraint_set->add<Constraint::JointConfiguration>("JointConfigurationConstraint");
46 auto joint_velocity_constraint = torque_problem->constraint_set->add<Constraint::JointVelocity>("JointVelocityConstraint");
47 auto joint_torque_constraint = torque_problem->constraint_set->add<Constraint::JointTorque>("JointTorqueConstraint");
48
49 mju_copy(d->qpos, m->key_qpos, m->nu);
50 robot_state.joint_position.resize(ndof);
51 robot_state.joint_velocity.resize(ndof);
52
53 traj = new TrajectoryGeneration(resource_path+"trajectory.csv", m->opt.timestep);
54}
55
56void updateController() override
57{
58 const int ndof = model->getNrOfDegreesOfFreedom();
59
60 for (int i=0; i<ndof ; ++i)
61 {
62 robot_state.joint_position[i] = d->qpos[i];
63 robot_state.joint_velocity[i] = d->qvel[i];
64 }
65 model->setRobotState(robot_state);
66
67 traj->update();
68 pinocchio::SE3 traj_pose(traj->pose.matrix());
69
70 pinocchio::SE3 current_pose(model->getFramePose(model->getTipFrameName()).matrix());
71 const pinocchio::SE3 tipMdes = current_pose.actInv(traj_pose);
72 auto err = pinocchio::log6(tipMdes).toVector();
73
74 Eigen::Matrix<double, 6, 1> p_gains;
75 p_gains << 1000, 1000, 1000, 1000, 1000, 1000;
76
77 Eigen::Matrix<double, 6, 1> d_gains = 2.0 * p_gains.cwiseSqrt();
78 Eigen::Matrix<double, 6, 1> xdd_star =
79 p_gains.cwiseProduct(err) +
80 d_gains.cwiseProduct(traj->velocity - model->getFrameVelocity(model->getTipFrameName())) + traj->acceleration;
81
82 main_task->setTargetAcceleration(xdd_star);
83 regularisation_task->setTargetTorque(model->getJointGravityTorques() -
84 robot_state.joint_velocity);
85 regularisation_task->setWeightingMatrix(
86 model->getInverseJointInertiaMatrix());
87 torque_problem->update(m->opt.timestep);
88
89 if (torque_problem->solutionFound())
90 {
91 sendJointTorque(torque_problem->getJointTorqueCommand());
92 }
93}
94};
95
96
97int main(int argc, const char** argv) {
98 MujocoQontrol mujoco_qontrol;
99 Qontrol::Log::Logger::parseArgv(argc, argv);
100
101 mjvCamera cam;
102 mjv_defaultCamera(&cam);
103
104 mjvOption opt;
105 mjv_defaultOption(&opt);
106
107 mjvPerturb pert;
108 mjv_defaultPerturb(&pert);
109
110 // simulate object encapsulates the UI
111 auto sim = std::make_unique<mj::Simulate>(
112 std::make_unique<mj::GlfwAdapter>(),
113 &cam, &opt, &pert, /* is_passive = */ false
114 );
115
116 std::string robot = argv[1];
117 std::string mujoco_scene = "./resources/"+robot+"/scene.xml";
118 mujoco_qontrol.resource_path = "./resources/"+robot+"/";
119
120 // start physics thread
121 std::thread physicsthreadhandle( &MujocoQontrol::PhysicsThread, mujoco_qontrol, sim.get(), mujoco_scene.c_str());
122
123 // start simulation UI loop (blocking call)
124 sim->RenderLoop();
125 physicsthreadhandle.join();
126
127 return 0;
128}
—
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 two tasks that will be updated every milliseconds.
The main task is expressed as a Cartesian Acceleration task.
std::shared_ptr<Task::JointTorque<ControlOutput::JointTorque>> regularisation_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.
main_task = torque_problem->task_set->add<Task::CartesianAcceleration>("MainTask"); regularisation_task = torque_problem->task_set->add<Task::JointTorque>("RegularisationTask",1e-5);
We then fill the task set of torque_problem with the main task and the regularisation task. Each tasks is given a name and a relative weight \(\omega\). This weight can be modified at any time.
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 fill the constraint set of torque_problem with the three pre-implemented constraints. Each constraint is given a name. These constraints will automatically be updated during the update of Qontrol.
robot_state.joint_position.resize(ndof); robot_state.joint_velocity.resize(ndof);
We also create the robot state and resize it.
traj = new TrajectoryGeneration(resource_path+"trajectory.csv", m->opt.timestep);
We create a simple trajectory that has been precalculated and store in a csv file. This trajectory start at the robot current Cartesian pose and does a translation of (-0.1, -0,1, -0.1) m.
Update
void updateController() override { 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 update the trajectory so that it gives the next Cartesian pose to reach in 1 ms.
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.
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(traj->velocity - model->getFrameVelocity(model->getTipFrameName())) + traj->acceleration; main_task->setTargetAcceleration(xdd_star); regularisation_task->setTargetTorque(model->getJointGravityTorques() - robot_state.joint_velocity); regularisation_task->setWeightingMatrix( model->getInverseJointInertiaMatrix());
The desired Cartesian acceleration is then fed to the main task. The regularisation task is also updated so that it compensate for gravity plus a damping term. The resulting regularisation task would be written : \(|| \boldsymbol{\tau} - (\boldsymbol{g} - \boldsymbol{\dot{q}})||^2_{M^{-1}}\)
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"""
3Non-Interactive Torque Control with Trajectory Following
4
5This example demonstrates:
6- Loading a robot model in both MuJoCo and Qontrol
7- Real-time torque-level inverse dynamics
8- Following a pre-defined Cartesian trajectory from CSV file
9- Cartesian acceleration tracking with joint torque commands
10
11Mirrors the C++ example: torqueQontrol.cpp
12
13Usage:
14 python torque_qontrol.py <robot_name>
15
16Example:
17 python torque_qontrol.py panda
18 python torque_qontrol.py panda --verbose
19"""
20
21import numpy as np
22import qontrol
23import pinocchio as pin
24import argparse
25import os
26import time
27from mujoco_helper import MujocoSimulator, compute_log6_error
28from trajectory_generation import TrajectoryGeneration
29
30
31class QontrolTorqueController:
32 """Torque controller with trajectory following using Qontrol"""
33
34 def __init__(self, robot_name: str, resource_path: str, mujoco_sim: MujocoSimulator, verbose: bool = False):
35 """
36 Initialize the Qontrol torque controller
37
38 Args:
39 robot_name: Name of the robot (e.g., 'panda')
40 resource_path: Path to robot resources folder
41 mujoco_sim: MuJoCo simulator instance
42 verbose: Print detailed diagnostics each iteration (default: False)
43 """
44 self.robot_name = robot_name
45 self.resource_path = resource_path
46 self.sim = mujoco_sim
47
48 # =================================================================
49 # Qontrol Setup - Robot Model and QP Problem
50 # =================================================================
51 urdf_path = os.path.join(resource_path, "robot.urdf")
52 if not os.path.exists(urdf_path):
53 raise FileNotFoundError(f"URDF file not found: {urdf_path}")
54
55 print(f"Loading Qontrol model from: {urdf_path}")
56 self.qontrol_model = qontrol.RobotModel.load_from_file(urdf_path)
57 self.ndof = self.qontrol_model.get_nr_of_degrees_of_freedom()
58 print(f"Robot has {self.ndof} degrees of freedom")
59
60 # Create QP solver
61 self.solver = qontrol.create_qpmad_solver()
62
63 # Create torque-level problem
64 self.torque_problem = qontrol.JointTorqueProblem(self.qontrol_model, self.solver)
65
66 # Add Cartesian acceleration task for end-effector tracking
67 self.main_task = self.torque_problem.task_set.add_cartesian_acceleration("MainTask", 1.0)
68
69 # Add joint torque regularization (minimize joint torques)
70 self.regularization_task = self.torque_problem.task_set.add_joint_torque("RegularizationTask", 1e-5)
71
72 # Add constraints
73 self.joint_config_constraint = self.torque_problem.constraint_set.add_joint_configuration("JointConfigurationConstraint")
74 self.joint_vel_constraint = self.torque_problem.constraint_set.add_joint_velocity("JointVelocityConstraint")
75 self.joint_torque_constraint = self.torque_problem.constraint_set.add_joint_torque("JointTorqueConstraint")
76
77 # Store verbose flag
78 self.verbose = verbose
79
80 # Robot state object
81 self.robot_state = qontrol.RobotState()
82 self.robot_state.resize(self.ndof)
83
84 # Get tip frame name
85 self.tip_frame = self.qontrol_model.get_tip_frame_name()
86 print(f"Controlling frame: {self.tip_frame}")
87
88 # Get initial joint state and update model
89 qpos, qvel = self.sim.get_joint_state()
90 self.robot_state.joint_position = qpos
91 self.robot_state.joint_velocity = qvel
92 self.qontrol_model.set_robot_state(self.robot_state)
93
94 print("dt:", self.sim.dt)
95
96 # =================================================================
97 # Trajectory Setup
98 # =================================================================
99 trajectory_path = os.path.join(resource_path, "trajectory.csv")
100 if not os.path.exists(trajectory_path):
101 raise FileNotFoundError(f"Trajectory file not found: {trajectory_path}")
102
103 print(f"Loading trajectory from: {trajectory_path}")
104 self.trajectory = TrajectoryGeneration(trajectory_path, self.sim.dt)
105
106 # =================================================================
107 # Control Parameters
108 # =================================================================
109 # PD gains for Cartesian space
110 self.p_gains = np.array([1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0])
111 self.d_gains = 2.0 * np.sqrt(self.p_gains) # Critical damping
112
113 # Statistics tracking
114 self.update_times = []
115 self.iteration_count = 0
116 self.last_stats_time = time.perf_counter()
117
118 def update(self):
119 """Main control update: compute and apply joint torques"""
120 start_time = time.perf_counter()
121
122 # 1. Update Qontrol model with current state from simulation
123 qpos, qvel = self.sim.get_joint_state()
124 self.robot_state.joint_position = qpos
125 self.robot_state.joint_velocity = qvel
126 self.qontrol_model.set_robot_state(self.robot_state)
127
128 # 2. Update trajectory to get next target
129 self.trajectory.update()
130 target_pose = self.trajectory.get_pose()
131 target_velocity = self.trajectory.get_velocity()
132 target_acceleration = self.trajectory.get_acceleration()
133
134 # 3. Get current end-effector pose
135 current_ee_matrix = self.qontrol_model.get_frame_pose(self.tip_frame)
136 current_se3 = pin.SE3(current_ee_matrix[:3, :3], current_ee_matrix[:3, 3])
137
138 # 4. Compute SE(3) log6 error
139 cartesian_error = compute_log6_error(target_pose, current_se3)
140 error_norm = np.linalg.norm(cartesian_error)
141
142 # 5. Get current end-effector velocity
143 ee_velocity = self.qontrol_model.get_frame_velocity(self.tip_frame)
144
145 # 6. Compute desired Cartesian acceleration (PD control + feedforward)
146 # xdd_star = Kp * err + Kd * (v_des - v_curr) + a_des
147 desired_acceleration = (self.p_gains * cartesian_error +
148 self.d_gains * (target_velocity - ee_velocity) +
149 target_acceleration)
150
151 # Set main task target
152 self.main_task.set_target_acceleration(desired_acceleration)
153
154 # 7. Set regularization task
155 # Target torques: gravity compensation minus velocity damping
156 g = self.qontrol_model.get_joint_gravity_torques()
157 target_torques = g - qvel
158 self.regularization_task.set_target_torque(target_torques)
159
160 # Set weighting matrix (inverse of inertia matrix)
161 M_inv = self.qontrol_model.get_inverse_joint_inertia_matrix()
162 self.regularization_task.set_weighting_matrix(M_inv)
163
164 # 8. Solve QP for joint torques
165 t_qp_start = time.perf_counter()
166 self.torque_problem.update(self.sim.dt)
167 t_qp = time.perf_counter() - t_qp_start
168
169 # 9. Apply torques to simulation
170 solution_found = self.torque_problem.solution_found()
171 if solution_found:
172 joint_torques = self.torque_problem.get_joint_torque_command()
173 torque_norm = np.linalg.norm(joint_torques)
174 self.sim.apply_torques(joint_torques)
175 else:
176 print("Warning: No QP solution found!")
177 torque_norm = 0.0
178 self.sim.apply_torques(np.zeros(self.ndof))
179
180 # Statistics
181 update_time = time.perf_counter() - start_time
182 self.update_times.append(update_time)
183 self.iteration_count += 1
184
185 # Verbose diagnostics
186 if self.verbose and self.iteration_count % 100 == 0:
187 print(f"\nIteration {self.iteration_count} (t={self.trajectory.time:.2f}s):")
188 print(f" QP solve: {t_qp*1000:.3f} ms")
189 print(f" Total: {update_time*1000:.3f} ms")
190 print(f" Error norm: {error_norm:.4f}")
191 print(f" Torque norm: {torque_norm:.2f} Nm")
192 print(f" Solution: {'FOUND' if solution_found else 'NOT FOUND'}")
193 target_pos = target_pose.translation
194 current_pos = current_se3.translation
195 pos_error = np.linalg.norm(target_pos - current_pos)
196 print(f" Position error: {pos_error*1000:.2f} mm")
197
198 # Print statistics every second
199 if not self.verbose:
200 current_time = time.perf_counter()
201 if current_time - self.last_stats_time >= 1.0:
202 avg_update_time = np.mean(self.update_times[-1000:])
203 progress = (self.trajectory.time / self.trajectory.duration) * 100
204 print(f"[Torque] t={self.trajectory.time:.1f}s/{self.trajectory.duration:.1f}s ({progress:.0f}%) | Compute: {avg_update_time*1000:.2f} ms | QP: {t_qp*1000:.2f} ms | Error: {error_norm:.4f}")
205 self.last_stats_time = current_time
206
207 # Check if trajectory is finished
208 if self.trajectory.is_finished():
209 return False # Signal to stop simulation
210
211 return True # Continue simulation
212
213 def print_statistics(self):
214 """Print final statistics"""
215 if self.update_times:
216 avg_time = np.mean(self.update_times)
217 update_rate = 1.0 / avg_time if avg_time > 0 else 0
218 print(f"\nFinal Statistics:")
219 print(f"Average update rate: {update_rate:.1f} Hz")
220 print(f"Average update time: {avg_time*1000:.2f} ms")
221 print(f"Max update time: {np.max(self.update_times)*1000:.2f} ms")
222 print(f"Min update time: {np.min(self.update_times)*1000:.2f} ms")
223 print(f"Total iterations: {self.iteration_count}")
224 print(f"Trajectory duration: {self.trajectory.duration:.2f}s")
225
226
227def main():
228 """Main entry point"""
229 parser = argparse.ArgumentParser(
230 description="Non-interactive torque control with trajectory following",
231 formatter_class=argparse.RawDescriptionHelpFormatter,
232 epilog="""
233Examples:
234 %(prog)s panda
235 %(prog)s panda --verbose
236 """
237 )
238 parser.add_argument("robot", type=str, help="Robot name (e.g., panda)")
239 parser.add_argument("--verbose", action="store_true", help="Print detailed diagnostics")
240
241 args = parser.parse_args()
242
243 # Get resource path
244 script_dir = os.path.dirname(os.path.abspath(__file__))
245 resource_path = os.path.join(script_dir, "..", "..", "..", "examples", "resources", args.robot)
246
247 if not os.path.exists(resource_path):
248 print(f"Error: Resource path not found: {resource_path}")
249 return 1
250
251 # Initialize MuJoCo simulator
252 sim = MujocoSimulator(args.robot, resource_path, "scene.xml")
253
254 # Initialize controller
255 controller = QontrolTorqueController(args.robot, resource_path, sim, verbose=args.verbose)
256
257 print("\n" + "="*60)
258 print("Starting torque control with trajectory following")
259 print("Press ESC to exit")
260 print("="*60 + "\n")
261
262 try:
263 # Run simulation with controller (will stop when trajectory finishes)
264 sim.run(controller.update)
265 except KeyboardInterrupt:
266 print("\nInterrupted by user")
267 except Exception as e:
268 print(f"\nError during simulation: {e}")
269 import traceback
270 traceback.print_exc()
271 return 1
272 finally:
273 # Print final statistics
274 controller.print_statistics()
275
276 return 0
277
278
279if __name__ == "__main__":
280 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.
self.qontrol_model = qontrol.RobotModel.load_from_file(urdf_path)
We use pinocchio for our model library.
self.torque_problem = qontrol.JointTorqueProblem(self.qontrol_model, self.solver)
The output of our qp controller is at the torque level.
self.main_task = self.torque_problem.task_set.add_cartesian_acceleration("MainTask", 1.0)
We then declare two tasks that will be updated every milliseconds.
The main task is expressed as a Cartesian Acceleration task.
self.regularization_task = self.torque_problem.task_set.add_joint_torque("RegularizationTask", 1e-5)
And we add a regularisation task (also at the torque level).
Initialization
During initialization we instantiate the model with the robot urdf.
urdf_path = os.path.join(resource_path, "robot.urdf")
if not os.path.exists(urdf_path):
raise FileNotFoundError(f"URDF file not found: {urdf_path}")
print(f"Loading Qontrol model from: {urdf_path}")
self.qontrol_model = qontrol.RobotModel.load_from_file(urdf_path)
We initialize the problem by giving it the model. By default, the qpmad library is used.
self.solver = qontrol.create_qpmad_solver()
# Create torque-level problem
self.torque_problem = qontrol.JointTorqueProblem(self.qontrol_model, self.solver)
We then fill the task set of torque_problem with the main task and the regularisation task. Each tasks is given a name and a relative weight \(\omega\). This weight can be modified at any time.
self.main_task = self.torque_problem.task_set.add_cartesian_acceleration("MainTask", 1.0)
# Add joint torque regularization (minimize joint torques)
self.regularization_task = self.torque_problem.task_set.add_joint_torque("RegularizationTask", 1e-5)
We then fill the constraint set of torque_problem with the three pre-implemented constraints. Each constraint is given a name. These constraints will automatically be updated during the update of Qontrol.
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")
We also create the robot state and resize it.
self.robot_state = qontrol.RobotState()
self.robot_state.resize(self.ndof)
We create a simple trajectory that has been precalculated and store in a csv file. This trajectory start at the robot current Cartesian pose and does a translation of (-0.1, -0,1, -0.1) m.
trajectory_path = os.path.join(resource_path, "trajectory.csv")
if not os.path.exists(trajectory_path):
raise FileNotFoundError(f"Trajectory file not found: {trajectory_path}")
print(f"Loading trajectory from: {trajectory_path}")
self.trajectory = TrajectoryGeneration(trajectory_path, self.sim.dt)
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 update the trajectory so that it gives the next Cartesian pose to reach in 1 ms.
def update(self):
"""Main control update: compute and apply joint torques"""
start_time = time.perf_counter()
# 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 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.
self.trajectory.update()
target_pose = self.trajectory.get_pose()
target_velocity = self.trajectory.get_velocity()
target_acceleration = self.trajectory.get_acceleration()
# 3. Get current end-effector pose
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])
# 4. Compute SE(3) log6 error
cartesian_error = compute_log6_error(target_pose, current_se3)
error_norm = np.linalg.norm(cartesian_error)
# 5. Get current end-effector velocity
ee_velocity = self.qontrol_model.get_frame_velocity(self.tip_frame)
# 6. Compute desired Cartesian acceleration (PD control + feedforward)
# xdd_star = Kp * err + Kd * (v_des - v_curr) + a_des
desired_acceleration = (self.p_gains * cartesian_error +
The desired Cartesian acceleration is then fed to the main task. The regularisation task is also updated so that it compensate for gravity plus a damping term. The resulting regularisation task would be written : \(|| \boldsymbol{\tau} - (\boldsymbol{g} - \boldsymbol{\dot{q}})||^2_{M^{-1}}\)
self.main_task.set_target_acceleration(desired_acceleration)
# 7. Set regularization task
# Target torques: gravity compensation minus velocity damping
g = self.qontrol_model.get_joint_gravity_torques()
target_torques = g - qvel
self.regularization_task.set_target_torque(target_torques)
# Set weighting matrix (inverse of inertia matrix)
M_inv = self.qontrol_model.get_inverse_joint_inertia_matrix()
self.regularization_task.set_weighting_matrix(M_inv)
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
# 9. Apply torques to simulation
solution_found = self.torque_problem.solution_found()
if solution_found:
joint_torques = self.torque_problem.get_joint_torque_command()
torque_norm = np.linalg.norm(joint_torques)
self.sim.apply_torques(joint_torques)
Main function
The main function function fetches the robot name given in argv and starts the Mujoco simulation.
def main():
"""Main entry point"""
parser = argparse.ArgumentParser(
description="Non-interactive torque control with trajectory following",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s panda
%(prog)s panda --verbose
"""
)
parser.add_argument("robot", type=str, help="Robot name (e.g., panda)")
parser.add_argument("--verbose", action="store_true", help="Print detailed diagnostics")
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
# Initialize MuJoCo simulator
sim = MujocoSimulator(args.robot, resource_path, "scene.xml")
# Initialize controller
controller = QontrolTorqueController(args.robot, resource_path, sim, verbose=args.verbose)
print("\n" + "="*60)
print("Starting torque control with trajectory following")
print("Press ESC to exit")
print("="*60 + "\n")
try:
# Run simulation with controller (will stop when trajectory finishes)
sim.run(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
finally:
# Print final statistics
controller.print_statistics()
return 0