Custom Constraint Interactive#
The following example solves a qp problem expressed at the joint velocity 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. 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")