Custom Task Interactive#
The following example solves a qp problem expressed at the joint velocity level such that:
.
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