diff --git a/python/examples/cart_pulling.py b/python/examples/cart_pulling.py index 103355a4a0ad6ca8d169a4e785360471b3cb379b..08d59cf3f1924f6f3aa1ea02dbc239b562c2246c 100644 --- a/python/examples/cart_pulling.py +++ b/python/examples/cart_pulling.py @@ -5,14 +5,15 @@ import argparse, argcomplete from functools import partial from ur_simple_control.managers import getMinimalArgParser, ControlLoopManager, RobotManager, ProcessManager from ur_simple_control.optimal_control.get_ocp_args import get_OCP_args -from ur_simple_control.optimal_control.crocoddyl_optimal_control import createCrocoEEPathFollowingOCP, solveCrocoOCP -from ur_simple_control.optimal_control.crocoddyl_mpc import CrocoEndEffectorPathFollowingMPCControlLoop, CrocoEndEffectorPathFollowingMPC, BaseAndEEPathFollowingMPC +from ur_simple_control.optimal_control.crocoddyl_optimal_control import * +from ur_simple_control.optimal_control.crocoddyl_mpc import * from ur_simple_control.basics.basics import followKinematicJointTrajP from ur_simple_control.util.logging_utils import LogManager from ur_simple_control.visualize.visualize import plotFromDict -from ur_simple_control.clik.clik import getClikArgs, cartesianPathFollowingWithPlanner +from ur_simple_control.clik.clik import getClikArgs, cartesianPathFollowingWithPlanner, controlLoopClik, invKinmQP, dampedPseudoinverse import pinocchio as pin import crocoddyl +from functools import partial import importlib.util from ur_simple_control.path_generation.planner import starPlanner, getPlanningArgs, createMap import yaml @@ -26,7 +27,8 @@ def get_args(): parser = get_OCP_args(parser) parser = getClikArgs(parser) # literally just for goal error parser = getPlanningArgs(parser) - parser.add_argument('--handlebar-height', type=float, default=0.5) + parser.add_argument('--handlebar-height', type=float, default=0.5,\ + help="heigh of handlebar of the cart to be pulled") parser.add_argument('--base-to-handlebar-preferred-distance', type=float, default=0.7, \ help="prefered path arclength from mobile base position to handlebar") argcomplete.autocomplete(parser) @@ -43,6 +45,127 @@ def get_args(): args.n_pol = mpc_params['n_pol'] return args +def isGraspOK(args, robot, grasp_pose): + isOK = False + SEerror = robot.getT_w_e().actInv(grasp_pose) + err_vector = pin.log6(SEerror).vector + # TODO: figure this out + # it seems you have to use just the arm to get to finish with this precision + #if np.linalg.norm(err_vector) < robot.args.goal_error: + if np.linalg.norm(err_vector) < 2*1e-1: + isOK = True + return not isOK + +def cartPullingControlLoop(args, robot : RobotManager, goal, solver_grasp, solver_pulling, + path_planner : ProcessManager, i : int, past_data): + """ + cartPulling + 0) if obstacles present, don't move + 1) if no obstacles, but not grasped/grasp is off-target, grasp the handlebar with a p2p strategy. + 2) if no obstacles, and grasp ok, then pull toward goal with cartPulling mpc + 3) parking? + 4) ? always true (or do nothing is always true, whatever) + """ + # we use binary as string representation (i don't want to deal with python's binary representation). + # the reason for this is that then we don't have disgusting nested ifs + priority_register = ['0','1','1'] + # TODO implement this based on laser scanning or whatever + #priority_register[0] = str(int(areObstaclesTooClose())) + # TODO: get grasp pose from vision, this is initial hack to see movement + if i < 300: + grasp_pose = pin.SE3(np.eye(3), np.array([9.0 + args.base_to_handlebar_preferred_distance, 4.0, args.handlebar_height])) + else: + grasp_pose = robot.getT_w_e() + priority_register[1] = str(int(isGraspOK(args, robot, grasp_pose))) + # interpret string as base 2 number, return int in base 10 + priority_int = "" + for prio in priority_register: + priority_int += prio + priority_int = int(priority_int, 2) + breakFlag = False + save_past_item = {} + log_item = {} + + # case 0) + if priority_int >= 4: + robot.sendQd(np.zeros(robot.model.nv)) + + # case 1) + # TODO: make it an argument obviously + usempc = False + if (priority_int < 4) and (priority_int >= 2): + # TODO: make goal an argument, remove Mgoal from robot + robot.Mgoal = grasp_pose + if usempc: + # TODO: make it not hit the handlebar or other shit in the process + for i, runningModel in enumerate(solver_grasp.problem.runningModels): + runningModel.differential.costs.costs['gripperPose'].cost.residual.reference = grasp_pose + solver_grasp.problem.terminalModel.differential.costs.costs['gripperPose'].cost.residual.reference = grasp_pose + robot.Mgoal = grasp_pose + #breakFlag, save_past_item, log_item = CrocoIKMPCControlLoop(args, robot, solver_grasp, i, past_data) + CrocoIKMPCControlLoop(args, robot, solver_grasp, i, past_data) + else: + #controlLoopClik(robot, invKinmQP, i, past_data) + clikController = partial(dampedPseudoinverse, 1e-3) + controlLoopClik(robot, clikController, i, past_data) + + # case 2) + # MASSIVE TODO: + # WHEN STARTING, TO INITIALIZE PREPOPULATE THE PATH WITH AN INTERPOLATION OF + # A LINE FROM WHERE THE GRIPPER IS NOW TO THE BASE + if priority_int < 2: + breakFlag, save_past_item, log_item = BaseAndEEPathFollowingMPCControlLoop(args, robot, solver_pulling, path_planner, i, past_data) + #BaseAndEEPathFollowingMPCControlLoop(args, robot, solver_pulling, path_planner, i, past_data) + + q = robot.getQ() + p = q[:2] + # needed for cart pulling + save_past_item['path2D_untimed'] = p + log_item['qs'] = q.reshape((robot.model.nq,)) + log_item['dqs'] = robot.getQd().reshape((robot.model.nv,)) + return breakFlag, save_past_item, log_item + + +def cartPulling(args, robot : RobotManager, goal, path_planner): + ############################ + # setup cart-pulling mpc # + ############################ + x0 = np.concatenate([robot.getQ(), robot.getQd()]) + problem_pulling = createBaseAndEEPathFollowingOCP(args, robot, x0) + if args.solver == "boxfddp": + solver_pulling = crocoddyl.SolverBoxFDDP(problem_pulling) + if args.solver == "csqp": + solver_pulling = mim_solvers.SolverCSQP(problem_pulling) + xs_init = [x0] * (solver_pulling.problem.T + 1) + us_init = solver_pulling.problem.quasiStatic([x0] * solver_pulling.problem.T) + solver_pulling.solve(xs_init, us_init, args.max_solver_iter) + + ############################################# + # setup point-to-point handlebar grasping # + # TODO: have option to swith this for clik # + ############################################# + grasp_pose = robot.getT_w_e() + problem_grasp = createCrocoIKOCP(args, robot, x0, grasp_pose) + if args.solver == "boxfddp": + solver_grasp = crocoddyl.SolverBoxFDDP(problem_grasp) + if args.solver == "csqp": + solver_grasp = mim_solvers.SolverCSQP(problem_grasp) + xs_init = [x0] * (solver_grasp.problem.T + 1) + us_init = solver_grasp.problem.quasiStatic([x0] * solver_grasp.problem.T) + solver_grasp.solve(xs_init, us_init, args.max_solver_iter) + + controlLoop = partial(cartPullingControlLoop, args, robot, goal, solver_grasp, solver_pulling, path_planner) + + log_item = {} + q = robot.getQ() + T_w_e = robot.getT_w_e() + log_item['qs'] = q.reshape((robot.model.nq,)) + log_item['dqs'] = robot.getQd().reshape((robot.model.nv,)) + save_past_item = {'path2D_untimed' : T_w_e.translation[:2]} + loop_manager = ControlLoopManager(robot, controlLoop, args, save_past_item, log_item) + loop_manager.run() + + if __name__ == "__main__": args = get_args() @@ -51,7 +174,6 @@ if __name__ == "__main__": robot = RobotManager(args) robot.q[0] = 9.0 robot.q[1] = 4.0 - #time.sleep(5) x0 = np.concatenate([robot.getQ(), robot.getQd()]) robot._step() @@ -79,7 +201,9 @@ if __name__ == "__main__": planning_function = partial(starPlanner, goal) # TODO: ensure alignment in orientation between planner and actual robot path_planner = ProcessManager(args, planning_function, robot.q[:2], 3, None) - #time.sleep(5) + # wait for meshcat to initialize + if args.visualize_manipulator: + time.sleep(5) # clik version #cartesianPathFollowingWithPlanner(args, robot, path_planner) # end-effector tracking version @@ -88,9 +212,10 @@ if __name__ == "__main__": # and also make the actual path for the cart and then construct the reference # for the mobile base out of a later part of the path) # atm this is just mobile base tracking - BaseAndEEPathFollowingMPC(args, robot, x0, path_planner) + cartPulling(args, robot, goal, path_planner) print("final position:") print(robot.getT_w_e()) + path_planner.terminateProcess() if args.save_log: robot.log_manager.plotAllControlLoops() diff --git a/python/ur_simple_control/__pycache__/managers.cpython-312.pyc b/python/ur_simple_control/__pycache__/managers.cpython-312.pyc index 0343be7262ccf487703d207bbfbaeb2cdb691335..e02e89e5479c52cad283376b57866178a3e4284e 100644 Binary files a/python/ur_simple_control/__pycache__/managers.cpython-312.pyc and b/python/ur_simple_control/__pycache__/managers.cpython-312.pyc differ diff --git a/python/ur_simple_control/basics/__pycache__/basics.cpython-312.pyc b/python/ur_simple_control/basics/__pycache__/basics.cpython-312.pyc index e1f51ccc304659f0c256d15a324b24fc9c0a6ee0..5f7f84bdf9b600f8d0c911ebd0919c211ea1c011 100644 Binary files a/python/ur_simple_control/basics/__pycache__/basics.cpython-312.pyc and b/python/ur_simple_control/basics/__pycache__/basics.cpython-312.pyc differ diff --git a/python/ur_simple_control/clik/clik.py b/python/ur_simple_control/clik/clik.py index 7b5300522657104aaccdaa6e489ab6cfc84ee426..c26e75c9ffe7dc58398453c5816f08b85e6f88ef 100644 --- a/python/ur_simple_control/clik/clik.py +++ b/python/ur_simple_control/clik/clik.py @@ -92,9 +92,8 @@ def invKinmQP(J, err_vector, lb=None, ub=None): # TODO: why is q all 0? q = np.array([0] * J.shape[1], dtype="double") G = None - # TODO: extend for orientation as well - b = err_vector#[:3] - A = J#[:3] + b = err_vector + A = J # TODO: you probably want limits here #lb = None #ub = None diff --git a/python/ur_simple_control/managers.py b/python/ur_simple_control/managers.py index f6731d07ba20184911cde95e4cb80c7a95727034..70e8783c25401084bb70c7741593b4353053d1bb 100644 --- a/python/ur_simple_control/managers.py +++ b/python/ur_simple_control/managers.py @@ -88,8 +88,10 @@ def getMinimalArgParser(): default="192.168.1.102") parser.add_argument('--pinocchio-only', action=argparse.BooleanOptionalAction, \ help="whether you want to just integrate with pinocchio", default=True) + parser.add_argument('--ctrl-freq', type=int, \ + help="frequency of the control loop", default=500) parser.add_argument('--fast-simulation', action=argparse.BooleanOptionalAction, \ - help="do you want simulation to run over 500Hz? (real-time viz relies on 500Hz)", default=False) + help="do you want simulation to as fast as possible? (real-time viz relies on 500Hz)", default=False) parser.add_argument('--visualize-manipulator', action=argparse.BooleanOptionalAction, \ help="whether you want to visualize the manipulator and workspace with meshcat", default=True) parser.add_argument('--real-time-plotting', action=argparse.BooleanOptionalAction, \ @@ -440,7 +442,8 @@ class RobotManager: #self.JOINT_ID = 6 self.ee_frame_id = self.model.getFrameId("tool0") #self.ee_frame_id = self.model.getFrameId("hande_right_finger_joint") - self.update_rate = 500 #Hz + # TODO: add -1 option here meaning as fast as possible + self.update_rate = args.ctrl_freq #Hz self.dt = 1 / self.update_rate # you better not give me crazy stuff # and i'm not clipping it, you're fixing it @@ -1135,7 +1138,8 @@ class ProcessManager: self.shm_cmd.unlink() if self.args.debug_prints: print(f"i am putting befree in {self.side_process.name}'s command queue to stop it") - self.command_queue.put_nowait("befree") + if self.comm_direction != 3: + self.command_queue.put_nowait("befree") try: self.side_process.terminate() if self.args.debug_prints: diff --git a/python/ur_simple_control/optimal_control/crocoddyl_mpc.py b/python/ur_simple_control/optimal_control/crocoddyl_mpc.py index d0a17c5b87a81e5fe305179912ed06bf62197bb5..644615345770826ac408fc3975adc19eebec6fdc 100644 --- a/python/ur_simple_control/optimal_control/crocoddyl_mpc.py +++ b/python/ur_simple_control/optimal_control/crocoddyl_mpc.py @@ -1,6 +1,6 @@ from ur_simple_control.managers import getMinimalArgParser, ControlLoopManager, RobotManager, ProcessManager from ur_simple_control.optimal_control.crocoddyl_optimal_control import createCrocoIKOCP, createCrocoEEPathFollowingOCP, createBaseAndEEPathFollowingOCP -from ur_simple_control.path_generation.planner import path2D_to_timed_SE3, pathPointFromPathParam, path2D_to_timed_SE3_base_and_ee +from ur_simple_control.path_generation.planner import path2D_timed, pathPointFromPathParam, path2D_to_SE3 import pinocchio as pin import crocoddyl import numpy as np @@ -54,13 +54,13 @@ def CrocoIKMPCControlLoop(args, robot : RobotManager, solver, i, past_data): robot.sendQd(vel_cmds) log_item['qs'] = x0[:robot.model.nq] - log_item['dqs'] = x0[robot.model.nq:] + log_item['dqs'] = x0[robot.model.nv:] log_item['dqs_cmd'] = vel_cmds log_item['u_tau'] = us[0] return breakFlag, save_past_dict, log_item -def CrocoIKMPC(args, robot, x0, goal): +def CrocoIKMPC(args, robot, goal): """ IKMPC ----- @@ -69,6 +69,7 @@ def CrocoIKMPC(args, robot, x0, goal): a dynamics level, and velocities we command are actually extracted from the state x(q,dq) """ + x0 = np.concatenate([robot.getQ(), robot.getQd()]) problem = createCrocoIKOCP(args, robot, x0, goal) if args.solver == "boxfddp": solver = crocoddyl.SolverBoxFDDP(problem) @@ -78,7 +79,6 @@ def CrocoIKMPC(args, robot, x0, goal): # technically should be done in controlloop because now # it's solved 2 times before the first command, # but we don't have time for details rn - x0 = np.concatenate([robot.getQ(), robot.getQd()]) xs_init = [x0] * (solver.problem.T + 1) us_init = solver.problem.quasiStatic([x0] * solver.problem.T) solver.solve(xs_init, us_init, args.max_solver_iter) @@ -112,6 +112,7 @@ def CrocoEndEffectorPathFollowingMPCControlLoop(args, robot : RobotManager, solv T_w_e = robot.getT_w_e() p = T_w_e.translation[:2] + # TODO: replace with shm if not (type(path_planner) == types.FunctionType): path_planner.sendFreshestCommand({'p' : p}) @@ -124,7 +125,7 @@ def CrocoEndEffectorPathFollowingMPCControlLoop(args, robot : RobotManager, solv data = path_planner.getData() if data == None: if args.debug_prints: - print("got no path so no doing anything") + print("CTRL: got no path so not i won't move") robot.sendQd(np.zeros(robot.model.nv)) log_item['qs'] = q.reshape((robot.model.nq,)) log_item['dqs'] = robot.getQd().reshape((robot.model.nv,)) @@ -203,7 +204,7 @@ def CrocoEndEffectorPathFollowingMPC(args, robot, x0, path_planner): loop_manager.run() -def BaseAndEEPathFollowingMPCControlLoop(args, robot : RobotManager, solver : crocoddyl.SolverBoxFDDP, path_planner : ProcessManager, i, past_data): +def BaseAndEEPathFollowingMPCControlLoop(args, robot : RobotManager, solver : crocoddyl.SolverBoxFDDP, path_planner : ProcessManager, iter_n, past_data): """ CrocoPathFollowingMPCControlLoop ----------------------------- @@ -219,54 +220,58 @@ def BaseAndEEPathFollowingMPCControlLoop(args, robot : RobotManager, solver : cr q = robot.getQ() T_w_e = robot.getT_w_e() - #p = T_w_e.translation[:2] p = q[:2] - #print("CTRL:", p) # NOTE: this is the actual position, not what the path suggested # whether this or path reference should be put in is debateable. # this feels more correct to me. save_past_dict['path2D_untimed'] = p + path_planner.sendCommandViaSharedMemory(p) - if not (type(path_planner) == types.FunctionType): - #path_planner.sendFreshestCommand({'p' : p}) - path_planner.sendCommandViaSharedMemory(p) - + ########################### + # get path from planner # + ########################### # NOTE: it's pointless to recalculate the path every time # if it's the same 2D path - # get the path from the base from the current base position onward - if type(path_planner) == types.FunctionType: - pathSE3 = path_planner(T_w_e, i) - else: - data = path_planner.getData() - if data == None: - if args.debug_prints: - print("got no path so no doing anything") - robot.sendQd(np.zeros(robot.model.nv)) - log_item['qs'] = q.reshape((robot.model.nq,)) - log_item['dqs'] = robot.getQd().reshape((robot.model.nv,)) - return breakFlag, save_past_dict, log_item - if data == "done": - breakFlag = True - - path_pol_base, path2D_untimed_base = data - #path2D_untimed_base.insert(0, p[1]) - #path2D_untimed_base.insert(0, p[0]) - path2D_untimed_base = np.array(path2D_untimed_base).reshape((-1,2)) - # should be precomputed somewhere but this is nowhere near the biggest problem - max_base_v = np.linalg.norm(robot.model.velocityLimit[:2]) - - # 1) make 6D path out of [[x0,y0],...] - # that represents the center of the cart - # TODO: USE THIS ONCE IMPLEMENTED - #pathSE3 = path2D_to_timed_SE3_base_and_ee(args, path_pol_base, path2D_untimed_base, max_base_v) - # TODO: separate out timing construction so that it can be used for the handlebar path as well - pathSE3_base = path2D_to_timed_SE3(args, path_pol_base, path2D_untimed_base, max_base_v, 0.0) - # TODO: EVIL AND HAS TO BE REMOVED FROM HERE - if args.visualize_manipulator: - if i % 20 == 0: - robot.visualizer_manager.sendCommand({"path": pathSE3_base}) + # get the path from the base from the current base position onward. + # but we're still fast so who cares + data = path_planner.getData() + if data == None: + if args.debug_prints: + print("CTRL: got no path so not i won't move") + robot.sendQd(np.zeros(robot.model.nv)) + log_item['qs'] = q.reshape((robot.model.nq,)) + log_item['dqs'] = robot.getQd().reshape((robot.model.nv,)) + return breakFlag, save_past_dict, log_item + + if data == "done": + breakFlag = True + robot.sendQd(np.zeros(robot.model.nv)) + log_item['qs'] = q.reshape((robot.model.nq,)) + log_item['dqs'] = robot.getQd().reshape((robot.model.nv,)) + return breakFlag, save_past_dict, log_item + + ########################################## + # construct timed 2D path for the base # + ########################################## + path_pol_base, path2D_untimed_base = data + path2D_untimed_base = np.array(path2D_untimed_base).reshape((-1,2)) + # ideally should be precomputed somewhere + max_base_v = np.linalg.norm(robot.model.velocityLimit[:2]) + # base just needs timing on the path, + # and it's of height 0 (i.e. the height of base's planar joint) + path_base = path2D_timed(args, path_pol_base, path2D_untimed_base, max_base_v, 0.0) - # now find the previous path point of arclength base_to_handlebar_preferred_distance + ################################################### + # construct timed SE3 path for the end-effector # + ################################################### + # this works as follow + # 1) find the previous path point of arclength base_to_handlebar_preferred_distance. + # first part of the path is from there to current base position, + # second is just the current base's plan. + # 2) construct timing on the first part. + # 3) join that with the already timed second part. + # 4) turn the timed 2D path into an SE3 trajectory + # NOTE: this can be O(1) instead of O(n) but i can't be bothered path_arclength = np.linalg.norm(p - past_data['path2D_untimed']) handlebar_path_index = -1 @@ -276,11 +281,33 @@ def BaseAndEEPathFollowingMPCControlLoop(args, robot : RobotManager, solver : cr break path_arclength += np.linalg.norm(past_data['path2D_untimed'][i - 1] - past_data['path2D_untimed'][i]) # i shouldn't need to copy-paste everything but what can you do - arra = np.array(past_data['path2D_untimed']) - path2D_handlebar = np.vstack((arra[i:], path2D_untimed_base)) - # now we need to construct timing for this - - + path2D_handlebar_1_untimed = np.array(past_data['path2D_untimed']) + # NOTE: BIG ASSUMPTION + # let's say we're computing on time, and that's the time spacing + # of previous path points. + # this means you need to lower the control frequency argument + # if you're not meeting deadlines. + # if you really need timing information, you should actually + # get it from ControlLoopManager instead of i, + # but let's say this is better because you're forced to know + # how fast you are instead of ducktaping around delays. + # TODO: actually save timing, pass t instead of i to controlLoops + # from controlLoopManager + # NOTE: this might not working when rosified so check that first + time_past = np.linspace(0.0, args.past_window_size * robot.dt, args.past_window_size) + s = np.linspace(0.0, args.n_knots * args.ocp_dt, args.n_knots) + path2D_handlebar_1 = np.hstack(( + np.interp(s, time_past, path2D_handlebar_1_untimed[:,0]).reshape((-1,1)), + np.interp(s, time_past, path2D_handlebar_1_untimed[:,1]).reshape((-1,1)))) + + # TODO: combine with base for maximum correctness + pathSE3_handlebar = path2D_to_SE3(path2D_handlebar_1, args.handlebar_height) + # TODO: EVIL AND HAS TO BE REMOVED FROM HERE + # TODO: make it work with just translations too + if args.visualize_manipulator: + if iter_n % 20 == 0: + #robot.visualizer_manager.sendCommand({"path": path_base}) + robot.visualizer_manager.sendCommand({"path": pathSE3_handlebar}) x0 = np.concatenate([robot.getQ(), robot.getQd()]) solver.problem.x0 = x0 @@ -290,10 +317,12 @@ def BaseAndEEPathFollowingMPCControlLoop(args, robot : RobotManager, solver : cr us_init = list(solver.us[1:]) + [solver.us[-1]] for i, runningModel in enumerate(solver.problem.runningModels): - runningModel.differential.costs.costs['gripperPose' + str(i)].cost.residual.reference = pathSE3_base[i] + runningModel.differential.costs.costs['base_translation' + str(i)].cost.residual.reference = path_base[i] + runningModel.differential.costs.costs['ee_pose' + str(i)].cost.residual.reference = pathSE3_handlebar[i] # idk if that's necessary - solver.problem.terminalModel.differential.costs.costs['gripperPose'+str(args.n_knots)].cost.residual.reference = pathSE3_base[-1] + solver.problem.terminalModel.differential.costs.costs['base_translation'+str(args.n_knots)].cost.residual.reference = path_base[-1] + solver.problem.terminalModel.differential.costs.costs['ee_pose'+str(args.n_knots)].cost.residual.reference = pathSE3_handlebar[-1] solver.solve(xs_init, us_init, args.max_solver_iter) xs = np.array(solver.xs) @@ -306,7 +335,7 @@ def BaseAndEEPathFollowingMPCControlLoop(args, robot : RobotManager, solver : cr log_item['dqs'] = robot.getQd().reshape((robot.model.nv,)) return breakFlag, save_past_dict, log_item -def BaseAndEEPathFollowingMPC(args, robot, x0, path_planner): +def BaseAndEEPathFollowingMPC(args, robot, path_planner): """ CrocoEndEffectorPathFollowingMPC ----- @@ -316,16 +345,13 @@ def BaseAndEEPathFollowingMPC(args, robot, x0, path_planner): are actually extracted from the state x(q,dq). """ + x0 = np.concatenate([robot.getQ(), robot.getQd()]) problem = createBaseAndEEPathFollowingOCP(args, robot, x0) if args.solver == "boxfddp": solver = crocoddyl.SolverBoxFDDP(problem) if args.solver == "csqp": solver = mim_solvers.SolverCSQP(problem) - # technically should be done in controlloop because now - # it's solved 2 times before the first command, - # but we don't have time for details rn - x0 = np.concatenate([robot.getQ(), robot.getQd()]) xs_init = [x0] * (solver.problem.T + 1) us_init = solver.problem.quasiStatic([x0] * solver.problem.T) solver.solve(xs_init, us_init, args.max_solver_iter) diff --git a/python/ur_simple_control/optimal_control/crocoddyl_optimal_control.py b/python/ur_simple_control/optimal_control/crocoddyl_optimal_control.py index e323d5969a5b1bb2678bdd21c44996bca3a9c33c..09157b30ead2d179ff9301f477cbc1fb5dec347a 100644 --- a/python/ur_simple_control/optimal_control/crocoddyl_optimal_control.py +++ b/python/ur_simple_control/optimal_control/crocoddyl_optimal_control.py @@ -396,7 +396,10 @@ def createBaseAndEEPathFollowingOCP(args, robot : RobotManager, x0): NOTE: the path MUST be time indexed with the SAME time used between the knots """ T_w_e = robot.getT_w_e() - path = [T_w_e] * args.n_knots + # TODO there has to be a path for the base and + # a different one for the ee + path_ee = [T_w_e] * args.n_knots + path_base = [np.append(x0[:2], 0.0)] * args.n_knots # underactuation is done by setting max-torque to 0 in the robot model, # and since we torques are constrained we're good state = crocoddyl.StateMultibody(robot.model) @@ -486,23 +489,21 @@ def createBaseAndEEPathFollowingOCP(args, robot : RobotManager, x0): runningCostModel.addCost("uReg", uRegCost, 1e-3) if args.solver == "boxfddp": runningCostModel.addCost("limitCost", limitCost, 1e3) - # TODO: implement -# framePlacementResidual = crocoddyl.ResidualModelFramePlacement( -# state, -# robot.model.getFrameId("tool0"), -# path["ee"][i], # this better be done with the same time steps as the knots -# # NOTE: this should be done outside of this function to clarity -# state.nv) - framePlacementResidual = crocoddyl.ResidualModelFramePlacement( + eePoseResidual = crocoddyl.ResidualModelFramePlacement( + state, + robot.model.getFrameId("tool0"), + path_ee[i], # this better be done with the same time steps as the knots + # NOTE: this should be done outside of this function to clarity + state.nv) + eeTrackingCost = crocoddyl.CostModelResidual(state, eePoseResidual) + runningCostModel.addCost("ee_pose" + str(i), eeTrackingCost, 1e2) + baseTranslationResidual = crocoddyl.ResidualModelFrameTranslation( state, robot.model.getFrameId("mobile_base"), - # TODO: implement - #path["base"][i], # this better be done with the same time steps as the knots - # # NOTE: this should be done outside of this function to clarity - path[i], + path_base[i], state.nv) - goalTrackingCost = crocoddyl.CostModelResidual(state, framePlacementResidual) - runningCostModel.addCost("gripperPose" + str(i), goalTrackingCost, 1e2) + baseTrackingCost = crocoddyl.CostModelResidual(state, baseTranslationResidual) + runningCostModel.addCost("base_translation" + str(i), baseTrackingCost, 1e2) if args.solver == "boxfddp": runningModel = crocoddyl.IntegratedActionModelEuler( @@ -540,7 +541,8 @@ def createBaseAndEEPathFollowingOCP(args, robot : RobotManager, x0): 0.0, ) - terminalCostModel.addCost("gripperPose" + str(args.n_knots), goalTrackingCost, 1e2) + terminalCostModel.addCost("ee_pose" + str(args.n_knots), eeTrackingCost, 1e2) + terminalCostModel.addCost("base_translation" + str(args.n_knots), baseTrackingCost, 1e2) # now we define the problem problem = crocoddyl.ShootingProblem(x0, runningModels, terminalModel) diff --git a/python/ur_simple_control/path_generation/planner.py b/python/ur_simple_control/path_generation/planner.py index a93d6d84bc2ae6a3181310847a53d17edf420254..f48dca2fca2573f98152a4dfae84776c88ba14a6 100644 --- a/python/ur_simple_control/path_generation/planner.py +++ b/python/ur_simple_control/path_generation/planner.py @@ -463,14 +463,12 @@ def pathVisualizer(x0, goal, map_as_list, positions): def pathPointFromPathParam(n_pol, path_dim, path_pol, s): return [np.polyval(path_pol[j*(n_pol+1):(j+1)*(n_pol+1)], s) for j in range(path_dim)] -def path2D_to_timed_SE3(args, path_pol, path2D_untimed, max_base_v, path_height): +def path2D_timed(args, path_pol, path2D_untimed, max_base_v, path_height): """ - path2D_to_timed_SE3 + path2D_timed --------------------- - we have a 2D path of (N,2) shape as reference - the OCP accepts SE3 (it could just rotations or translation too), - so this function constructs it from the 2D path. - it also times it as this is what the current ocp formulation needs: + we have a 2D path of (N,2) shape as reference. + it times it as this is what the current ocp formulation needs: there should be a timing associated with the path, because defining the cost function as the fit of the rollout to the path is complicated to do software-wise. @@ -487,6 +485,11 @@ def path2D_to_timed_SE3(args, path_pol, path2D_untimed, max_base_v, path_height) trajectory here, it's a man-made heuristic, and we should leave that to the MPC, but that requires too much coding and there is no time rn. + the idea is to use compute the tangent of the path, + and use that to make a 2D frenet frame. + this is the put to some height, making it SE3. + i.e. roll and pitch are fixed to be 0, + but you could make them some other constant """ #################################################### @@ -530,28 +533,24 @@ def path2D_to_timed_SE3(args, path_pol, path2D_untimed, max_base_v, path_height) # what it should be but gets stuck if we're not yet on path #s = (i * args.ocp_dt) * t_to_s # full path + # NOTE: this should be wrong, and ocp_dt correct, + # but it works better for some reason xd s = i * (max_s / args.n_knots) #path2D.append(pathPointFromPathParam(args.n_pol, args.np, path_pol, s)) path2D.append(np.array([np.interp(s, s_vec, path2D_untimed[:,0]), - np.interp(s, s_vec, path2D_untimed[:,1])])) + np.interp(s, s_vec, path2D_untimed[:,1]), + path_height])) path2D = np.array(path2D) + return path2D - #################################################### - # constructing the SE3 reference from [x,y] path # - #################################################### - # 1) we need to add points so that there are args.n_knots of them - # the -1 is here because we're finite differencing to get theta, - # so we need one extra - #path2D = list(path2D) - ## in case there's too few - #while len(path2D) - 1 < args.n_knots: - # path2D.append(path2D[-1]) - ## in case there's too much - #while len(path2D) - 1> args.n_knots: - # path2D.pop() - #path2D = np.array(path2D) - - +def path2D_to_SE3(path2D, path_height): + """ + path2D_SE3 + ---------- + we have a 2D path of (N,2) shape as reference + the OCP accepts SE3 (it could just rotations or translation too), + so this function constructs it from the 2D path. + """ ######################### # path2D into pathSE2 # ######################### @@ -566,170 +565,36 @@ def path2D_to_timed_SE3(args, path_pol, path2D_untimed, max_base_v, path_height) # elementwise arctan2 thetas = np.arctan2(x_diff, y_diff) - #thetas = thetas.reshape((-1, 1)) - #path_SE2 = np.hstack((path2D, thetas)) - ###################################### # construct SE3 from SE2 reference # ###################################### # the plan is parallel to the ground because it's a mobile # manipulation task - # TODO: enforce timing, interpolate the path accordingly - path = [] + pathSE3 = [] for i in range(len(path2D) - 1): rotation = pin.rpy.rpyToMatrix(0.0, 0.0, thetas[i]) rotation = pin.rpy.rpyToMatrix(np.pi/2, np.pi/2,0.0) @ rotation #rotation = np.eye(3) translation = np.array([path2D[i][0], path2D[i][1], path_height]) - path.append(pin.SE3(rotation, translation)) - - return path + pathSE3.append(pin.SE3(rotation, translation)) + pathSE3.append(pin.SE3(rotation, translation)) + return pathSE3 +def path2D_to_timed_SE3(todo): + pass -def path2D_to_timed_SE3_base_and_ee(args, path_pol, path2D_untimed, max_base_v): - """ - path2D_to_timed_SE3 - --------------------- - we have a 2D path of (N,2) shape as reference. - from this we construct timed SE3 references for the cart, end-effector(s) and the mobile base, - in that order. this is done as follows. - 0) the path starts at the cart. we align the end-effector and the mobile base to be - ON this path as well. to we can follow the path, we need to make the path smooth enough. - that is considered a part of the planning problem. - 1) when the initial plan is set, the base-endeffector-cart system needs to be aligned with - the path. we can ensure AN initial alignment in the beginning when we grasp the cart. - furthermore, we can compute the initial path to see what the alignment should be. - then we can let either point-to-point clik or mpc align the bodies to this, - or let the controller-planner architecture run, - BUT then we need to test the stability w.r.t. initial point. - 2) the cart is a rigid body, so it doesn't matter which point of it is being referenced. - (one point localizes the whole rigid body because it's rigid). - we select the point(s) on the handlebar where we will perform the grasp. - 3) DUE to rigid grasping, the end-effector(s)'s reference is the same - as the one for the handlebar. thus we actually construct only 2 references from the 2D path. - the initial point of the path is this one. - 4) the path is of some length (it's a variable in the planner). we ensure it is long enough - to fit the base on it. this does mean that we can not GUARANTEE we avoid all obstacles, - because the path-planning guarantees this only for the initial path point. - 5) TODO for extra fun and profit: make an underactuated 2-point model in the path planner. - 6) timing is obtained from the maximum velocity of the robot and total path length. - this is a stupid heuristic. the time-scaling of the path should be a decision variable in the OCP. - 7) TODO for extra fun and profit: make time-scaling of the path a decision variable in the OCP. - """ - - #################################################### - # getting a timed 2D trajectory from a heuristic # - #################################################### - # the strategy is somewhat reasonable at least: - # assume we're moving at 90% max velocity in the base, - # and use that. - perc_of_max_v = 0.9 - base_v = perc_of_max_v * max_base_v - - # 1) the length of the path divided by 0.9 * max_vel - # gives us the total time of the trajectory, - # so we first compute that - # easiest possible way to get approximate path length - # (yes it should be from the polynomial approximation but that takes time to write) - x_i = path2D_untimed[:,0][:-1] # no last element - y_i = path2D_untimed[:,1][:-1] # no last element - x_i_plus_1 = path2D_untimed[:,0][1:] # no first element - y_i_plus_1 = path2D_untimed[:,1][1:] # no first element - x_diff = x_i_plus_1 - x_i - x_diff = x_diff.reshape((-1,1)) - y_diff = y_i_plus_1 - y_i - y_diff = y_diff.reshape((-1,1)) - path_length = np.sum(np.linalg.norm(np.hstack((x_diff, y_diff)), axis=1)) - total_time = path_length / base_v - # 2) we find the correspondence between s (path parameter) and time - # TODO: read from where max_s should be 5, but seba checked that it's 5 for some reason - max_s = 5 - t_to_s = max_s / total_time - # 3) construct the ocp-timed 2D path for the cart and for the mobile base of the robot - # TODO: tune this value!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!1 - # TODO: tune this value!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!1 - # TODO: tune this value!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!1 - # TODO make this an argument - # TODO: we need a function that takes time as input and spits -# need to have set interpolation to implement that - # out a path point of distance base_to_handlebar_prefered_distance from time 0. - # (it's the classic integral for curve length) - # cart_to_base_prefered_distance as percentage of path length * size of s (again why the fuck isn't s=1????) - handlebar_to_base_as_s = (args.base_to_handlebar_preferred_distance / path_length) * max_s - path2D_mobile_base = [] - path2D_cart = [] - # time is i * args.ocp_dt - for i in range(args.n_knots + 1): - s_cart = (i * args.ocp_dt) * t_to_s - # since we're pulling the robot is in front of the cart - s_mobile_base = s_cart + cart_to_base_as_s - path2D_cart.append(pathPointFromPathParam(args.n_pol, args.np, path_pol, s)) - path2D_cart = np.array(path2D_cart) - - - #################################################### - # constructing the SE3 reference from [x,y] path # - #################################################### - # 1) we need to add points so that there are args.n_knots of them - # the -1 is here because we're finite differencing to get theta, - # so we need one extra - #path2D = list(path2D) - ## in case there's too few - #while len(path2D) - 1 < args.n_knots: - # path2D.append(path2D[-1]) - ## in case there's too much - #while len(path2D) - 1> args.n_knots: - # path2D.pop() - #path2D = np.array(path2D) - - - ######################### - # path2D into pathSE2 # - ######################### - # construct theta - # since it's a pairwise operation it can't be done on the last point - x_i = path2D_cart[:,0][:-1] # no last element - y_i = path2D_cart[:,1][:-1] # no last element - x_i_plus_1 = path2D_cart[:,0][1:] # no first element - y_i_plus_1 = path2D_cart[:,1][1:] # no first element - x_diff = x_i_plus_1 - x_i - y_diff = y_i_plus_1 - y_i - # elementwise arctan2 - thetas = np.arctan2(x_diff, y_diff) - - - #thetas = thetas.reshape((-1, 1)) - #path_SE2 = np.hstack((path2D, thetas)) - - ###################################### - # construct SE3 from SE2 reference # - ###################################### - # the plan is parallel to the ground because it's a mobile - # manipulation task - # TODO: enforce timing, interpolate the path accordingly - path = [] - for i in range(len(path2D_cart) - 1): - rotation = pin.rpy.rpyToMatrix(0.0, 0.0, thetas[i]) - rotation = pin.rpy.rpyToMatrix(np.pi/2, np.pi/2,0.0) @ rotation - #rotation = np.eye(3) - translation = np.array([path2D_cart[i][0], path2D_cart[i][1], - args.handlebar_height]) - path.append(pin.SE3(rotation, translation)) - - return path - -# function to be put into ProcessManager, -# spitting out path points -#def starPlanner(goal, args, init_cmd, cmd_queue : Queue, data_queue : Queue): -# let's try shared memory def starPlanner(goal, args, init_cmd, shm_name, lock : Lock, shm_data): """ starPlanner ------------ - wild dark dynamical system magic done by albin, - with more dark magic on top to get it to spit + function to be put into ProcessManager, + spitting out path points. + it's wild dark dynamical system magic done by albin, + with software dark magic on top to get it to spit out path points and just the path points. - goal and path are [x,y] + goal and path are [x,y], + but there are utility functions to construct SE3 paths out of this + elsewhere in the library. """ # shm stuff shm = shared_memory.SharedMemory(name=shm_name) @@ -764,12 +629,19 @@ def starPlanner(goal, args, init_cmd, shm_name, lock : Lock, shm_data): # has to be a blocking call #cmd = cmd_queue.get() #p = cmd['p'] + # TODO: make a sendCommand type thing which will + # handle locking, pickling or numpy-arraying for you + # if for no other reason than not copy-pasting code lock.acquire() p[:] = p_shared[:] lock.release() if np.linalg.norm(p - goal) < convergence_threshold: - data_queue.put("done") + data_pickled = pickle.dumps("done") + lock.acquire() + shm_data.buf[:len(data_pickled)] = data_pickled + #shm_data.buf[len(data_pickled):] = bytes(0) + lock.release() break # Update the scene