Torque Qontrol#

# Torque control

The following example solves a qp problem expressed at the torque level such that:

\[\begin{split}\begin{array}{ccc}\boldsymbol{\tau}^{opt} = & \underset{\boldsymbol{\tau}}{\mathrm{argmin}} & ||\boldsymbol{\dot{v}}(\boldsymbol{\tau}) - \boldsymbol{\dot{v}}^{target} || + \omega || \boldsymbol{\tau} - (\boldsymbol{g} - \boldsymbol{\dot{q}})||^2_{M^{-1}}\\& \textrm{s.t.} & \boldsymbol{\tau^{min}} \leq \boldsymbol{\tau} \leq \boldsymbol{\tau^{max}}. \\ & & \boldsymbol{q}^{min} \leq \boldsymbol{q}(\boldsymbol\tau) \leq \boldsymbol{q}^{max} \\ & & \boldsymbol{\dot{q}}^{min} \leq \boldsymbol{\dot{q}}(\boldsymbol\tau) \leq \boldsymbol{\dot{q}}^{max} \end{array}\end{split}\]

.

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