diff --git a/examples/cart_pulling/MASSIVE_TODO b/examples/cart_pulling/MASSIVE_TODO new file mode 100644 index 0000000000000000000000000000000000000000..eecf06f159f04179c22031c7d0ed83b9b86d6ad8 --- /dev/null +++ b/examples/cart_pulling/MASSIVE_TODO @@ -0,0 +1,8 @@ +1. you have to find a way to keep track of the path for the end-effector. one trillion percent it manages to break itself, you need to save it in a log and replay everything. make change to the log_item thing to make not everything automatically plottable (check value type or something) + --> preferably do that while running the disjoint control because it survives the hassle +2. make proper path-tracking cost in python, that's shouldn't be too much of a hassle + 2.1. do it with numdiff first + 2.2. put in the analytic derivative +3. put in progressively higher goal tracking costs. +4. dynamically change the costs - if the end-effector is too fucked, track just the first point while fixing the base, whatever. +5. re-initialize previous path if the error is too big. diff --git a/examples/pinocchio_math_understanding/slerp.py b/examples/pinocchio_math_understanding/slerp.py new file mode 100644 index 0000000000000000000000000000000000000000..417e167ef69f8681b4faad16b1780b7e1a91978b --- /dev/null +++ b/examples/pinocchio_math_understanding/slerp.py @@ -0,0 +1,30 @@ +from smc.visualization.meshcat_viewer_wrapper.visualizer import MeshcatVisualizer + +import time +import pinocchio as pin +import numpy as np +import meshcat_shapes + +A = pin.SE3.Random() +A.translation /= 2 +B = pin.SE3.Random() +B.translation /= 2 + +interpolated = [] +t_line = np.linspace(0, 1, 20) +for t in t_line: + interpolated.append(pin.SE3.Interpolate(A, B, t)) + +visualizer = MeshcatVisualizer() +meshcat_shapes.frame(visualizer.viewer["A"], opacity=0.5) +meshcat_shapes.frame(visualizer.viewer["B"], opacity=0.5) +# meshcat_shapes.frame(visualizer.viewer["C"], opacity=0.5) +visualizer.viewer["A"].set_transform(A.homogeneous) +visualizer.viewer["B"].set_transform(B.homogeneous) +# visualizer.viewer["C"].set_transform(C.homogeneous) + +visualizer.addFramePath("", interpolated) +visualizer.addFramePath("", interpolated) + + +time.sleep(100) diff --git a/python/smc/control/optimal_control/abstract_croco_ocp.py b/python/smc/control/optimal_control/abstract_croco_ocp.py index 2aa452fdcb2d1581f55abbba029dbc45b213db8a..e2551af9dd86faa346d107d208d10fe1492a943b 100644 --- a/python/smc/control/optimal_control/abstract_croco_ocp.py +++ b/python/smc/control/optimal_control/abstract_croco_ocp.py @@ -84,10 +84,11 @@ class CrocoOCP(abc.ABC): # put these costs into the running costs for i in range(self.args.n_knots): - self.runningCostModels[i].addCost("xReg", self.xRegCost, 1e-3) - self.runningCostModels[i].addCost("uReg", self.uRegCost, 1e-3) + self.runningCostModels[i].addCost("xReg", self.xRegCost, self.args.u_reg_cost) + self.runningCostModels[i].addCost("uReg", self.uRegCost, self.args.x_reg_cost) # and add the terminal cost, which is the distance to the goal # NOTE: shouldn't there be a final velocity = 0 here? + # --> no if you're not stopping at the last knot! self.terminalCostModel.addCost("uReg", self.uRegCost, 1e3) def constructStateLimits(self) -> None: 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 d163fb6f5e83a88aab11f7e9f7589b0fb2ef631b..5afb4b5021852fe7d0dd599aaf2225a0f4f52a0a 100644 --- a/python/smc/control/optimal_control/croco_mpc_path_following.py +++ b/python/smc/control/optimal_control/croco_mpc_path_following.py @@ -166,8 +166,8 @@ def CrocoEEPathFollowingFromPlannerMPCControlLoop( # create a 3D reference out of the path path_EE_SE3 = path2D_to_SE3_fixed(path2D, args.handlebar_height) - for pose in path_EE_SE3: - print(pose.translation) +# for pose in path_EE_SE3: +# print(pose.translation) # TODO: EVIL AND HAS TO BE REMOVED FROM HERE if args.visualizer: @@ -307,8 +307,8 @@ def BaseAndEEPathFollowingFromPlannerMPCControlLoop( # 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 = robot.T_w_e.rotation + #for ppp in pathSE3_handlebar: + # ppp.rotation = robot.T_w_e.rotation ########################################### if args.visualizer: diff --git a/python/smc/control/optimal_control/util.py b/python/smc/control/optimal_control/util.py index 177b2cf490781465d61cf999a40efd06aaa6235c..16b220bd6e8bd3f31285d74b18644e6ed3de6a81 100644 --- a/python/smc/control/optimal_control/util.py +++ b/python/smc/control/optimal_control/util.py @@ -1,19 +1,52 @@ import argparse -def get_OCP_args(parser : argparse.ArgumentParser): - parser.add_argument("--ocp-dt", type=float, default=1e-2, \ - help="time length between state-control-pair decision variables") - parser.add_argument("--n-knots", type=int, default=100, \ - help="number of state-control-pair decision variables") - parser.add_argument("--stop-at-final", type=int, default=1, \ - help="the trajectory generated by the OCP will be followed. it might not have finally velocity 0. \ - if this argument is set to true, the final velocity will be set to 0 (there will be overshoot).") - parser.add_argument("--solver", type=str, default="boxfddp", \ - help="optimal control problem solver you want", choices=["boxfddp", "csqp"]) - parser.add_argument("--max-solver-iter", type=int, default=200, \ - help="number of iterations the ocp solver takes. ~100-500 for just ocp (it can converge before ofc), ~10 for mpc") - parser.add_argument("--ee-pose-cost", type=float, default=1e2, \ - help="ee pose cost in pulling mpc") - parser.add_argument("--base-translation-cost", type=float, default=1e2, \ - help="base translation cost in pulling mpc") + +def get_OCP_args(parser: argparse.ArgumentParser): + parser.add_argument( + "--ocp-dt", + type=float, + default=1e-2, + help="time length between state-control-pair decision variables", + ) + parser.add_argument( + "--n-knots", + type=int, + default=100, + help="number of state-control-pair decision variables", + ) + parser.add_argument( + "--stop-at-final", + type=int, + default=1, + help="the trajectory generated by the OCP will be followed. it might not have finally velocity 0. \ + if this argument is set to true, the final velocity will be set to 0 (there will be overshoot).", + ) + parser.add_argument( + "--solver", + type=str, + default="boxfddp", + help="optimal control problem solver you want", + choices=["boxfddp", "csqp"], + ) + parser.add_argument( + "--max-solver-iter", + type=int, + default=200, + help="number of iterations the ocp solver takes. ~100-500 for just ocp (it can converge before ofc), ~10 for mpc", + ) + parser.add_argument( + "--ee-pose-cost", type=float, default=1e2, help="ee pose cost in pulling mpc" + ) + parser.add_argument( + "--base-translation-cost", + type=float, + default=1e2, + help="base translation cost in pulling mpc", + ) + parser.add_argument( + "--x-reg-cost", type=float, default=1e-3, help="state regulation cost" + ) + parser.add_argument( + "--u-reg-cost", type=float, default=1e-3, help="control input regulation cost" + ) return parser diff --git a/python/smc/robots/implementations/heron.py b/python/smc/robots/implementations/heron.py index 2f740df9eee9ded6fc01d93a577a0a0229821d96..b4a9edb936082608516171a8433dcc84d75aa94f 100644 --- a/python/smc/robots/implementations/heron.py +++ b/python/smc/robots/implementations/heron.py @@ -194,7 +194,9 @@ def heron_approximation(): # TODO: look what's done in pink, see if this can be usable # after you've removed camera joint and similar. # NOTE: NOT USED, BUT STUFF WILL NEED TO BE EXTRACTED FROM THIS EVENTUALLY -def get_heron_model_from_full_urdf(): +def get_heron_model_from_full_urdf() -> ( + tuple[pin.Model, pin.GeometryModel, pin.GeometryModel, pin.Data] +): # urdf_path_relative = files('smc.robot_descriptions.urdf').joinpath('ur5e_with_robotiq_hande_FIXED_PATHS.urdf') urdf_path_absolute = "/home/gospodar/home2/gospodar/lund/praxis/software/ros/ros-containers/home/model.urdf" diff --git a/python/smc/robots/implementations/mobile_yumi.py b/python/smc/robots/implementations/mobile_yumi.py index 1842c64bf561760e91401d87f67d809ed5760dcc..40adca68c57d737e610d19cbc7eaed3966fb373c 100644 --- a/python/smc/robots/implementations/mobile_yumi.py +++ b/python/smc/robots/implementations/mobile_yumi.py @@ -1,5 +1,12 @@ from smc.robots.robotmanager_abstract import AbstractRobotManager from smc.robots.interfaces.dual_arm_interface import DualArmInterface +from smc.robots.interfaces.mobile_base_interface import ( + MobileBaseInterface, + get_mobile_base_model, +) +from smc.robots.implementations.yumi import get_yumi_model + +import pinocchio as pin class MobileYumiRobotManager(DualArmInterface): @@ -42,84 +49,12 @@ class RealMobileYumiRobotManager(MobileYumiRobotManager): self.publisher_joints_cmd.publish(msg) -def get_yumi_model(): - - urdf_path_relative = files("smc.robots.robot_descriptions").joinpath("yumi.urdf") - urdf_path_absolute = os.path.abspath(urdf_path_relative) - # mesh_dir = files('smc') - # mesh_dir_absolute = os.path.abspath(mesh_dir) - # mesh_dir_absolute = "/home/gospodar/lund/praxis/software/ros/ros-containers/home/heron_description/MIR_robot" - mesh_dir_absolute = None - - model = None - collision_model = None - visual_model = None - # this command just calls the ones below it. both are kept here - # in case pinocchio people decide to change their api. - # model, collision_model, visual_model = pin.buildModelsFromUrdf(urdf_path_absolute, mesh_dir_absolute) - model_arms = pin.buildModelFromUrdf(urdf_path_absolute) - visual_model_arms = pin.buildGeomFromUrdf( - model_arms, urdf_path_absolute, pin.GeometryType.VISUAL, None, mesh_dir_absolute - ) - collision_model_arms = pin.buildGeomFromUrdf( - model_arms, - urdf_path_absolute, - pin.GeometryType.COLLISION, - None, - mesh_dir_absolute, - ) - - data_arms = pin.Data(model_arms) +def get_mobile_yumi_model() -> ( + tuple[pin.Model, pin.GeometryModel, pin.GeometryModel, pin.Data] +): - # mobile base as planar joint (there's probably a better - # option but whatever right now) - model_mobile_base = pin.Model() - model_mobile_base.name = "mobile_base" - geom_model_mobile_base = pin.GeometryModel() - joint_name = "mobile_base_planar_joint" - parent_id = 0 - # TEST - joint_placement = pin.SE3.Identity() - # joint_placement.rotation = pin.rpy.rpyToMatrix(0, -np.pi/2, 0) - # joint_placement.translation[2] = 0.2 - MOBILE_BASE_JOINT_ID = model_mobile_base.addJoint( - parent_id, pin.JointModelPlanar(), joint_placement.copy(), joint_name - ) - # we should immediately set velocity limits. - # there are no position limit by default and that is what we want. - model_mobile_base.velocityLimit[0] = 2 - model_mobile_base.velocityLimit[1] = 2 - model_mobile_base.velocityLimit[2] = 2 - model_mobile_base.effortLimit[0] = 200 - model_mobile_base.effortLimit[1] = 2 - model_mobile_base.effortLimit[2] = 200 - - # pretty much random numbers - # TODO: find heron (mir) numbers - body_inertia = pin.Inertia.FromBox(30, 0.5, 0.3, 0.4) - # maybe change placement to sth else depending on where its grasped - model_mobile_base.appendBodyToJoint( - MOBILE_BASE_JOINT_ID, body_inertia, pin.SE3.Identity() - ) - box_shape = fcl.Box(0.5, 0.3, 0.4) - body_placement = pin.SE3.Identity() - geometry_mobile_base = pin.GeometryObject( - "box_shape", MOBILE_BASE_JOINT_ID, box_shape, body_placement.copy() - ) - - geometry_mobile_base.meshColor = np.array([1.0, 0.1, 0.1, 1.0]) - geom_model_mobile_base.addGeometryObject(geometry_mobile_base) - - # have to add the frame manually - model_mobile_base.addFrame( - pin.Frame( - "mobile_base", - MOBILE_BASE_JOINT_ID, - 0, - joint_placement.copy(), - pin.FrameType.JOINT, - ) - ) + model_arms, visual_model_arms, collision_model_arms, _ = get_yumi_model() + model_mobile_base, geom_model_mobile_base = get_mobile_base_model(False) # frame-index should be 1 model, visual_model = pin.appendModel( diff --git a/python/smc/robots/implementations/ur5e.py b/python/smc/robots/implementations/ur5e.py index d50ce5de8b0e343d16d427c5d9a2fd070ecf4f4d..7a3ec9853e16a0202e706c047adc94a0887ad26a 100644 --- a/python/smc/robots/implementations/ur5e.py +++ b/python/smc/robots/implementations/ur5e.py @@ -185,7 +185,7 @@ class RealUR5eRobotManager(RobotManagerUR5e, AbstractRealRobotManager): raise NotImplementedError("freedrive function only written for ur5e") -def get_model(): +def get_model()-> tuple[pin.Model, pin.GeometryModel, pin.GeometryModel, pin.Data]: urdf_path_relative = files("smc.robots.robot_descriptions.urdf").joinpath( "ur5e_with_robotiq_hande_FIXED_PATHS.urdf" diff --git a/python/smc/robots/implementations/yumi.py b/python/smc/robots/implementations/yumi.py new file mode 100644 index 0000000000000000000000000000000000000000..b96ac6939988c210ab3b253e677a35d8dc7d9c42 --- /dev/null +++ b/python/smc/robots/implementations/yumi.py @@ -0,0 +1,63 @@ +from smc.robots.interfaces.dual_arm_interface import DualArmInterface + +from importlib.resources import files +from os import path +import pinocchio as pin + + +class RobotManagerYuMi(DualArmInterface): + + @property + def model(self): + return self._model + + @property + def data(self): + return self._data + + @property + def visual_model(self): + return self._visual_model + + @property + def collision_model(self): + return self._collision_model + + def __init__(self, args): + if args.debug_prints: + print("RobotManagerUR5e init") + self._model, self._collision_model, self._visual_model, self._data = get_model() + self._ee_frame_id = self.model.getFrameId("tool0") + self._MAX_ACCELERATION = 1.7 # const + self._MAX_QD = 3.14 # const + super().__init__(args) + + +def get_yumi_model() -> ( + tuple[pin.Model, pin.GeometryModel, pin.GeometryModel, pin.Data] +): + + urdf_path_relative = files("smc.robots.robot_descriptions").joinpath("yumi.urdf") + urdf_path_absolute = path.abspath(urdf_path_relative) + # mesh_dir = files('smc') + # mesh_dir_absolute = os.path.abspath(mesh_dir) + # mesh_dir_absolute = "/home/gospodar/lund/praxis/software/ros/ros-containers/home/heron_description/MIR_robot" + mesh_dir_absolute = None + + # this command just calls the ones below it. both are kept here + # in case pinocchio people decide to change their api. + # model, collision_model, visual_model = pin.buildModelsFromUrdf(urdf_path_absolute, mesh_dir_absolute) + model = pin.buildModelFromUrdf(urdf_path_absolute) + visual_model = pin.buildGeomFromUrdf( + model, urdf_path_absolute, pin.GeometryType.VISUAL, None, mesh_dir_absolute + ) + collision_model = pin.buildGeomFromUrdf( + model, + urdf_path_absolute, + pin.GeometryType.COLLISION, + None, + mesh_dir_absolute, + ) + + data = pin.Data(model) + return model, collision_model, visual_model, data diff --git a/python/smc/robots/interfaces/dual_arm_interface.py b/python/smc/robots/interfaces/dual_arm_interface.py index e9b0d605394cf669a3aa2c03387b4012e159f6e7..f93465e25015fd6390e72073570ee29c656aeb24 100644 --- a/python/smc/robots/interfaces/dual_arm_interface.py +++ b/python/smc/robots/interfaces/dual_arm_interface.py @@ -1,11 +1,12 @@ -from smc.robots.robotmanager_abstract import AbstractRobotManager +from smc.robots.interfaces.single_arm_interface import SingleArmInterface import abc import numpy as np import pinocchio as pin +from argparse import Namespace -class DualArmInterface(AbstractRobotManager): +class DualArmInterface(SingleArmInterface): """ DualArmInterface ---------------- @@ -18,12 +19,37 @@ class DualArmInterface(AbstractRobotManager): # (better than putting in a magic number) # self._l_ee_frame_id = self.model.getFrameId("left_ee_name") # self._r_ee_frame_id = self.model.getFrameId("right_ee_name") + + def __init__(self, args: Namespace): + self._T_w_abs: pin.SE3 + self._T_w_l: pin.SE3 + self._T_w_r: pin.SE3 + self._T_l_r: pin.SE3 + self._l_ee_frame_id: int + self._r_ee_frame_id: int + if args.debug_prints: + print("DualArmInterface init") + super().__init__(args) + @property - @abc.abstractmethod - def l_ee_frame_id(self) -> int: ... + def T_w_e(self): + return self.T_w_abs + @property - @abc.abstractmethod - def r_ee_frame_id(self) -> int: ... + def T_w_l(self): + return self._T_w_l.copy() + + @property + def l_ee_frame_id(self) -> int: + return self._l_ee_frame_id + + @property + def T_w_r(self): + return self._T_w_r.copy() + + @property + def r_ee_frame_id(self) -> int: + return self._r_ee_frame_id # T_abs_l and T_abs_r are relative transformations between the absolute dual-arm frame, # and the left and right end-effector frames respectively @@ -34,29 +60,28 @@ class DualArmInterface(AbstractRobotManager): @abc.abstractmethod def T_abs_l(self) -> pin.SE3: ... - def __init__(self): - self._T_w_abs: pin.SE3 - self._T_w_l: pin.SE3 - self._T_w_r: pin.SE3 - - # TODO: you have to have relative frame transformations here - # (from absolute frame to left frame and to the right frame) - raise NotImplementedError - - def constructT_w_abs(self): - raise NotImplementedError - @property - def T_w_abs(self, q=None): + def T_w_abs(self) -> pin.SE3: """ getT_w_abs ----------- get absolute frame, as seen in base frame, based on the current, or provident joint configuration - TODO: figure out what's the best way to get this """ - raise NotImplementedError("need to add the case where you construct it from q") + T_w_abs = pin.SE3.Interpolate(self._T_w_l, self._T_w_r, 0.5) + self._T_w_abs = T_w_abs return self._T_w_abs.copy() + def T_l_r(self) -> pin.SE3: + T_l_r = self._T_w_l.actInv(self._T_w_r) + self._T_l_r = T_l_r + return self._T_l_r.copy() + + def getV_w_abs(self, V_w_l: pin.Motion, V_w_r: pin.Motion) -> pin.Motion: + return 0.5 * (V_w_l + V_w_r) + + def getV_w_lr(self, V_w_l: pin.Motion, V_w_r: pin.Motion) -> pin.Motion: + return V_w_r - V_w_l + def getLeftRightT_w_e(self, q=None): """ getLeftRightT_w_e @@ -80,17 +105,40 @@ class DualArmInterface(AbstractRobotManager): ) # NOTE: this also returns the frame, so less copying possible # pin.updateFramePlacements(self.model, self.data) - pin.updateFramePlacement(self.model, self.data, self.l_ee_frame_id) - pin.updateFramePlacement(self.model, self.data, self.r_ee_frame_id) + pin.updateFramePlacement(self.model, self.data, self._l_ee_frame_id) + pin.updateFramePlacement(self.model, self.data, self._r_ee_frame_id) return ( - self.data.oMf[self.l_ee_frame_id].copy(), - self.data.oMf[self.r_ee_frame_id].copy(), + self.data.oMf[self._l_ee_frame_id].copy(), + self.data.oMf[self._r_ee_frame_id].copy(), ) + # TODO: put back in when python3.12 will be safe to use + # @override def forwardKinematics(self): - pin.updateFramePlacement(self.model, self.data, self.l_ee_frame_id) - pin.updateFramePlacement(self.model, self.data, self.r_ee_frame_id) - self._T_w_l = self.data.oMf[self.l_ee_frame_id].copy() - self._T_w_r = self.data.oMf[self.r_ee_frame_id].copy() - self.constructT_w_abs() - raise NotImplementedError + pin.forwardKinematics( + self.model, + self.data, + self._q, + ) + pin.updateFramePlacement(self.model, self.data, self._l_ee_frame_id) + pin.updateFramePlacement(self.model, self.data, self._r_ee_frame_id) + self._T_w_l = self.data.oMf[self._l_ee_frame_id].copy() + self._T_w_r = self.data.oMf[self._r_ee_frame_id].copy() + self.T_w_abs # will update _T_w_abs in the process + + # TODO: put back in when python3.12 will be safe to use + # @override + # NOTE: this isn't useful unless it's in world frame + def getJacobian(self) -> np.ndarray: + # the other arm filled with zeros + J_left = pin.computeFrameJacobian( + self.model, self.data, self._q, self._l_ee_frame_id + ) + + J_right = pin.computeFrameJacobian( + self.model, self.data, self._q, self._r_ee_frame_id + ) + J = np.zeros((12, self.nq)) + J[: self.nq, :6] = J_left + J[self.nq :, 6:] = J_right + return J