Skip to content
Snippets Groups Projects
Select Git revision
  • 6d5be9a9d4cca12cbb186d76a4d9761944b9862f
  • main default protected
  • rosification
  • refactoring
  • pp
  • mpc
  • realtimelogplotter
  • alv
  • gitlab_ci_podman
  • restructuring
  • viz_fix
11 results

drawing_from_input_drawing.py

Blame
  • user avatar
    6d5be9a9
    History
    drawing_from_input_drawing.py 25.45 KiB
    # this is the main file for the drawing your drawings with a dmp with force feedback
    # TODO:
    # 2. delete the unnecessary comments
    # 8. add some code to pick up the marker from a prespecified location
    # 10. write documentation as you go along
    # BIG TODO:
    # DOES NOT WORK WITH SPEED SLIDER =/= 1.0!!!!!!!!!!!!!!!!!!!!!!!!!!!
    # realistic solution: keep it at 1.0 at all times. make acceleration and/or
    # speeds small
    
    import pinocchio as pin
    import numpy as np
    import matplotlib.pyplot as plt
    import copy
    import argparse
    import time
    from functools import partial
    from ur_simple_control.util.get_model import get_model
    from ur_simple_control.visualize.visualize import plotFromDict
    from ur_simple_control.util.draw_path import drawPath
    from ur_simple_control.dmp.dmp import DMP, NoTC,TCVelAccConstrained 
    # TODO merge these clik files as well, they don't deserve to be separate
    # TODO but first you need to clean up clik.py as specified there
    from ur_simple_control.clik.clik_point_to_point import getClikController, moveL, moveUntilContact
    from ur_simple_control.clik.clik_trajectory_following import map2DPathTo3DPlane, clikCartesianPathIntoJointPath
    from ur_simple_control.managers import ControlLoopManager, RobotManager
    from ur_simple_control.util.calib_board_hacks import calibratePlane, getSpeedInDirectionOfN
    from ur_simple_control.basics.basics import moveJ
    
    #######################################################################
    #                            arguments                                #
    #######################################################################
    
    def getArgs():
        #######################################################################
        #                          generic arguments                          #
        #######################################################################
        parser = argparse.ArgumentParser(description='Make a drawing on screen,\
                watch the robot do it on the whiteboard.',
                formatter_class=argparse.ArgumentDefaultsHelpFormatter)
        # TODO this one won't really work but let's leave it here for the future
        parser.add_argument('--simulation', action=argparse.BooleanOptionalAction, 
                help="whether you are running the UR simulator. \
                        NOTE: doesn't actually work because it's not a physics simulator", \
                        default=False)
        parser.add_argument('--pinocchio-only', action=argparse.BooleanOptionalAction, 
                help="whether you want to just integrate with pinocchio.\
                        NOTE: doesn't actually work because it's not a physics simulator", \
                        default=False)
        parser.add_argument('--visualize', action=argparse.BooleanOptionalAction, 
                help="whether you want to visualize with gepetto, but \
                        NOTE: not implemented yet", default=False)
        parser.add_argument('--gripper', action=argparse.BooleanOptionalAction, \
                help="whether you're using the gripper", default=False)
        parser.add_argument('--acceleration', type=float, \
                help="robot's joints acceleration. scalar positive constant, \
                max 1.7, and default 0.4. \
                BE CAREFUL WITH THIS. the urscript doc says this is 'lead axis acceleration'.\
                TODO: check what this means", default=0.3)
        parser.add_argument('--speed-slider', type=float,\
                help="cap robot's speed with the speed slider \
                        to something between 0 and 1, 1.0 by default because for dmp. \
                        BE CAREFUL WITH THIS.", default=1.0)
        parser.add_argument('--max-iterations', type=int, \
                help="maximum allowable iteration number (it runs at 500Hz)", default=50000)
        parser.add_argument('--debug-prints', action=argparse.BooleanOptionalAction, \
                help="print some debug info", default=False)
        #######################################################################
        #                 your controller specific arguments                  #
        #######################################################################
        # not applicable here, but leaving it in the case it becomes applicable
        # it's also in the robot manager even though it shouldn't be
        parser.add_argument('--past-window-size', type=int, \
                help="how many timesteps of past data you want to save", default=5)
        parser.add_argument('--goal-error', type=float, \
                help="the final position error you are happy with. NOTE: not used here", \
                default=1e-3)
        # TODO: test the interaction of this and the overall demo
        parser.add_argument('--tikhonov-damp', type=float, \
                help="damping scalar in tiknohov regularization.\
                This is used when generating the joint trajectory from the drawing.", \
                default=1e-2)
        # TODO add the rest
        parser.add_argument('--clik-controller', type=str, \
                help="select which click algorithm you want", \
                default='dampedPseudoinverse', \
                choices=['dampedPseudoinverse', 'jacobianTranspose'])
            # maybe you want to scale the control signal
        parser.add_argument('--controller-speed-scaling', type=float, \
                default='1.0', help='not actually_used atm')
        #############################
        #  dmp  specific arguments  #
        #############################
        parser.add_argument('--temporal-coupling', action=argparse.BooleanOptionalAction, \
                help="whether you want to use temporal coupling", default=True)
        parser.add_argument('--kp', type=float, \
                help="proportial control constant for position errors", \
                default=1.0)
        parser.add_argument('--tau0', type=float, \
                help="total time needed for trajectory. if you use temporal coupling,\
                      you can still follow the path even if it's too fast", \
                default=5)
        parser.add_argument('--gamma-nominal', type=float, \
                help="positive constant for tuning temporal coupling: the higher,\
                the fast the return rate to nominal tau", \
                default=1.0)
        parser.add_argument('--gamma-a', type=float, \
                help="positive constant for tuning temporal coupling, potential term", \
                default=0.5)
        parser.add_argument('--eps-tc', type=float, \
                help="temporal coupling term, should be small", \
                default=0.001)
        parser.add_argument('--alpha', type=float, \
                help="force feedback proportional coefficient", \
                #default=0.007)
                default=0.05)
        parser.add_argument('--beta', type=float, \
                help="low-pass filter beta parameter", \
                default=0.01)
        # TODO add low pass filtering and make it's parameters arguments too
        #######################################################################
        #                       task specific arguments                       #
        #######################################################################
        # TODO measure this for the new board
        parser.add_argument('--board-width', type=float, \
                help="width of the board (in meters) the robot will write on", \
                #default=0.5)
                default=0.25)
        parser.add_argument('--board-height', type=float, \
                help="height of the board (in meters) the robot will write on", \
                #default=0.35)
                default=0.25)
        parser.add_argument('--board-axis', type=str, \
                choices=['z', 'y'], \
                help="(world) axis (direction) in which you want to search for the board", \
                default="z")
        parser.add_argument('--calibration', action=argparse.BooleanOptionalAction, \
                help="whether you want to do calibration", default=False)
        parser.add_argument('--draw-new', action=argparse.BooleanOptionalAction, \
                help="whether draw a new picture, or use the saved path path_in_pixels.csv", default=True)
        parser.add_argument('--pick-up-marker', action=argparse.BooleanOptionalAction, \
                help="""
        whether the robot should pick up the marker.
        NOTE: THIS IS FROM A PREDEFINED LOCATION.
        """, default=False)
        parser.add_argument('--find-marker-offset', action=argparse.BooleanOptionalAction, \
                help="""
        whether you want to do find marker offset (recalculate TCP
        based on the marker""", default=False)
        parser.add_argument('--n-calibration-tests', type=int, \
                help="number of calibration tests you want to run", default=10)
        parser.add_argument('--clik-goal-error', type=float, \
                help="the clik error you are happy with", default=1e-2)
        parser.add_argument('--max-init-clik-iterations', type=int, \
                help="number of max clik iterations to get to the first point", default=10000)
        parser.add_argument('--max-running-clik-iterations', type=int, \
                help="number of max clik iterations between path points", default=1000)
        args = parser.parse_args()
        if args.gripper and args.simulation:
            raise NotImplementedError('Did not figure out how to put the gripper in \
                    the simulation yet, sorry :/ . You can have only 1 these flags right now')
        return args
    
    # go and pick up the marker
    # marker position:
    """
      R =
     -0.73032 -0.682357 0.0319574
    -0.679774  0.730578 0.0645244
    -0.067376 0.0253997 -0.997404
      p =  0.574534 -0.508343  0.249325
    
    pin: 0.5745 -0.5083 0.2493 3.1161 0.0674 -2.392
    ur5: 0.5796 -0.4982 0.0927 -1.1314 2.8725 0.0747
    q: -0.5586 -2.3103 -1.1638 -1.2468 1.6492 0.262 0.0 0.0
    
    """
    def getMarker(args, robot):
        # init position
        Tinit = robot.getMtool().copy()
        # goal position, predefined
        Tgoal = pin.SE3()
        Tgoal.rotation = np.array([
            [ -0.73032, -0.682357, 0.0319574],
            [-0.679774,  0.730578, 0.0645244],
            [-0.067376, 0.0253997, -0.997404]])
        Tgoal.translation = np.array([0.574534, -0.508343,  0.249325])
    
        # slighly up from goal to not hit marker weirdly
        TgoalUP = Tgoal.copy() 
        TgoalUP.translation += np.array([0,0,0.1])
    
        # instantiate controller
        clik_controller = getClikController(args)
        controlLoop = partial(controlLoopClik, robot, clik_controller)
        log_dict = {
                'qs' : np.zeros((args.max_iterations, robot.model.nq)),
                'dqs' : np.zeros((args.max_iterations, robot.model.nq)),
            }
        loop_manager = ControlLoopManager(robot, controlLoop, args, {}, log_dict)
    
        print("going to above marker")
        robot.Mgoal = TgoalUP.copy()
        log_dict, final_iteration = loop_manager.run()
        print("going down to marker")
        robot.Mgoal = Tgoal.copy()
        log_dict, final_iteration = loop_manager.run()
        print("picking up marker")
        robot.gripper.move(255,255,255)
        time.sleep(2)
        print("going up")
        robot.Mgoal = TgoalUP.copy()
        log_dict, final_iteration = loop_manager.run()
        print("going back")
        robot.Mgoal = Tinit.copy()
        log_dict, final_iteration = loop_manager.run()
        print("got back")
    #    robot.stopHandler(None, None)
    #    robot.stopHandler(None, None)
    #    robot.stopHandler(None, None)
    #    print("bye")
    #    exit()
    
    """
    findMarkerOffset
    ---------------
    This relies on having the correct orientation of the plane 
    and the correct translation vector for top-left corner.
    Idea is you pick up the marker, go to the top corner,
    touch it, and see the difference between that and the translation vector.
    Obviously it's just a hacked solution, but it works so who cares.
    """
    def findMarkerOffset(args, robot, rotation_matrix, translation_vector, q_init):
        # TODO make this more general
        # so TODO: calculate TCP speed based on the rotation matrix
        # and then go
        if args.board_axis == 'z':
            axis_of_rot = rotation_matrix[:,2]
        elif args.board_axis == 'y':
            axis_of_rot = rotation_matrix[:,1]
        else:
            print("you passed", board_axis, ", but it has to be 'z' or 'y'")
            exit()
        # it's going out of the board, and we want to go into the board, right????
        # TODO test this
        #z_of_rot = z_of_rot 
        print("vector i'm following:", axis_of_rot)
        speed = getSpeedInDirectionOfN(rotation_matrix, args.board_axis)
        #speed[2] = speed[2] * -1
        #robot.rtde_control.moveUntilContact(speed)
        moveUntilContact(args, robot, speed)
        # we use the pin coordinate system because that's what's 
        # the correct thing long term accross different robots etc
        q = robot.getQ()
        pin.forwardKinematics(robot.model, robot.data, np.array(q))
        current_translation = robot.data.oMi[6].translation
        # i only care about the z because i'm fixing the path atm
        # but, let's account for the possible milimiter offset 'cos why not
        print("translation_vector", translation_vector)
        print("current_translation", current_translation)
        print("translation_vector - current_translation", \
                translation_vector - current_translation)
        marker_offset = np.linalg.norm(translation_vector - current_translation)
    #    robot.setSpeedSlider(old_speed_slider)
        return marker_offset
    
    #######################################################################
    #                            control loop                             #
    #######################################################################
    
    # feedforward velocity, feedback position and force for impedance
    # TODO: actually write this out
    def controller():
        pass
    
    # TODO:
    # regarding saving data you have 2 options:
    # 1) explicitely return what you want to save - you can't magically read local variables
    # 2) make controlLoop a class and then save handle the saving behind the scenes -
    #    now you these variables are saved in a class so they're not local variables
    # option 1 is clearly more hands-on and thus worse
    # option 2 introduces a third big class and makes everything convoluted.
    # for now, we go for option 1) because it's simpler to implement and deal with.
    # but in the future, implementing 2) should be tried. you really have to try 
    # to do it cleanly to see how good/bad it is.
    # in that case you'd most likely want to inherit ControlLoopManager and go from there.
    # you'd still need to specify what you're saving with self.that_variable so no matter
    # there's no running away from that in any case.
    # it's 1) for now, it's the only non-idealy-clean part of this solution, and it's ok really.
    # TODO but also look into something fancy like some decorator or something and try
    # to find option 3)
    
    # control loop to be passed to ControlLoopManager
    def controlLoopWriting(wrench_offset, dmp, tc, controller, robot, i, past_data):
        breakFlag = False
        # TODO rename this into something less confusing
        save_past_dict = {}
        log_item = {}
        dmp.step(robot.dt) # dmp step
        # temporal coupling step
        tau = dmp.tau + tc.update(dmp, robot.dt) * robot.dt
        dmp.set_tau(tau)
        q = robot.getQ()
        Mtool = robot.getMtool()
        # TODO look into UR code/api for estimating the same
        # based on currents in the joints.
        # it's probably worse, but maybe some sensor fusion-type thing
        # is actually better, who knows.
        # also you probably want to do the fusion of that onto tau (got from J.T @ wrench)
        #Z = np.diag(np.array([0.6, 0.6, 1.0, 0.5, 0.5, 0.5]))
        #Z = np.diag(np.array([0.6, 1.0, 0.6, 0.5, 0.5, 0.5]))
        #Z = np.diag(np.array([1.0, 1.0, 1.0, 1.0, 1.0, 1.0]))
        Z = np.diag(np.array([0.0, 0.0, 2.0, 0.0, 0.0, 0.0]))
        #Z = np.diag(np.ones(6))
        #Z = np.diag(np.array([0.1, 0.1, 1.0, 0.1, 0.1, 0.1]))
    
        #Z = np.diag(np.array([1.0, 0.6, 1.0, 0.5, 0.5, 0.5]))
        wrench = robot.getWrench()
        wrench = wrench - wrench_offset
        #wrench = np.average(np.array(past_data['wrench']), axis=0)
    
        # first-order low pass filtering instead
        # beta is a smoothing coefficient, smaller values smooth more, has to be in [0,1]
        wrench = args.beta * wrench + (1 - args.beta) * past_data['wrench'][-1]
    
        # it's just coordinate change from base to end-effector,
        # they're NOT the same rigid body,
        # the vector itself is not changing, only the coordinate representation
        mapping = np.zeros((6,6))
        mapping[0:3, 0:3] = Mtool.rotation
        mapping[3:6, 3:6] = Mtool.rotation
        wrench = mapping.T @ wrench
    
        wrench = Z @ wrench
        # deepcopy for good coding practise (and correctness here)
        save_past_dict['wrench'] = copy.deepcopy(wrench)
        # rolling average
        if i % 100 == 0:
            print(wrench)
        pin.forwardKinematics(robot.model, robot.data, q)
        J = pin.computeJointJacobian(robot.model, robot.data, q, robot.JOINT_ID)
        dq = robot.getQd()[:6].reshape((6,1))
        # get joitn 
        tau = J.T @ wrench
        tau = tau[:6].reshape((6,1))
        # compute control law:
        # - feedforward the velocity and the force reading
        # - feedback the position 
        # TODO: don't use vel for qd, it's confusion (yes, that means changing dmp code too)
        # TODO: put this in a controller function for easy swapping (or don't if you won't swap)
        # solve this q[:6] bs (clean it up)
        vel_cmd = dmp.vel + args.kp * (dmp.pos - q[:6].reshape((6,1))) + args.alpha * tau
        robot.sendQd(vel_cmd)
    
        # TODO find a better criterion for stopping
        if (np.linalg.norm(dmp.vel) < 0.0001) and (i > 5000):
            breakFlag = True
        # immediatelly stop if something weird happened (some non-convergence)
        if np.isnan(vel_cmd[0]):
            breakFlag = True
    
        # log what you said you'd log
        # TODO fix the q6 situation (hide this)
        log_item['qs'] = q[:6].reshape((6,))
        log_item['dmp_poss'] = dmp.pos.reshape((6,))
        log_item['dqs'] = dq.reshape((6,))
        log_item['dmp_vels'] = dmp.vel.reshape((6,))
    
        return breakFlag, save_past_dict, log_item
    
    if __name__ == "__main__":
        #######################################################################
        #                           software setup                            #
        #######################################################################
        args = getArgs()
        print(args)
        clikController = getClikController(args)
        robot = RobotManager(args)
    
        # calibrate FT first
        wrench_offset = robot.calibrateFT()
        robot.wrench_offset = wrench_offset
        #######################################################################
        #          drawing a path, making a joint trajectory for it           #
        #######################################################################
        # TODO make these ifs make more sense
        
        # draw the path on the screen
        if args.draw_new:
            pixel_path = drawPath()
            # make it 3D
        else:
            pixel_path_file_path = './path_in_pixels.csv'
            pixel_path = np.genfromtxt(pixel_path_file_path, delimiter=',')
        # do calibration if specified
        if args.calibration:
            rotation_matrix, translation_vector, q_init = \
                calibratePlane(args, robot, args.board_width, args.board_height, \
                               args.n_calibration_tests)
            print(q_init)
            print(rotation_matrix)
            print(translation_vector)
        else:
            # TODO: save this somewhere obviously
            # also make it prettier if possible
            print("using predefined values")
            #q_init = np.array([1.4545, -1.7905, -1.1806, -1.0959, 1.6858, -0.1259, 0.0, 0.0])
            #translation_vector = np.array([0.10125722 ,0.43077874 ,0.9110792 ])
            #rotation_matrix = np.array([[1.  ,       0.         ,0.00336406],
            #                            [-0.        , -0.00294646,  0.99999   ],
            #                            [ 0.        , -0.99999  ,  -0.00294646]])
            q_init = robot.getQ()
            Mtool = robot.getMtool()
            translation_vector = Mtool.translation.copy()
            rotation_matrix = Mtool.rotation.copy()
    
        # make the path 3D
        path = map2DPathTo3DPlane(pixel_path, args.board_width, args.board_height)
        # TODO: fix and trust z axis in 2D to 3D path
        # TODO: add an offset of the marker (this is of course approximate)
        # TODO: make this an argument once the rest is OK
        # ---> just go to the board while you have the marker ready to find this
        # ---> do that right here
        if args.pick_up_marker:
            getMarker(args, robot)
    
        if args.find_marker_offset:
            # find the marker offset
            # TODO find a better q init (just moveL away from the board)
            marker_offset = findMarkerOffset(args, robot, rotation_matrix, translation_vector, q_init)
            print('marker_offset', marker_offset)
            robot.stopHandler(None,None)
            # Z
            #path = path + np.array([0.0, 0.0, -1 * marker_offset])
            # Y
            #path = path + np.array([0.0, -1 * marker_offset, 0.0])
            path = path + np.array([0.0, 0.0, -1 * marker_offset])
        else:
            #path = path + np.array([0.0, -0.0938, 0.0])
            #path = path + np.array([0.0, 0.0, -0.0938])
            # NEW MARKER IS SHORTER
            #path = path + np.array([0.0, 0.0, -0.0813])
            # NOTE GOOD FOR SHORT
            #path = path + np.array([0.0, 0.0, -0.0750])
            #path = path + np.array([0.0, 0.0, -0.0791])
            #path = path + np.array([0.0, 0.0, -0.0808])
            #path = path + np.array([0.0, 0.0, -0.0108])
            # NOTE: THIS IS THE ONE
            # NOTE: not a single one is the one, the f/t sensor sucks
            # and this is the best number to change to get it to work [upside_down_emoji]
            # but this is very close
            #path = path + np.array([0.0, 0.0, -0.0785])
            path = path + np.array([0.0, 0.0, -0.1043])
            #path = path + np.array([0.0, 0.0, -0.1000])
            #path = path + np.array([0.0, 0.0, -0.1573])
            #path = path + np.array([0.0, 0.2938, 0.0])
    
        # and if you don't want to draw new nor calibrate, but you want the same path
        # with a different clik, i'm sorry, i can't put that if here.
        # atm running the same joint trajectory on the same thing makes for easier testing
        # of the final system.
        if args.draw_new or args.calibration:
            
            #path = path + np.array([0.0, 0.0, -0.0938])
            # create a joint space trajectory based on the 3D path
        # TODO: add flag of success (now it's your eyeballs and printing)
        # and immediatelly exit if it didn't work
            joint_trajectory = clikCartesianPathIntoJointPath(path, args, robot, \
                clikController, q_init, rotation_matrix, translation_vector)
        else:
            joint_trajectory_file_path = './joint_trajectory.csv'
            joint_trajectory = np.genfromtxt(joint_trajectory_file_path, delimiter=',')
        
        # create DMP based on the trajectory
        dmp = DMP(joint_trajectory)
        if not args.temporal_coupling:
            tc = NoTC()
        else:
            # TODO test whether this works (it should, but test it)
            # test the interplay between this and the speed slider
            # ---> SPEED SLIDER HAS TO BE AT 1.0
            v_max_ndarray = np.ones(robot.n_joints) * robot.max_qd #* args.speed_slider
            # test the interplay between this and the speed slider
            # args.acceleration is the actual maximum you're using
            a_max_ndarray = np.ones(robot.n_joints) * args.acceleration #* args.speed_slider
            tc = TCVelAccConstrained(args.gamma_nominal, args.gamma_a, v_max_ndarray, a_max_ndarray, args.eps_tc)
    
        # TODO and NOTE the weight, TCP and inertial matrix needs to be set on the robot
        # you already found an API in rtde_control for this, just put it in initialization 
        # under using/not-using gripper parameters
        # ALSO NOTE: to use this you need to change the version inclusions in
        # ur_rtde due to a bug there in the current ur_rtde + robot firmware version 
        # (the bug is it works with the firmware verion, but ur_rtde thinks it doesn't)
        # here you give what you're saving in the rolling past window 
        # it's initial value.
        # controlLoopManager will populate the queue with these initial values
        save_past_dict = {
                'wrench' : np.zeros(6),
            }
        # here you give it it's initial value
        log_dict = {
                'qs' : np.zeros((args.max_iterations, 6)),
                'dmp_poss' : np.zeros((args.max_iterations, 6)),
                'dqs' : np.zeros((args.max_iterations, 6)),
                'dmp_vels' : np.zeros((args.max_iterations, 6)),
            }
        controlLoop = partial(controlLoopWriting, wrench_offset, dmp, tc, controller, robot)
        loop_manager = ControlLoopManager(robot, controlLoop, args, save_past_dict, log_dict)
        #######################################################################
        #                           physical setup                            #
        #######################################################################
        # TODO: add marker picking
        # get up from the board
        current_pose = robot.getMtool()
        # Z
        #current_pose.translation[2] = current_pose.translation[2] + 0.03
        # Y
        #current_pose.translation[1] = current_pose.translation[1] + 0.03
        #moveL(args, robot, current_pose)
        # move to initial pose
        dmp.step(1/500)
        first_q = dmp.pos.reshape((6,))
        first_q = list(first_q)
        first_q.append(0.0)
        first_q.append(0.0)
        first_q = np.array(first_q)
        #pin.forwardKinematics(robot.model, robot.data, first_q)
        mtool = robot.getMtool(q_given=first_q)
        mtool.translation[1] = mtool.translation[1] - 0.0035
        #mtool.translation[1] = mtool.translation[1] - 0.04
        print('going to starting write position')
        moveL(args, robot, mtool)
        #moveL
    
        #moveJ(args, robot, dmp.pos.reshape((6,)))
        # and now we can actually run
        log_dict, final_iteration = loop_manager.run()
        mtool = robot.getMtool()
        print("move a bit back")
        if args.board_axis == 'z' or args.board_axis == 'y':
            mtool.translation[1] = mtool.translation[1] - 0.1
    #    if args.board_axis == 'z':
    #        mtool.translation[2] = mtool.translation[2] - 0.1
        moveL(args, robot, mtool)
    
        plotFromDict(log_dict, args)
        robot.stopHandler(None, None)
        robot.stopHandler(None, None)
        robot.stopHandler(None, None)
        # plot results
        plotFromDict(log_dict, args)
        # TODO: add some math to analyze path precision