diff --git a/examples/cart_pulling/base_and_ee_path_following_from_planner.py b/examples/cart_pulling/base_and_ee_path_following_from_planner.py index 54c7dfbc2a1427f7eb960c33437eb3584a30eb80..67da1a3069cfd4d05916edfad0b9d3c04b69da46 100644 --- a/examples/cart_pulling/base_and_ee_path_following_from_planner.py +++ b/examples/cart_pulling/base_and_ee_path_following_from_planner.py @@ -85,6 +85,7 @@ if __name__ == "__main__": p_base[2] = 0.0 print(pathSE3[0].translation) print(p_base) + # TODO: UNCOMMENT EEAndBaseP2PMPC(args, robot, pathSE3[0], p_base) print("initialized!") BaseAndEEPathFollowingMPC(args, robot, path_planner) diff --git a/python/smc/control/controller_templates/path_following_template.py b/python/smc/control/controller_templates/path_following_template.py index 4dd851c1f036606f5d57215d1989132a325ca01d..e7c815ea6f817ad48c87bec343400cb2b8dcd313 100644 --- a/python/smc/control/controller_templates/path_following_template.py +++ b/python/smc/control/controller_templates/path_following_template.py @@ -5,6 +5,7 @@ from pinocchio import SE3, log6 from argparse import Namespace from typing import Any, Callable import numpy as np +from collections import deque global control_loop_return control_loop_return = tuple[bool, dict[str, np.ndarray], dict[str, np.ndarray]] @@ -13,21 +14,28 @@ control_loop_return = tuple[bool, dict[str, np.ndarray], dict[str, np.ndarray]] def PathFollowingFromPlannerControlLoop( path_planner: ProcessManager, get_position: Callable[[AbstractRobotManager], np.ndarray], - SOLVER: Callable, + SOLVER: Any, control_loop: Callable[ - [Any, np.ndarray, Namespace, AbstractRobotManager, dict[str, np.ndarray], int], - np.ndarray, + [ + Any, + np.ndarray, + Namespace, + AbstractRobotManager, + int, + dict[str, deque[np.ndarray]], + ], + tuple[np.ndarray, dict[str, np.ndarray]], ], args: Namespace, robot: AbstractRobotManager, - past_data: dict[str, np.ndarray], t: int, # will be float eventually + past_data: dict[str, deque[np.ndarray]], ) -> control_loop_return: """ - Point2PointControlLoop + PathFollowingFromPlannerControlLoop --------------- - generic control loop for point to point motion - (handling error to final point etc). + handles getting path and with comm with the planner. + then you do whatever you want with the path, and execute a path following controller """ breakFlag = False log_item = {} @@ -51,12 +59,13 @@ def PathFollowingFromPlannerControlLoop( path2D_untimed = np.array(path2D_untimed).reshape((-1, 2)) v_cmd, log_item_inner = control_loop( - SOLVER, path2D_untimed, args, robot, past_data, t + SOLVER, path2D_untimed, args, robot, t, past_data ) robot.sendVelocityCommand(v_cmd) log_item.update(log_item_inner) log_item["qs"] = robot.q log_item["dqs"] = robot.v - log_item["dqs_cmd"] = v_cmd.reshape((robot.model.nv,)) + # NOTE: shouldn't be here + save_past_item["path2D_untimed"] = p return breakFlag, save_past_item, log_item diff --git a/python/smc/control/optimal_control/croco_mpc_path_following.py b/python/smc/control/optimal_control/croco_mpc_path_following.py index e6500f3f409034b0ad17062e7dc30b2b2d1d1843..134e942ea42cec649bd7e89973035cd37b773b1a 100644 --- a/python/smc/control/optimal_control/croco_mpc_path_following.py +++ b/python/smc/control/optimal_control/croco_mpc_path_following.py @@ -11,16 +11,19 @@ from smc.path_generation.path_math.path2d_to_6d import ( path2D_to_SE3_fixed, ) from smc.path_generation.path_math.cart_pulling_path_math import ( - time_base_path, construct_EE_path, ) from smc.path_generation.path_math.path_to_trajectory import path2D_timed +from smc.control.controller_templates.path_following_template import ( + PathFollowingFromPlannerControlLoop, +) import numpy as np from functools import partial import types from argparse import Namespace from pinocchio import SE3, log6 +from collections import deque def CrocoEEPathFollowingMPCControlLoop( @@ -47,10 +50,10 @@ def CrocoEEPathFollowingMPCControlLoop( q = robot.q T_w_e = robot.T_w_e - pathSE3 = path_planner(T_w_e, t) + path_EE_SE3 = path_planner(T_w_e, t) x0 = np.concatenate([robot.q, robot.v]) - ocp.warmstartAndReSolve(x0, data=pathSE3) + ocp.warmstartAndReSolve(x0, data=path_EE_SE3) xs = np.array(ocp.solver.xs) us = np.array(ocp.solver.us) vel_cmds = xs[1, robot.model.nq :] @@ -60,21 +63,23 @@ def CrocoEEPathFollowingMPCControlLoop( # TODO: EVIL AND HAS TO BE REMOVED FROM HERE if args.visualizer: if t % int(np.ceil(args.ctrl_freq / 25)) == 0: - robot.visualizer_manager.sendCommand({"frame_path": pathSE3}) + robot.visualizer_manager.sendCommand({"frame_path": path_EE_SE3}) + err_vector_ee = log6(robot.T_w_e.actInv(path_EE_SE3[0])) + log_item["err_vec_ee"] = err_vector_ee log_item["qs"] = q.reshape((robot.model.nq,)) log_item["dqs"] = robot.v.reshape((robot.model.nv,)) return breakFlag, save_past_item, log_item def CrocoEEPathFollowingFromPlannerMPCControlLoop( - args, - robot: SingleArmInterface, ocp: CrocoOCP, - path_planner: ProcessManager, - t, - _, -): + path2D_untimed: np.ndarray, + args: Namespace, + robot: SingleArmInterface, + t: int, + _: dict[str, np.ndarray], +) -> tuple[np.ndarray, dict[str, np.ndarray]]: """ CrocoPathFollowingMPCControlLoop ----------------------------- @@ -84,56 +89,28 @@ def CrocoEEPathFollowingFromPlannerMPCControlLoop( or an instance of ProcessManager which spits out path points by calling ProcessManager.getData() """ - breakFlag = False - log_item = {} - save_past_item = {} - - q = robot.q - T_w_e = robot.T_w_e - p = T_w_e.translation[:2] - - # NOTE: it's pointless to recalculate the path every time - # if it's the same 2D path - - path_planner.sendCommand(p) - data = path_planner.getData() - - if data == "done": - breakFlag = True - - if data == "done" or data is None: - robot.sendVelocityCommand(np.zeros(robot.model.nv)) - log_item["qs"] = q - log_item["dqs"] = robot.v - return breakFlag, save_past_item, log_item - - _, path2D_untimed = data - path2D_untimed = np.array(path2D_untimed).reshape((-1, 2)) - # who cares if the velocity is right, - # it should be kinda right so that we need less ocp iterations - # and that's it max_base_v = np.linalg.norm(robot._max_v[:2]) path2D = path2D_timed(args, path2D_untimed, max_base_v) # create a 3D reference out of the path - pathSE3 = path2D_to_SE3_fixed(path2D, args.handlebar_height) + path_EE_SE3 = path2D_to_SE3_fixed(path2D, args.handlebar_height) + for pose in path_EE_SE3: + print(pose.translation) # TODO: EVIL AND HAS TO BE REMOVED FROM HERE if args.visualizer: if t % int(np.ceil(args.ctrl_freq / 25)) == 0: - robot.visualizer_manager.sendCommand({"frame_path": pathSE3}) + robot.visualizer_manager.sendCommand({"frame_path": path_EE_SE3}) x0 = np.concatenate([robot.q, robot.v]) - ocp.warmstartAndReSolve(x0, data=pathSE3) + ocp.warmstartAndReSolve(x0, data=path_EE_SE3) xs = np.array(ocp.solver.xs) - us = np.array(ocp.solver.us) - vel_cmds = xs[1, robot.model.nq :] + v_cmd = xs[1, robot.model.nq :] - robot.sendVelocityCommand(vel_cmds) + err_vector_ee = log6(robot.T_w_e.actInv(path_EE_SE3[0])) + log_item = {"err_vec_ee": err_vector_ee} - log_item["qs"] = q.reshape((robot.model.nq,)) - log_item["dqs"] = robot.v.reshape((robot.model.nv,)) - return breakFlag, save_past_item, log_item + return v_cmd, log_item def CrocoEEPathFollowingMPC( @@ -161,14 +138,21 @@ def CrocoEEPathFollowingMPC( CrocoEEPathFollowingMPCControlLoop, args, robot, ocp, path_planner ) else: + get_position = lambda robot: robot.T_w_e.translation[:2] controlLoop = partial( + PathFollowingFromPlannerControlLoop, + path_planner, + get_position, + ocp, CrocoEEPathFollowingFromPlannerMPCControlLoop, args, robot, - ocp, - path_planner, ) - log_item = {"qs": np.zeros(robot.model.nq), "dqs": np.zeros(robot.model.nv)} + log_item = { + "qs": np.zeros(robot.model.nq), + "dqs": np.zeros(robot.model.nv), + "err_vec_ee": np.zeros(6), + } save_past_item = {} loop_manager = ControlLoopManager( robot, controlLoop, args, save_past_item, log_item @@ -213,8 +197,9 @@ def BaseAndEEPathFollowingMPCControlLoop( robot.visualizer_manager.sendCommand({"path": path_base}) robot.visualizer_manager.sendCommand({"frame_path": path_EE}) - err_vector_ee = T_w_e.actInv(path_EE[0]) + err_vector_ee = log6(T_w_e.actInv(path_EE[0])) err_vector_base = np.linalg.norm(q[:2] - path_base[0][:2]) # z axis is irrelevant + log_item["err_vec_ee"] = err_vector_ee log_item["err_norm_ee"] = np.linalg.norm(err_vector_ee).reshape((1,)) log_item["err_norm_base"] = np.linalg.norm(err_vector_base).reshape((1,)) log_item["qs"] = q.reshape((robot.model.nq,)) @@ -223,63 +208,35 @@ def BaseAndEEPathFollowingMPCControlLoop( def BaseAndEEPathFollowingFromPlannerMPCControlLoop( - args, - robot, ocp: CrocoOCP, - path_planner: ProcessManager, - t, - past_data, -): - """ - CrocoPathFollowingMPCControlLoop - ----------------------------- - end-effector(s) follow their path(s). - - path planner can either be a function which spits out a list of path points - or an instance of ProcessManager which spits out path points - by calling ProcessManager.getData() - """ - breakFlag = False - log_item = {} - save_past_dict = {} - - q = robot.q - T_w_e = robot.T_w_e - p = q[:2] - # 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.sendCommand(p) + path2D_untimed_base: np.ndarray, + args: Namespace, + robot: SingleArmInterface, + t: int, + past_data: dict[str, deque[np.ndarray]], +) -> tuple[np.ndarray, dict[str, np.ndarray]]: + p = robot.q[:2] - ########################### - # get path from planner # - ########################### - data = path_planner.getData() + # NOTE: this one is obtained as the future path from path planner + max_base_v = np.linalg.norm(robot._max_v[:2]) + path_base = path2D_timed(args, path2D_untimed_base, max_base_v) + path_base = np.hstack((path_base, np.zeros((len(path_base), 1)))) - if data == "done": - breakFlag = True - if data == "done" or data is None: - robot.sendVelocityCommand(np.zeros(robot.model.nv)) - log_item["qs"] = q.reshape((robot.model.nq,)) - log_item["dqs"] = robot.v.reshape((robot.model.nv,)) - return breakFlag, save_past_dict, log_item + pathSE3_handlebar = construct_EE_path(args, p, past_data["path2D_untimed"]) - ########################################## - # construct timed 2D path for the base # - ########################################## - _, path2D_untimed_base = data - path_base = time_base_path( - args, path2D_untimed_base, np.linalg.norm(robot._max_v[:2]) - ) + #print("BASEcurrent_position", p) + #print("EEcurrent_position", robot.T_w_e.translation) + #print("=" * 5, "desired handlebar traj", "=" * 5) + #for pose in pathSE3_handlebar: + # print(pose.translation) - pathSE3_handlebar = construct_EE_path(args, p, past_data["path2D_untimed"]) + ########################################### # TODO: remove # SETTING ROTATION IS BROKEN ATM # BUT SETTING DISTANCES (TRANSLATIONS) IS TOO. # WE DEAL WITH DISTANCES FIRST, UNTIL THAT'S DONE THIS STAYS HERE for ppp in pathSE3_handlebar: - ppp.rotation = T_w_e.rotation + ppp.rotation = robot.T_w_e.rotation ########################################### if args.visualizer: @@ -290,19 +247,15 @@ def BaseAndEEPathFollowingFromPlannerMPCControlLoop( x0 = np.concatenate([robot.q, robot.v]) ocp.warmstartAndReSolve(x0, data=(path_base, pathSE3_handlebar)) xs = np.array(ocp.solver.xs) - us = np.array(ocp.solver.us) - vel_cmds = xs[1, robot.model.nq :] + v_cmd = xs[1, robot.model.nq :] - robot.sendVelocityCommand(vel_cmds) - - err_vector_ee = log6(T_w_e.actInv(pathSE3_handlebar[0])) - err_vector_base = np.linalg.norm(q[:2] - path_base[0][:2]) # z axis is irrelevant + err_vector_ee = log6(robot.T_w_e.actInv(pathSE3_handlebar[0])) + err_vector_base = np.linalg.norm(p - path_base[0][:2]) # z axis is irrelevant + log_item = {} log_item["err_vec_ee"] = err_vector_ee log_item["err_norm_ee"] = np.linalg.norm(err_vector_ee).reshape((1,)) log_item["err_norm_base"] = np.linalg.norm(err_vector_base).reshape((1,)) - log_item["qs"] = q.reshape((robot.model.nq,)) - # log_item["dqs"] = robot.v.reshape((robot.model.nv,)) - return breakFlag, save_past_dict, log_item + return v_cmd, log_item def initializePastData( @@ -312,19 +265,8 @@ def initializePastData( # (which didn't actually happen because this just started) p_ee = T_w_e.translation[:2] straight_line_path = np.linspace(p_ee, p_base, args.past_window_size) - straight_line_path_timed = path2D_timed(args, straight_line_path, max_base_v) - # time it the same way the base path is timed - # time_past = np.linspace( - # 0.0, args.past_window_size * (1 / args.ctrl_freq), args.past_window_size - # ) - # s = np.linspace(0.0, args.n_knots * args.ocp_dt, args.past_window_size) - # path2D_handlebar = np.hstack( - # ( - # np.interp(s, time_past, straight_line_path[:, 0]).reshape((-1, 1)), - # np.interp(s, time_past, straight_line_path[:, 1]).reshape((-1, 1)), - # ) - # ) - # return path2D_handlebar + # straight_line_path_timed = path2D_timed(args, straight_line_path, max_base_v) + # return straight_line_path_timed # this one is shortened to args.n_knots! and we want the whole buffer return straight_line_path @@ -356,16 +298,19 @@ def BaseAndEEPathFollowingMPC( BaseAndEEPathFollowingMPCControlLoop, args, robot, ocp, path_planner ) else: + get_position = lambda robot: robot.q[:2] controlLoop = partial( + PathFollowingFromPlannerControlLoop, + path_planner, + get_position, + ocp, BaseAndEEPathFollowingFromPlannerMPCControlLoop, args, robot, - ocp, - path_planner, ) log_item = { "qs": np.zeros(robot.model.nq), - # "dqs": np.zeros(robot.model.nv), + "dqs": np.zeros(robot.model.nv), "err_vec_ee": np.zeros((6,)), "err_norm_ee": np.zeros((1,)), "err_norm_base": np.zeros((1,)), @@ -522,7 +467,7 @@ def BaseAndDualArmEEPathFollowingMPCControlLoop( return breakFlag, save_past_dict, log_item -def BaseAndDualARmEEPathFollowingMPC(args, robot, path_planner): +def BaseAndDualArmEEPathFollowingMPC(args, robot, path_planner): """ CrocoEndEffectorPathFollowingMPC ----- diff --git a/python/smc/path_generation/path_math/cart_pulling_path_math.py b/python/smc/path_generation/path_math/cart_pulling_path_math.py index 499fc63baa86c3ea7ce00da1f31a6e8ac79a2a15..c3c1f12443b98898435f96608a19554c81f7a4e2 100644 --- a/python/smc/path_generation/path_math/cart_pulling_path_math.py +++ b/python/smc/path_generation/path_math/cart_pulling_path_math.py @@ -57,18 +57,6 @@ def getCurrentPositionHandlebarInPastBasePath( return handlebar_path_index -def time_base_path( - args: Namespace, path2D_untimed_base, max_base_v: float -) -> np.ndarray: - # NOTE: this one is obtained as the future path from path planner - path2D_untimed_base = np.array(path2D_untimed_base).reshape((-1, 2)) - # ideally should be precomputed somewhere - path_base = path2D_timed(args, path2D_untimed_base, max_base_v) - # and it's of height 0 (i.e. the height of base's planar joint) - path_base = np.hstack((path_base, np.zeros((len(path_base), 1)))) - return path_base - - # NOTE: extremely inefficient. # there should be no copy-pasting of the whole path at every single point in time, # instead all the computations should be cached. @@ -94,7 +82,7 @@ def construct_EE_path( 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. + 2) construct timing on the first part. (robot.dt =/= ocp_dt) 3) (optional) join that with the already timed second part. --> not doing this because paths are super short and this won't ever happen with the current setup 4) turn the timed 2D path into an SE3 trajectory @@ -114,6 +102,8 @@ def construct_EE_path( """ # i shouldn't need to copy-paste everything but what can you do past_path2D = np.array(past_path2D_from_window).reshape((-1, 2)) + # print("-" * 5, "past_window", "-" * 5) + # print(past_path2D) # step (1) handlebar_path_index = getCurrentPositionHandlebarInPastBasePath( args, p_base_current, past_path2D diff --git a/python/smc/path_generation/path_math/path_to_trajectory.py b/python/smc/path_generation/path_math/path_to_trajectory.py index 15d9fdd5e5193d93aea0212cb6a312cb839adefc..f07958693afdf1788eb9baa7651bd4d659b38a1a 100644 --- a/python/smc/path_generation/path_math/path_to_trajectory.py +++ b/python/smc/path_generation/path_math/path_to_trajectory.py @@ -53,9 +53,12 @@ def path2D_timed(args, path2D_untimed, max_base_v): 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)) - print(path_length) + # NOTE: sometimes the path planner gives me the same god damn points + # and that's it. can't do much about, expect set those point then. + # and i need to return now so that the math doesn't break down the line + if path_length < 1e-3: + return np.ones((args.n_knots + 1, 2)) * path2D_untimed[0] total_time = path_length / base_v - print(total_time) # 2) we find the correspondence between s and time # TODO: read from where it should be, but seba checked that it's 5 for some reason # TODO THIS IS N IN PATH PLANNING, MAKE THIS A SHARED ARGUMENT