Custom Constraint Interactive#

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

\[\begin{split}\begin{array}{ccc}\boldsymbol{\dot{q}}^{opt} = & \underset{\boldsymbol{\dot{q}}}{\mathrm{argmin}} & ||J(\boldsymbol{q})\boldsymbol{\dot{q}} - \boldsymbol{v}^{target} || + \omega || \boldsymbol{\dot{q}} ||^2\\& \textrm{s.t.} & \boldsymbol{\dot{q}^{min}} \leq \boldsymbol{\dot{q}} \leq \boldsymbol{\dot{q}^{max}}. \\ & & \boldsymbol{q}^{min} \leq \boldsymbol{q}(\boldsymbol{\dot{q}}) \leq \boldsymbol{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. In this example the joint velocity constraint is defined as a custom constraint, meaning that the user as to update the constraint manually.

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#define MUJOCO_PLUGIN_DIR "mujoco_plugin"
 20
 21
 22using namespace Qontrol;
 23
 24class MujocoQontrol : public MujocoSim 
 25{
 26public:
 27//------------------------------------------- simulation -------------------------------------------
 28std::shared_ptr<Qontrol::Model::RobotModel> model;
 29std::shared_ptr<Qontrol::JointVelocityProblem> velocity_problem;
 30std::shared_ptr<Qontrol::Task::CartesianVelocity<Qontrol::ControlOutput::JointVelocity>> main_task;
 31std::shared_ptr<Qontrol::Constraint::GenericConstraint> custom_constraint;
 32pinocchio::SE3 init_pose;
 33
 34Qontrol::RobotState robot_state;
 35
 36TrajectoryGeneration* traj;
 37std::string resource_path;
 38void initController() override
 39{
 40  model =
 41      Model::RobotModel::loadModelFromFile(resource_path+"robot.urdf");
 42  
 43  velocity_problem = std::make_shared<Qontrol::JointVelocityProblem>(model);  
 44  main_task = velocity_problem->task_set->add<Task::CartesianVelocity>("MainTask"); // <-- Based on the template given will implement correct task representation  
 45  auto regularisation_task = velocity_problem->task_set->add<Task::JointVelocity>("RegularisationTask",1e-5); // <-- Based on the template given will implement correct task representationn
 46
 47  auto joint_configuration_constraint = velocity_problem->constraint_set->add<Constraint::JointConfiguration>("JointConfigurationConstraint");
 48  custom_constraint =
 49      velocity_problem->constraint_set->add("CustomConstraint",model->getNrOfDegreesOfFreedom());
 50
 51  mju_copy(d->qpos, m->key_qpos, m->nu);
 52  robot_state.joint_position.resize(model->getNrOfDegreesOfFreedom());
 53  robot_state.joint_velocity.resize(model->getNrOfDegreesOfFreedom());
 54  for (int i=0; i<model->getNrOfDegreesOfFreedom() ; ++i)
 55  {
 56    robot_state.joint_position[i] = d->qpos[i];
 57    robot_state.joint_velocity[i] = d->qvel[i];
 58  }
 59  model->setRobotState(robot_state);
 60
 61  traj = new TrajectoryGeneration(resource_path+"trajectory.csv", m->opt.timestep);
 62}
 63
 64void updateController() override
 65{
 66
 67  for (int i=0; i<model->getNrOfDegreesOfFreedom() ; ++i)
 68  {
 69    robot_state.joint_position[i] = d->qpos[i];
 70    robot_state.joint_velocity[i] = d->qvel[i];
 71  }
 72  model->setRobotState(robot_state);
 73
 74  pinocchio::SE3 current_pose(model->getFramePose(model->getTipFrameName()).matrix());
 75  traj->update();
 76  pinocchio::SE3 traj_pose(traj->pose.matrix());
 77  const pinocchio::SE3 tipMdes = current_pose.actInv(traj_pose);
 78  auto err = pinocchio::log6(tipMdes).toVector();
 79  Eigen::Matrix<double,6,1> p_gains;
 80  p_gains << 10,10,10,10,10,10;
 81  Eigen::Matrix<double,6,1> xd_star = p_gains.cwiseProduct(err);
 82  main_task->setTargetVelocity(xd_star);
 83  custom_constraint->setConstraintMatrix(Eigen::MatrixXd::Identity(
 84  model->getNrOfDegreesOfFreedom(), model->getNrOfDegreesOfFreedom()));
 85  custom_constraint->setUpperBounds(model->getJointVelocityLimits());
 86  custom_constraint->setLowerBounds(-model->getJointVelocityLimits());
 87  velocity_problem->update(m->opt.timestep);
 88
 89  if (velocity_problem->solutionFound())
 90  {
 91    sendJointVelocity(velocity_problem->getJointVelocityCommand());    
 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  mjvScene scn;
102  mjv_defaultScene(&scn);
103
104  mjvCamera cam;
105  mjv_defaultCamera(&cam);
106
107  mjvOption opt;
108  mjv_defaultOption(&opt);
109
110  mjvPerturb pert;
111  mjv_defaultPerturb(&pert);
112
113  // simulate object encapsulates the UI
114  auto sim = std::make_unique<mj::Simulate>(
115      std::make_unique<mj::GlfwAdapter>(), &cam, &opt, &pert, /* fully_managed = */ true
116  );
117
118
119  std::string robot = argv[1];
120  const char* mujoco_model = "./resources/"+robot+"/scene.xml";
121  mujoco_qontrol.resource_path = "./resources/"+robot+"/";
122
123  // start physics thread
124  std::thread physicsthreadhandle( &MujocoQontrol::PhysicsThread, mujoco_qontrol, sim.get(), mujoco_model);
125
126  // start simulation UI loop (blocking call)
127  sim->RenderLoop();
128  physicsthreadhandle.join();
129
130  return 0;
131}

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 velocity level.

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

The main task is expressed as a Cartesian velocity task.

std::shared_ptr<Qontrol::Constraint::GenericConstraint> custom_constraint;

Here we declare the custom constraint as a GenericConstraint.

Initialization

void initController() override
{
  model =
      Model::RobotModel::loadModelFromFile(resource_path+"robot.urdf");

During initialization we instantiate the model with the robot urdf.

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

We initialize the problem by giving it the model. By default, the qpmad library is used.

std::shared_ptr<Qontrol::Task::CartesianVelocity<Qontrol::ControlOutput::JointVelocity>> main_task;
std::shared_ptr<Qontrol::Constraint::GenericConstraint> custom_constraint;
pinocchio::SE3 init_pose;

Qontrol::RobotState robot_state;

TrajectoryGeneration* traj;
std::string resource_path;
void initController() override
{
  model =
      Model::RobotModel::loadModelFromFile(resource_path+"robot.urdf");
  
  velocity_problem = std::make_shared<Qontrol::JointVelocityProblem>(model);  
  main_task = velocity_problem->task_set->add<Task::CartesianVelocity>("MainTask"); // <-- Based on the template given will implement correct task representation  
  auto regularisation_task = velocity_problem->task_set->add<Task::JointVelocity>("RegularisationTask",1e-5); // <-- Based on the template given will implement correct task representationn

We then fill the task set of velocity_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. In this example, the regularisation tasks is defined as a joint veloicty task. Its means that this task will minimize the overall robot joint veloicty.

  auto joint_configuration_constraint = velocity_problem->constraint_set->add<Constraint::JointConfiguration>("JointConfigurationConstraint");

We then fill the constraint set of velocity_problem with the pre-implemented joint configuration constraint. Each constraint is given a name. This constraint will automatically be updated during the update of Qontrol.

      velocity_problem->constraint_set->add("CustomConstraint",model->getNrOfDegreesOfFreedom());

Then we add our custom constraint. We give it a name to identify it and also the size of the constraint. This size will be used by Qontrol to resize the constraint set.

  robot_state.joint_position.resize(model->getNrOfDegreesOfFreedom());
  robot_state.joint_velocity.resize(model->getNrOfDegreesOfFreedom());
  for (int i=0; i<model->getNrOfDegreesOfFreedom() ; ++i)
  {
    robot_state.joint_position[i] = d->qpos[i];
    robot_state.joint_velocity[i] = d->qvel[i];
  }
  model->setRobotState(robot_state);

We create the robot state and fill it with the simulated robot current state.

  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
{

  for (int i=0; i<model->getNrOfDegreesOfFreedom() ; ++i)
  {
    robot_state.joint_position[i] = d->qpos[i];
    robot_state.joint_velocity[i] = d->qvel[i];
  }
  model->setRobotState(robot_state);

  pinocchio::SE3 current_pose(model->getFramePose(model->getTipFrameName()).matrix());
  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.

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

We then compute the desired Cartesian velocity using a simple proportionnal 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 are the proportionnal gains of the controller.

  main_task->setTargetVelocity(xd_star);

The desired Cartesian velocity is then fed to the main task.

  custom_constraint->setConstraintMatrix(Eigen::MatrixXd::Identity(
  model->getNrOfDegreesOfFreedom(), model->getNrOfDegreesOfFreedom()));
  custom_constraint->setUpperBounds(model->getJointVelocityLimits());
  custom_constraint->setLowerBounds(-model->getJointVelocityLimits());

Here we update the custom constraint. We first set the constraint matrix A to the identity matrix. Then we set the lower and upper bounds of the constraint with the robot joint velocity limits.

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

  mjvScene scn;
  mjv_defaultScene(&scn);

  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, /* fully_managed = */ true
  );


  std::string robot = argv[1];
  const char* mujoco_model = "./resources/"+robot+"/scene.xml";
  mujoco_qontrol.resource_path = "./resources/"+robot+"/";

  // start physics thread
  std::thread physicsthreadhandle( &MujocoQontrol::PhysicsThread, mujoco_qontrol, sim.get(), mujoco_model);

  // 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"""
  3Custom Constraint Configuration Example
  4
  5This example demonstrates how to manually configure constraints by setting
  6constraint matrix and bounds directly, while using pre-implemented tasks.
  7
  8Compare with velocity_control_interactive.py which uses pre-implemented constraints.
  9
 10Usage:
 11    python custom_constraint_interactive.py <robot_name>
 12    
 13Example:
 14    python custom_constraint_interactive.py panda
 15"""
 16
 17import numpy as np
 18import qontrol
 19import pinocchio as pin
 20import argparse
 21import os
 22from mujoco_helper import MujocoSimulator, compute_log6_error
 23
 24
 25class CustomConstraintController:
 26    """Velocity controller with custom manual constraint configuration"""
 27    
 28    def __init__(self, robot_name: str, resource_path: str, mujoco_sim: MujocoSimulator):
 29        """
 30        Initialize the controller
 31        
 32        Args:
 33            robot_name: Name of the robot
 34            resource_path: Path to robot resources
 35            mujoco_sim: MuJoCo simulator instance
 36        """
 37        self.robot_name = robot_name
 38        self.sim = mujoco_sim
 39        self.model = mujoco_sim.model
 40        self.data = mujoco_sim.data
 41        
 42        # Load robot model with Qontrol
 43        urdf_path = os.path.join(resource_path, "robot.urdf")
 44        if not os.path.exists(urdf_path):
 45            raise FileNotFoundError(f"URDF file not found: {urdf_path}")
 46        
 47        print(f"Loading Qontrol model from: {urdf_path}")
 48        self.qontrol_model = qontrol.RobotModel.load_from_file(urdf_path)
 49        self.ndof = self.qontrol_model.get_nr_of_degrees_of_freedom()
 50        print(f"Robot has {self.ndof} degrees of freedom")
 51        
 52        # Create QP solver
 53        self.solver = qontrol.create_qpmad_solver()
 54        
 55        # Create velocity-level QP problem
 56        self.velocity_problem = qontrol.JointVelocityProblem(self.qontrol_model, self.solver)
 57        
 58        # Create velocity-level QP problem
 59        self.velocity_problem = qontrol.JointVelocityProblem(self.qontrol_model, self.solver)
 60        
 61        # Add task for Cartesian velocity control (using pre-implemented task)
 62        self.main_task = self.velocity_problem.task_set.add_cartesian_velocity("MainTask", 1.0)
 63        
 64        # Add regularization task for joint velocity minimization (using pre-implemented task)
 65        self.regularization_task = self.velocity_problem.task_set.add_joint_velocity("Regularization", 1e-5)
 66        
 67        # Add configuration constraint using pre-implemented constraint
 68        self.joint_config_constraint = (
 69            self.velocity_problem.constraint_set.add_joint_configuration(
 70                "JointConfigurationConstraint"
 71            )
 72        )
 73        
 74        # Set horizon for position constraint (timesteps to joint limits)
 75        self.joint_config_constraint.set_horizon(15)
 76        
 77        # Add CUSTOM velocity constraint (manual configuration)
 78        # This replicates the joint velocity constraint but configured manually
 79        self.custom_velocity_constraint = self.velocity_problem.constraint_set.add(
 80            "CustomVelocityConstraint",
 81            self.ndof  # Dimension of the constraint
 82        )
 83        
 84        # Velocity limits will be set manually each iteration
 85        # (In practice, you would use add_joint_velocity, but this shows how to do it manually)
 86        
 87        # Robot state object
 88        self.robot_state = qontrol.RobotState()
 89        self.robot_state.resize(self.ndof)
 90        
 91        # Get tip frame name
 92        self.tip_frame = self.qontrol_model.get_tip_frame_name()
 93        print(f"Controlling frame: {self.tip_frame}")
 94        
 95        # Velocity limits (rad/s)
 96        self.velocity_max = 1.0
 97        
 98        # Control timestep
 99        self.dt = self.model.opt.timestep
100        
101        # Control gains (P controller)
102        self.p_gains = np.array([10.0, 10.0, 10.0, 10.0, 10.0, 10.0])  # [position, orientation] gains
103        
104        print(f"Controller initialized for {robot_name}")
105        print(f"  DOF: {self.ndof}")
106        print(f"  Control timestep: {self.dt} s")
107        print(f"  Using CUSTOM velocity constraint (manual configuration)")
108    
109    def update(self):
110        """
111        Compute control command
112        
113        Args:
114            q: Joint positions
115            dq: Joint velocities
116            
117        Returns:
118            Joint velocity commands
119        """
120        # Update robot state
121        qpos, qvel = self.sim.get_joint_state()
122        self.robot_state.joint_position = qpos
123        self.robot_state.joint_velocity = qvel
124        self.qontrol_model.set_robot_state(self.robot_state)
125        
126        # Get end-effector pose
127        current_se3 = self.sim.get_frame_pose_se3(self.tip_frame)
128        
129        # Get mocap target pose
130        target_se3 = self.sim.get_mocap_pose_se3()
131        
132        # Compute error in SE(3) (log map)
133        error = compute_log6_error(target_se3, current_se3)
134        
135        # Desired velocity (P controller in SE(3))
136        desired_velocity = self.p_gains * error
137        
138        # Update pre-implemented tasks
139        self.main_task.set_target_velocity(desired_velocity)
140        self.regularization_task.set_target_velocity(np.zeros(self.ndof))
141        
142        # Update CUSTOM velocity constraint (MANUAL configuration)
143        # This demonstrates the internal workings of constraints
144        # The constraint is: lower_bound <= A * dq_cmd <= upper_bound
145        # For velocity limits: -velocity_max <= dq_cmd <= velocity_max
146        # So A = I (identity matrix)
147        constraint_matrix = np.eye(self.ndof)
148        lower_bounds = np.full(self.ndof, -self.velocity_max)
149        upper_bounds = np.full(self.ndof, self.velocity_max)
150        
151        # Set constraint manually
152        self.custom_velocity_constraint.set_constraint_matrix(constraint_matrix)
153        self.custom_velocity_constraint.set_lower_bounds(lower_bounds)
154        self.custom_velocity_constraint.set_upper_bounds(upper_bounds)
155
156        self.velocity_problem.update(self.sim.dt)
157
158        # Solve QP
159        if self.velocity_problem.solution_found():
160            joint_velocities = self.velocity_problem.get_joint_velocity_command()
161            
162            # Directly set joint velocities in simulation
163            self.sim.set_joint_velocities(joint_velocities)
164            
165        else:
166            print("Warning: No QP solution found!")
167            # Fallback: set velocities to zero
168            self.sim.set_joint_velocities(np.zeros(self.ndof))
169
170
171def main():
172    parser = argparse.ArgumentParser(description="Custom constraint configuration example")
173    parser.add_argument("robot", type=str, help="Robot name (e.g., panda, ur5)")
174    args = parser.parse_args()
175    
176    # Set up paths
177    script_dir = os.path.dirname(os.path.abspath(__file__))
178    resource_path = os.path.join(script_dir, "..", "..", "..", "examples", "resources", args.robot)
179    resource_path = os.path.abspath(resource_path)
180    
181    # Create simulator
182    sim = MujocoSimulator(args.robot, resource_path)
183    
184    # Initialize mocap to end-effector pose
185    
186    # Create controller
187    controller = CustomConstraintController(args.robot, resource_path, sim)
188
189    sim.init_mocap_to_frame(controller.tip_frame)
190    
191    
192    # Run interactive simulation
193    sim.run_interactive(
194        controller_callback=controller.update,
195        instructions="""Controls:
196    - Drag the red sphere (mocap body) to move the target
197    - The robot will track the mocap target
198    - Press ESC or close window to exit
199    - Double-click to select mocap body  
200        This example demonstrates CUSTOM CONSTRAINT configuration."""
201        )
202    
203    print("Simulation ended")
204
205
206if __name__ == "__main__":
207    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.velocity_problem = qontrol.JointVelocityProblem(self.qontrol_model, self.solver)

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

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

The main task is expressed as a Cartesian velocity task.

        self.custom_velocity_constraint = self.velocity_problem.constraint_set.add(
            "CustomVelocityConstraint",
            self.ndof  # Dimension of the constraint

Here we declare the custom constraint as a GenericConstraint.

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 velocity-level QP problem
        self.velocity_problem = qontrol.JointVelocityProblem(self.qontrol_model, self.solver)

We then fill the task set of velocity_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. In this example, the regularisation tasks is defined as a joint veloicty task. Its means that this task will minimize the overall robot joint veloicty.

        self.main_task = self.velocity_problem.task_set.add_cartesian_velocity("MainTask", 1.0)
        
        # Add regularization task for joint velocity minimization (using pre-implemented task)
        self.regularization_task = self.velocity_problem.task_set.add_joint_velocity("Regularization", 1e-5)

We then fill the constraint set of velocity_problem with the pre-implemented joint configuration constraint. Each constraint is given a name. This constraint will automatically be updated during the update of Qontrol.

        self.joint_config_constraint = (
            self.velocity_problem.constraint_set.add_joint_configuration(
                "JointConfigurationConstraint"
            )
        )
        
        # Set horizon for position constraint (timesteps to joint limits)
        self.joint_config_constraint.set_horizon(15)

Then we add our custom constraint. We give it a name to identify it and also the size of the constraint. This size will be used by Qontrol to resize the constraint set.

        self.custom_velocity_constraint = self.velocity_problem.constraint_set.add(
            "CustomVelocityConstraint",
            self.ndof  # Dimension of the constraint

We create the robot state and fill it with the simulated robot current state.

        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.

        target_se3 = self.sim.get_mocap_pose_se3()

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):
        """
        Compute control command
        
        Args:
            q: Joint positions
            dq: Joint velocities
            
        Returns:
            Joint velocity commands
        """
        # Update robot state
        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 velocity using a simple proportionnal 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 are the proportionnal gains of the controller.

        error = compute_log6_error(target_se3, current_se3)
        
        # Desired velocity (P controller in SE(3))
        desired_velocity = self.p_gains * error

The desired Cartesian velocity is then fed to the main task.

        self.main_task.set_target_velocity(desired_velocity)

Here we update the custom constraint. We first set the constraint matrix A to the identity matrix. Then we set the lower and upper bounds of the constraint with the robot joint velocity limits.

        constraint_matrix = np.eye(self.ndof)
        lower_bounds = np.full(self.ndof, -self.velocity_max)
        upper_bounds = np.full(self.ndof, self.velocity_max)
        
        # Set constraint manually
        self.custom_velocity_constraint.set_constraint_matrix(constraint_matrix)
        self.custom_velocity_constraint.set_lower_bounds(lower_bounds)
        self.custom_velocity_constraint.set_upper_bounds(upper_bounds)

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)

        # Solve QP
        if self.velocity_problem.solution_found():
            joint_velocities = self.velocity_problem.get_joint_velocity_command()
            
            # Directly set joint velocities in simulation
            self.sim.set_joint_velocities(joint_velocities)

Main function

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

def main():
    parser = argparse.ArgumentParser(description="Custom constraint configuration example")
    parser.add_argument("robot", type=str, help="Robot name (e.g., panda, ur5)")
    args = parser.parse_args()
    
    # Set up paths
    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)
    
    # Create simulator
    sim = MujocoSimulator(args.robot, resource_path)
    
    # Initialize mocap to end-effector pose
    
    # Create controller
    controller = CustomConstraintController(args.robot, resource_path, sim)

    sim.init_mocap_to_frame(controller.tip_frame)
    
    
    # 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  
        This example demonstrates CUSTOM CONSTRAINT configuration."""
        )
    
    print("Simulation ended")