Custom Task 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 is defined as a custom task and 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
 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::GenericTask> custom_task;
 31pinocchio::SE3 init_pose;
 32
 33Qontrol::RobotState robot_state;
 34
 35TrajectoryGeneration* traj;
 36std::string resource_path;
 37void initController() override
 38{
 39  model =
 40      Model::RobotModel::loadModelFromFile(resource_path+"robot.urdf");
 41  
 42  const int ndof = model->getNrOfDegreesOfFreedom();
 43  
 44  velocity_problem = std::make_shared<Qontrol::JointVelocityProblem>(model);  
 45
 46  custom_task = velocity_problem->task_set->add("CustomMainTask", 6, 1.0);
 47  auto regularisation_task = velocity_problem->task_set->add<Task::JointVelocity>("RegularisationTask",1e-5); // <-- Based on the template given will implement correct task representationn
 48
 49  auto joint_configuration_constraint = velocity_problem->constraint_set->add<Constraint::JointConfiguration>("JointConfigurationConstraint");
 50  auto joint_velocity_constraint = velocity_problem->constraint_set->add<Constraint::JointVelocity>("JointVelocityConstraint");
 51
 52  mju_copy(d->qpos, m->key_qpos, m->nu);
 53  robot_state.joint_position.resize(ndof);
 54  robot_state.joint_velocity.resize(ndof);
 55  for (int i=0; i<ndof ; ++i)
 56  {
 57    robot_state.joint_position[i] = d->qpos[i];
 58    robot_state.joint_velocity[i] = d->qvel[i];
 59  }
 60  model->setRobotState(robot_state);
 61  
 62  traj = new TrajectoryGeneration(resource_path+"trajectory.csv", m->opt.timestep);
 63  
 64}
 65
 66void updateController() override
 67{
 68  const int ndof = model->getNrOfDegreesOfFreedom();
 69
 70  for (int i=0; i<ndof ; ++i)
 71  {
 72    robot_state.joint_position[i] = d->qpos[i];
 73    robot_state.joint_velocity[i] = d->qvel[i];
 74  }
 75  model->setRobotState(robot_state);
 76
 77  pinocchio::SE3 current_pose(model->getFramePose(model->getTipFrameName()).matrix());
 78  traj->update();
 79  pinocchio::SE3 traj_pose(traj->pose.matrix());
 80  const pinocchio::SE3 tipMdes = current_pose.actInv(traj_pose);
 81  auto err = pinocchio::log6(tipMdes).toVector();
 82  Eigen::Matrix<double,6,1> p_gains;
 83  p_gains << 10,10,10,10,10,10;
 84  Eigen::Matrix<double,6,1> xd_star = p_gains.cwiseProduct(err);
 85  custom_task->setE(model->getJacobian(model->getTipFrameName()));
 86  custom_task->setf(xd_star);
 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  std::string robot = argv[1];
119  std::string mujoco_model = "./resources/"+robot+"/scene.xml";
120  mujoco_qontrol.resource_path = "./resources/"+robot+"/";
121
122  // start physics thread
123  std::thread physicsthreadhandle( &MujocoQontrol::PhysicsThread, mujoco_qontrol, sim.get(), mujoco_model);
124
125  // start simulation UI loop (blocking call)
126  sim->RenderLoop();
127  physicsthreadhandle.join();
128
129  return 0;
130}

Explanation of the code

Declaration

First we declare all the objects that will be used to define our problem.

std::shared_ptr<Qontrol::Task::GenericTask> custom_task;

We use pinocchio for our model library.

  velocity_problem = std::make_shared<Qontrol::JointVelocityProblem>(model);  

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

  custom_task = velocity_problem->task_set->add("CustomMainTask", 6, 1.0);

Here we declare the custom type as a GenericTask.

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.

  custom_task = velocity_problem->task_set->add("CustomMainTask", 6, 1.0);

We fill the task set of velocity_problem with the custom task. We give it a name, the dimension of the task, and the relative weight relatively to the other tasks. Here the task is in Cartesian space so it uses 6 degrees of freedom. Since it is the main task it has a maximal priority relatively to the other tasks so we give it a weight of 1.

  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 with the regularisation task. 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. It is given a small weight so that it doesn’t interfere with the main task

  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 fill the constraint set of velocity_problem with the two pre-implemented constraints. Each constraint is given a name. These constraints will automatically be updated during the update of Qontrol.

  mju_copy(d->qpos, m->key_qpos, m->nu);
  robot_state.joint_position.resize(ndof);
  robot_state.joint_velocity.resize(ndof);
  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);

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

  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.

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

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.

  custom_task->setE(model->getJacobian(model->getTipFrameName()));
  custom_task->setf(xd_star);

We then update the terms of the custom task. The E matrix is equal to the current robot Jacobian matrix expressed at the tip of the robot. The f term is the desired Cartesian velocity computed previously.

  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];
  std::string 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 Task Configuration Example
  4
  5This example demonstrates how to manually configure tasks by computing Jacobian
  6and setting E/f matrices directly, while using pre-implemented constraints.
  7
  8Compare with velocity_control_interactive.py which uses pre-implemented tasks.
  9
 10Usage:
 11    python custom_task_interactive.py <robot_name>
 12    
 13Example:
 14    python custom_task_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 CustomTaskController:
 26    """Velocity controller with custom manual task 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 (e.g., 'panda')
 34            resource_path: Path to robot resources folder
 35            mujoco_sim: MuJoCo simulator instance
 36        """
 37        self.robot_name = robot_name
 38        self.resource_path = resource_path
 39        self.sim = mujoco_sim
 40        
 41        # =================================================================
 42        # Qontrol Setup - Robot Model and QP Problem
 43        # =================================================================
 44        urdf_path = os.path.join(resource_path, "robot.urdf")
 45        if not os.path.exists(urdf_path):
 46            raise FileNotFoundError(f"URDF file not found: {urdf_path}")
 47        
 48        print(f"Loading Qontrol model from: {urdf_path}")
 49        self.qontrol_model = qontrol.RobotModel.load_from_file(urdf_path)
 50        self.ndof = self.qontrol_model.get_nr_of_degrees_of_freedom()
 51        print(f"Robot has {self.ndof} degrees of freedom")
 52        
 53        # Create QP solver
 54        self.solver = qontrol.create_qpmad_solver()
 55        
 56        # Create velocity-level problem
 57        self.velocity_problem = qontrol.JointVelocityProblem(self.qontrol_model, self.solver)
 58        
 59        # CUSTOM TASK: Add generic task and manually configure it
 60        # Using generic add() with dimension instead of add_cartesian_velocity()
 61        self.main_task = self.velocity_problem.task_set.add("MainTask", 6, 1.0)
 62        
 63        # CUSTOM TASK: Add generic regularization task
 64        self.regularization_task = self.velocity_problem.task_set.add("RegularizationTask", self.ndof, 1e-5)
 65        
 66        # Add constraints using pre-implemented classes
 67        self.joint_config_constraint = self.velocity_problem.constraint_set.add_joint_configuration("JointConfigurationConstraint")
 68        self.joint_vel_constraint = self.velocity_problem.constraint_set.add_joint_velocity("JointVelocityConstraint")
 69        
 70        # Set horizon for position constraint
 71        self.joint_config_constraint.set_horizon(15)
 72        
 73        # Robot state object
 74        self.robot_state = qontrol.RobotState()
 75        self.robot_state.resize(self.ndof)
 76        
 77        # Get tip frame name
 78        self.tip_frame = self.qontrol_model.get_tip_frame_name()
 79        print(f"Controlling frame: {self.tip_frame}")
 80        
 81        # =================================================================
 82        # Control Parameters
 83        # =================================================================
 84        self.p_gains = np.array([10.0, 10.0, 10.0, 5.0, 5.0, 5.0])  # [position, orientation] gains
 85    
 86    def update(self):
 87        """Main control update: compute and apply joint velocities"""
 88        # 1. Update Qontrol model with current state from simulation
 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        # 2. Get target and current poses
 95        target_se3 = self.sim.get_mocap_pose_se3()
 96        current_se3 = self.sim.get_frame_pose_se3(self.tip_frame)
 97        
 98        # 3. Compute SE(3) log6 error
 99        cartesian_error = compute_log6_error(target_se3, current_se3)
100        
101        # 4. Compute desired Cartesian velocity (P control)
102        desired_velocity = self.p_gains * cartesian_error
103        
104        # 5. CUSTOM TASK CONFIGURATION: Manually set task matrices
105        # Instead of: self.main_task.set_target_velocity(desired_velocity)
106        # We manually compute and set E (Jacobian) and f (desired velocity):
107        jacobian = self.qontrol_model.get_jacobian(self.tip_frame)
108        self.main_task.set_E(jacobian)  # Set task matrix E (Jacobian)
109        self.main_task.set_f(desired_velocity)  # Set target velocity f
110        
111        # Regularization task: minimize joint velocities
112        self.regularization_task.set_E(np.eye(self.ndof))
113        self.regularization_task.set_f(np.zeros(self.ndof))
114        
115        # 6. Solve QP for joint velocities (constraints auto-update)
116        self.velocity_problem.update(self.sim.dt)
117        
118        # 7. Apply joint velocities directly to simulation
119        if self.velocity_problem.solution_found():
120            joint_velocities = self.velocity_problem.get_joint_velocity_command()
121            self.sim.set_joint_velocities(joint_velocities)
122        else:
123            self.sim.set_joint_velocities(np.zeros(self.ndof))
124
125
126def main():
127    """Main entry point"""
128    parser = argparse.ArgumentParser(
129        description="Interactive velocity control with custom task configuration",
130        formatter_class=argparse.RawDescriptionHelpFormatter,
131        epilog="""
132Examples:
133  %(prog)s panda
134  %(prog)s ur5
135  
136The program expects the following files in examples/resources/<robot>/:
137  - scene_interactive.xml (MuJoCo scene with mocap body)
138  - robot.urdf (Robot description for Qontrol)
139"""
140    )
141    
142    parser.add_argument(
143        "robot",
144        type=str,
145        help="Robot name (e.g., 'panda', 'ur5')"
146    )
147    
148    parser.add_argument(
149        "--resources",
150        type=str,
151        default=None,
152        help="Path to resources directory (default: ../examples/resources/<robot>/)"
153    )
154    
155    args = parser.parse_args()
156    
157    # Determine resource path
158    if args.resources:
159        resource_path = args.resources
160    else:
161        # Default: ../../../examples/resources/<robot>/
162        # (from bindings/python/examples to examples/resources)
163        script_dir = os.path.dirname(os.path.abspath(__file__))
164        resource_path = os.path.join(script_dir, "..", "..", "..", "examples", "resources", args.robot)
165    
166    resource_path = os.path.abspath(resource_path)
167    
168    if not os.path.exists(resource_path):
169        print(f"Error: Resource path not found: {resource_path}")
170        print(f"\nExpected directory structure:")
171        print(f"  {resource_path}/")
172        print(f"    ├── scene_interactive.xml")
173        print(f"    └── robot.urdf")
174        return 1
175    
176    try:
177        # Create MuJoCo simulator
178        sim = MujocoSimulator(args.robot, resource_path, "scene_interactive.xml")
179        
180        # Create Qontrol controller with custom task configuration
181        controller = CustomTaskController(args.robot, resource_path, sim)
182        
183        # Initialize mocap to robot's end-effector pose
184        sim.init_mocap_to_frame(controller.tip_frame)
185        
186        # Run interactive simulation
187        sim.run_interactive(
188            controller_callback=controller.update,
189            instructions="""Controls:
190  - Drag the red sphere (mocap body) to move the target
191  - The robot will track the mocap target
192  - Press ESC or close window to exit
193  - Double-click to select mocap body
194
195This example demonstrates CUSTOM TASK configuration:
196  - Manually computing Jacobian and setting E/f matrices
197  - Using pre-implemented constraints"""
198        )
199        
200    except KeyboardInterrupt:
201        print("\nInterrupted by user")
202        return 0
203    except Exception as e:
204        print(f"\nError: {e}")
205        import traceback
206        traceback.print_exc()
207        return 1
208    
209    return 0
210
211
212if __name__ == "__main__":
213    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.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("MainTask", 6, 1.0)

Here we declare the custom type as a GenericTask.

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

We fill the task set of velocity_problem with the custom task. We give it a name, the dimension of the task, and the relative weight relatively to the other tasks. Here the task is in Cartesian space so it uses 6 degrees of freedom. Since it is the main task it has a maximal priority relatively to the other tasks so we give it a weight of 1.

        self.regularization_task = self.velocity_problem.task_set.add("RegularizationTask", self.ndof, 1e-5)

We then fill the task set with the regularisation task. 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. It is given a small weight so that it doesn’t interfere with the main task

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

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

        self.robot_state = qontrol.RobotState()
        self.robot_state.resize(self.ndof)

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

        sim.init_mocap_to_frame(controller.tip_frame)

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

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 velocities"""
        # 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 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.

        cartesian_error = compute_log6_error(target_se3, current_se3)
        
        # 4. Compute desired Cartesian velocity (P control)
        desired_velocity = self.p_gains * cartesian_error

We then update the terms of the custom task. The E matrix is equal to the current robot Jacobian matrix expressed at the tip of the robot. The f term is the desired Cartesian velocity computed previously.

        jacobian = self.qontrol_model.get_jacobian(self.tip_frame)
        self.main_task.set_E(jacobian)  # Set task matrix E (Jacobian)
        self.main_task.set_f(desired_velocity)  # Set target velocity f
        
        # Regularization task: minimize joint velocities
        self.regularization_task.set_E(np.eye(self.ndof))
        self.regularization_task.set_f(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)
        
        # 7. Apply joint velocities directly to simulation
        if self.velocity_problem.solution_found():
            joint_velocities = self.velocity_problem.get_joint_velocity_command()
            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():
    """Main entry point"""
    parser = argparse.ArgumentParser(
        description="Interactive velocity control with custom task configuration",
        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, "scene_interactive.xml")
        
        # Create Qontrol controller with custom task configuration
        controller = CustomTaskController(args.robot, resource_path, sim)
        
        # Initialize mocap to robot's end-effector pose
        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 TASK configuration:
  - Manually computing Jacobian and setting E/f matrices
  - Using pre-implemented constraints"""
        )
        
    except KeyboardInterrupt:
        print("\nInterrupted by user")
        return 0