diff --git a/examples/navigation/mobile_base_navigation.py b/examples/navigation/mobile_base_navigation.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/python/smc/control/cartesian_space/cartesian_space_point_to_point.py b/python/smc/control/cartesian_space/cartesian_space_point_to_point.py index aa84d9231c8567f25267d41c0104e079a828f7c2..945c100de8c9ed96dc960a2f9b6290669d20e892 100644 --- a/python/smc/control/cartesian_space/cartesian_space_point_to_point.py +++ b/python/smc/control/cartesian_space/cartesian_space_point_to_point.py @@ -28,7 +28,7 @@ def controlLoopClik(robot: SingleArmInterface, ik_solver, T_w_goal: pin.SE3, _, err_vector = pin.log6(SEerror).vector if np.linalg.norm(err_vector) < robot.args.goal_error: breakFlag = True - J = pin.computeFrameJacobian(robot.model, robot.data, q, robot.ee_frame_id) + J = robot.getJacobian() # compute the joint velocities based on controller you passed # qd = ik_solver(J, err_vector, past_qd=past_data['dqs_cmd'][-1]) qd = ik_solver(J, err_vector) @@ -37,13 +37,13 @@ def controlLoopClik(robot: SingleArmInterface, ik_solver, T_w_goal: pin.SE3, _, qd = dampedPseudoinverse(1e-2, J, err_vector) robot.sendVelocityCommand(qd) - log_item["qs"] = q.reshape((robot.model.nq,)) + log_item["qs"] = q.reshape((robot.nq,)) log_item["dqs"] = robot.v - log_item["dqs_cmd"] = qd.reshape((robot.model.nv,)) + log_item["dqs_cmd"] = qd.reshape((robot.nv,)) log_item["err_norm"] = np.linalg.norm(err_vector).reshape((1,)) # we're not saving here, but need to respect the API, # hence the empty dict - save_past_item["dqs_cmd"] = qd.reshape((robot.model.nv,)) + save_past_item["dqs_cmd"] = qd.reshape((robot.nv,)) return breakFlag, save_past_item, log_item @@ -60,13 +60,13 @@ def moveL(args: Namespace, robot: SingleArmInterface, T_w_goal): controlLoop = partial(controlLoopClik, robot, ik_solver, T_w_goal) # we're not using any past data or logging, hence the empty arguments log_item = { - "qs": np.zeros(robot.model.nq), - "dqs": np.zeros(robot.model.nv), - "dqs_cmd": np.zeros(robot.model.nv), + "qs": np.zeros(robot.nq), + "dqs": np.zeros(robot.nv), + "dqs_cmd": np.zeros(robot.nv), "err_norm": np.zeros(1), } save_past_dict = { - "dqs_cmd": np.zeros(robot.model.nv), + "dqs_cmd": np.zeros(robot.nv), } loop_manager = ControlLoopManager( robot, controlLoop, args, save_past_dict, log_item @@ -125,16 +125,16 @@ def moveUntilContactControlLoop( if np.linalg.norm(wrench[mask]) > args.contact_detecting_force: print("hit with", np.linalg.norm(wrench[mask])) breakFlag = True - robot.sendVelocityCommand(np.zeros(robot.model.nq)) + robot.sendVelocityCommand(np.zeros(robot.nq)) if (args.pinocchio_only) and (i > 500): print("let's say you hit something lule") breakFlag = True # pin.computeJointJacobian is much different than the C++ version lel - J = pin.computeFrameJacobian(robot.model, robot.data, q, robot.ee_frame_id) + J = robot.getJacobian() # compute the joint velocities. qd = ik_solver(J, speed) robot.sendVelocityCommand(qd) - log_item["qs"] = q.reshape((robot.model.nq,)) + log_item["qs"] = q.reshape((robot.nq,)) log_item["wrench"] = wrench.reshape((6,)) return breakFlag, {}, log_item @@ -152,7 +152,7 @@ def moveUntilContact( controlLoop = partial(moveUntilContactControlLoop, args, robot, speed, ik_solver) # we're not using any past data or logging, hence the empty arguments log_item = {"wrench": np.zeros(6)} - log_item["qs"] = np.zeros((robot.model.nq,)) + log_item["qs"] = np.zeros((robot.nq,)) loop_manager = ControlLoopManager(robot, controlLoop, args, {}, log_item) loop_manager.run() print("Collision detected!!") diff --git a/python/smc/control/control_loop_manager.py b/python/smc/control/control_loop_manager.py index bf2c31a8359452353b672226c175ac2852e3327e..9ad9ef4557e9442ab7d1d9fcd9009b4124a0dc31 100644 --- a/python/smc/control/control_loop_manager.py +++ b/python/smc/control/control_loop_manager.py @@ -141,17 +141,21 @@ class ControlLoopManager: if i % self.viz_update_rate == 0: # don't send what wasn't ready if self.args.visualizer: + self.robot_manager.visualizer_manager.sendCommand( + { + "q": self.robot_manager._q, + } + ) if self.robot_manager.robot_name != "yumi": self.robot_manager.visualizer_manager.sendCommand( { - "q": self.robot_manager.q, "T_w_e": self.robot_manager.T_w_e, } ) else: T_w_e_left, T_w_e_right = self.robot_manager.T_w_e self.robot_manager.visualizer_manager.sendCommand( - {"q": self.robot_manager.q, "T_w_e": T_w_e_left} + {"T_w_e": T_w_e_left} ) if self.robot_manager.robot_name == "heron": T_base = self.robot_manager.data.oMi[1] @@ -230,7 +234,7 @@ class ControlLoopManager: # and put it into robot_manager.stopRobot() print("sending 300 speedjs full of zeros and exiting") for _ in range(300): - vel_cmd = np.zeros(self.robot_manager.model.nv) + vel_cmd = np.zeros(self.robot_manager.nv) self.robot_manager.sendVelocityCommand(vel_cmd) self.robot_manager.stopRobot() diff --git a/python/smc/robots/implementations/heron.py b/python/smc/robots/implementations/heron.py index 9054445a1eb0e19eef3aee37177ba8ad3f2ade0e..99cd46ba0345c39f7f5fbafb34c2d6c199ef6ce0 100644 --- a/python/smc/robots/implementations/heron.py +++ b/python/smc/robots/implementations/heron.py @@ -3,6 +3,7 @@ from smc.robots.interfaces.force_torque_sensor_interface import ( ForceTorqueOnSingleArmWrist, ) from smc.robots.interfaces.mobile_base_interface import MobileBaseInterface +from smc.robots.interfaces.whole_body_interface import SingleArmWholeBodyInterface from smc.robots.implementations.ur5e import get_model import time @@ -11,7 +12,9 @@ import pinocchio as pin import hppfcl as fcl -class RobotManagerHeron(ForceTorqueOnSingleArmWrist, MobileBaseInterface): +class RobotManagerHeron( + ForceTorqueOnSingleArmWrist, SingleArmWholeBodyInterface, MobileBaseInterface +): @property def model(self): return self._model @@ -34,6 +37,7 @@ class RobotManagerHeron(ForceTorqueOnSingleArmWrist, MobileBaseInterface): self._model, self._collision_model, self._visual_model, self._data = ( heron_approximation() ) + self._mode = SingleArmWholeBodyInterface.control_mode.whole_body self._ee_frame_id = self.model.getFrameId("tool0") self._base_frame_id = self.model.getFrameId("mobile_base") # TODO: CHANGE THIS TO REAL VALUES diff --git a/python/smc/robots/interfaces/mobile_base_interface.py b/python/smc/robots/interfaces/mobile_base_interface.py index e93a7a1ab2c2f634c11cbc9c37da1f12ee78693f..2593f6a63a522ae4aa8efde35457400f79abdcc9 100644 --- a/python/smc/robots/interfaces/mobile_base_interface.py +++ b/python/smc/robots/interfaces/mobile_base_interface.py @@ -1,7 +1,8 @@ -import numpy as np +from smc.robots.robotmanager_abstract import AbstractRobotManager +import numpy as np import pinocchio as pin -from smc.robots.robotmanager_abstract import AbstractRobotManager +from argparse import Namespace class MobileBaseInterface(AbstractRobotManager): @@ -18,21 +19,13 @@ class MobileBaseInterface(AbstractRobotManager): implementing an abstract mobile base interface. """ - def __init__(self, args): + def __init__(self, args: Namespace): if args.debug_prints: print("MobileBase init") self._base_frame_id: int self._T_w_b: pin.SE3 super().__init__(args) - @property - def base_position2D(self): - return self._q[:2] - - @property - def base_position3D(self): - return np.array(list(self._q[:2]) + [0.0]) - @property def base_SE2_pose(self): # NOTE: idk if this is the best way to calculate theta diff --git a/python/smc/robots/interfaces/single_arm_interface.py b/python/smc/robots/interfaces/single_arm_interface.py index b6f6e46f15f8a23eca52b0a75b836b2ef4e55b0d..ce49432eb3fa0bb3d43b6c27c91a40de78aa42a3 100644 --- a/python/smc/robots/interfaces/single_arm_interface.py +++ b/python/smc/robots/interfaces/single_arm_interface.py @@ -31,6 +31,11 @@ class SingleArmInterface(AbstractRobotManager): pin.updateFramePlacement(self.model, self.data, self._ee_frame_id) return self.data.oMf[self._ee_frame_id].copy() + def getJacobian(self) -> np.ndarray: + return pin.computeFrameJacobian( + self.model, self.data, self._q, self._ee_frame_id + ) + def forwardKinematics(self): pin.forwardKinematics( self.model, diff --git a/python/smc/robots/interfaces/whole_body_interface.py b/python/smc/robots/interfaces/whole_body_interface.py new file mode 100644 index 0000000000000000000000000000000000000000..752fdff47f290a5b069ee8aec89fd726f1798926 --- /dev/null +++ b/python/smc/robots/interfaces/whole_body_interface.py @@ -0,0 +1,137 @@ +from smc.robots.interfaces.mobile_base_interface import MobileBaseInterface +from smc.robots.interfaces.single_arm_interface import SingleArmInterface + +import pinocchio as pin +from argparse import Namespace +import numpy as np +from enum import Enum + +# TODO: put back in when python3.12 will be safe to use +# from typing import override + + +class SingleArmWholeBodyInterface(SingleArmInterface, MobileBaseInterface): + """ + SingleArmWholeBodyInterface + --------------------------- + exists to provide either: + 1) whole body values + 2) base only value + 3) arm only value + + what you get depends on the mode you set - they're enumerate as above + """ + + control_mode = Enum("mode", [("whole_body", 1), ("base_only", 2), ("arm_only", 3)]) + + def __init__(self, args: Namespace): + if args.debug_prints: + print("SingleArmWholeBodyInterface init") + self._mode: SingleArmWholeBodyInterface.control_mode + super().__init__(args) + + @property + def mode(self) -> control_mode: + return self._mode + + @mode.setter + def setMode(self, mode: control_mode) -> None: + assert type(mode) is self.control_mode + self._mode = mode + + # TODO: put back in when python3.12 will be safe to use + # @override + @property + def q(self) -> np.ndarray: + # 3if self._mode.value == 1: + # 3 return self._q.copy() + + if self._mode.value == 2: + return self._q[:4] + + if self._mode.value == 3: + return self._q[4:] + + return self._q.copy() + + @property + def nq(self): + if self._mode.value == 2: + return 4 + + if self._mode.value == 3: + return self.model.nq - 4 + + return self.model.nq + + @property + def v(self) -> np.ndarray: + # 3if self._mode.value == 1: + # 3 return self._q.copy() + + if self._mode.value == 2: + return self._v[:3] + + if self._mode.value == 3: + return self._v[3:] + + return self._v.copy() + + @property + def nv(self): + if self._mode.value == 2: + return 3 + + if self._mode.value == 3: + return self.model.nv - 3 + + return self.model.nv + + # TODO: put back in when python3.12 will be safe to use + # @override + def forwardKinematics(self) -> None: + pin.forwardKinematics( + self.model, + self.data, + self._q, + ) + pin.updateFramePlacement(self.model, self.data, self._ee_frame_id) + pin.updateFramePlacement(self.model, self.data, self._base_frame_id) + self._T_w_e = self.data.oMf[self._ee_frame_id].copy() + self._T_w_b = self.data.oMf[self._base_frame_id].copy() + + # TODO: put back in when python3.12 will be safe to use + # @override + def getJacobian(self) -> np.ndarray: + J_full = pin.computeFrameJacobian( + self.model, self.data, self._q, self._ee_frame_id + ) + # if self._mode.value == 1: + # return J_full + + if self._mode.value == 2: + return J_full[:, :3] + + if self._mode.value == 3: + return J_full[:, 3:] + + return J_full + + # TODO: put back in when python3.12 will be safe to use + # @override + def sendVelocityCommand(self, v) -> None: + """ + sendVelocityCommand + ------------------- + 1) saturate the command to comply with hardware limits or smaller limits you've set + 2) send it via the particular robot's particular communication interface + """ + if self._mode.value == 2: + v = np.hstack((v, np.zeros(self.model.nv - 3))) + + if self._mode.value == 3: + v = np.hstack((np.zeros(3), v)) + assert type(v) == np.ndarray + assert len(v) == self.model.nv + v = np.clip(v, -1 * self._max_v, self._max_v) + self.sendVelocityCommandToReal(v) diff --git a/python/smc/robots/robotmanager_abstract.py b/python/smc/robots/robotmanager_abstract.py index 2b94efd52e249f42b0b7c02c6a673fb423f01c26..ffe9976f1649a9c5392e89b4a991d55ecaeeec4f 100644 --- a/python/smc/robots/robotmanager_abstract.py +++ b/python/smc/robots/robotmanager_abstract.py @@ -108,8 +108,9 @@ class AbstractRobotManager(abc.ABC): self.collision_model, self.visual_model, ) + # NOTE: it HAS TO be _q, we override q to support control of subsets of joints self.visualizer_manager = ProcessManager( - args, side_function, {"q": self.q}, 0 + args, side_function, {"q": self._q}, 0 ) # TODO: move this bs to visualizer, there are 0 reasons to keep it here if args.visualize_collision_approximation: @@ -141,10 +142,18 @@ class AbstractRobotManager(abc.ABC): def q(self): return self._q.copy() + @property + def nq(self): + return self.model.nq + @property def v(self): return self._v.copy() + @property + def nv(self): + return self.model.nv + # _updateQ and _updateV could be put into getters and setters of q and v respectively. # however, because i don't trust the interfaces of a random robot to play ball, # i want to keep this separated. diff --git a/python/smc/robots/utils.py b/python/smc/robots/utils.py index d18574145dd440bc8e8908e81c82d854697e37b2..c3720b626724c3e9d8fe9ff84506284db6195809 100644 --- a/python/smc/robots/utils.py +++ b/python/smc/robots/utils.py @@ -190,9 +190,7 @@ def defineGoalPointCLI(robot): robot._step() q = robot.q # define goal - pin.forwardKinematics(robot.model, robot.data, np.array(q)) - pin.updateFramePlacement(robot.model, robot.data, robot.ee_frame_id) - T_w_e = robot.data.oMf[robot.ee_frame_id] + T_w_e = robot.T_w_e print("You can only specify the translation right now.") if robot.args.real: print(