Example #1
0
class HoloFly(object):
    """Docstring for HoloFly. """

    def __init__(self):
        """TODO: to be defined1. """
        # Init an instance of holodeck environment.
        self.env = Holodeck.make("UrbanCity")

        # Init vimfly to get keyboard commands.
        self.vfly = VimFly()

        # Init our state vector:
        # x = [x, y, z, roll, pitch, yaw, u, v, w, p, q, r]
        self.states = np.zeros((12,))
        self.commands = np.zeros((4,))

        # Make an opencv window.
        cv2.namedWindow('Holodeck Image')

        # Init previous img
        self.prev_frame = None

        # Optical flow stuff.
        # Make grid of points to compute OF at.
        grid = np.indices((512,512))
        pixels_to_skip = 8;
        row_start = 128
        row_end = 384

        # Canyon following
        # Make points for left section.
        left = grid[:, row_start:row_end:pixels_to_skip, pixels_to_skip:128:pixels_to_skip]
        left_px = left[1, :, :].reshape((-1,))
        left_py = left[0, :, :].reshape((-1,))
        left_pixels = np.vstack((left_px, left_py))
        self.left_pixels = left_pixels.T.reshape(-1,1,2)

        # Make points for right section.
        right = grid[:, row_start:row_end:pixels_to_skip, 384+pixels_to_skip:512 :pixels_to_skip]
        right_px = right[1, :, :].reshape((-1,))
        right_py = right[0, :, :].reshape((-1,))
        right_pixels = np.vstack((right_px, right_py))
        self.right_pixels = right_pixels.T.reshape(-1,1,2)

        # Things to control off
        self.lr_diff = 0

        # Obstacle avoidance
        # Make points for mleft section.
        mleft = grid[:, row_start:row_end:pixels_to_skip, 64 + pixels_to_skip:256:pixels_to_skip]
        mleft_px = mleft[1, :, :].reshape((-1,))
        mleft_py = mleft[0, :, :].reshape((-1,))
        mleft_pixels = np.vstack((mleft_px, mleft_py))
        self.mleft_pixels = mleft_pixels.T.reshape(-1,1,2)

        # Make points for mright section.
        mright = grid[:, row_start:row_end:pixels_to_skip, 256+pixels_to_skip:448:pixels_to_skip]
        mright_px = mright[1, :, :].reshape((-1,))
        mright_py = mright[0, :, :].reshape((-1,))
        mright_pixels = np.vstack((mright_px, mright_py))
        self.mright_pixels = mright_pixels.T.reshape(-1,1,2)

        # Things to control off
        self.mlr_diff = 0

        # Height
        # Make points for mleft section.
        bot = grid[:, 384:448:pixels_to_skip, 192:321:pixels_to_skip]
        bot_px = bot[1, :, :].reshape((-1,))
        bot_py = bot[0, :, :].reshape((-1,))
        bot_pixels = np.vstack((bot_px, bot_py))
        self.bot_pixels = bot_pixels.T.reshape(-1,1,2)

        # Things to control off
        self.bot = 0
        self.alt_command = 0

        # Time to Collision
        # Make points for mleft section.
        collision_points = grid[:, 200, 200-24:256+24:pixels_to_skip]
        # print ("col points: ", collision_points)
        col_px = collision_points[1, :].reshape((-1,))
        col_py = collision_points[0, :].reshape((-1,))
        col_pixels = np.vstack((col_px, col_py))
        self.col_pixels = col_pixels.T.reshape(-1,2)

        # Things to control off
        self.ttc = 9999

        # PID controllers.
        self.vx_pid = PID(p=1.0, d=0.1, max_=np.pi*30./180., min_=-np.pi*30./180.)
        self.vy_pid = PID(p=1.0, d=0.1, max_=np.pi*30./180., min_=-np.pi*30./180.)
        self.PID_yaw = PID(p=1.0, i=0.1)
        self.yaw_control = 0.0
        self.yaw_command = 0.0

        # Flag for automode
        self.autonomous = False

        # Run til we press escape.
        self.run()

    def run(self):
        while(1):
            # Get commands from vimfly
            command, auto, quit, reset = self.vfly.manual_control()

            # Switch autonomous flag
            if auto:
                self.autonomous = not self.autonomous
                print ("Autonomous mode now: ", self.autonomous)

                # Set my yaw command
                self.yaw_command = self.states[5]
                self.alt_command = 0.

            # Take a step in the holodeck.
            if not self.autonomous:
                state, reward, terminal, _ = self.env.step(command)
            else:
                auto_command = self.auto_control(command)
                state, reward, terminal, _ = self.env.step(auto_command)

            # Process camera and other sensors.
            self.process_sensors(state)
            pixels = state[Sensors.PRIMARY_PLAYER_CAMERA]
            if self.prev_frame is None:
                self.prev_frame = cv2.cvtColor(pixels, cv2.COLOR_BGR2GRAY)
            else:
                self.image_filters(pixels)
                self.prev_frame = cv2.cvtColor(pixels, cv2.COLOR_BGR2GRAY)

            # Reset environment if we pressed r
            if reset:
                self.env.reset()

            # Quit if we press ESC
            ch = cv2.waitKey(1)
            if ch == 27 or quit:
                break

    def auto_control(self, command):
        # print ("\nCommand in", command)
        auto_command = command.copy()

        # Vx
        if self.ttc < 0.25:
            vx_desired = 0.0
            print ("\n\n\n\nSTOP\n\n\n\n")
        else:
            vx_desired = 10.0
        auto_command[1] = -self.vx_pid.computePID(vx_desired, self.states[6], 1./30)
        # auto_command[1] = -self.vx_pid.computePID(command[1].copy()*10.0, self.states[6], 1./30)

        # Use OF to compute vy desired
        k = 2.0
        vy_desired = k*self.lr_diff
        # print ("vy desired: ", vy_desired)
        auto_command[0] = -self.vy_pid.computePID(vy_desired, self.states[7], 1./30)
        # auto_command[0] = -self.vy_pid.computePID(command[0].copy()*10.0, self.states[7], 1./30)
        # print ("vx desired", command[1].copy()*5.0)
        # print ("vx true", self.states[6])
        # print ("vy desired", command[0].copy()*5.0)
        # print ("vy true", self.states[7])
        # auto_command[2] = self.PID_yaw.computePID(0., self.states[5], 1./30)
        # print ("auto_command", auto_command)

        # # Use middle pixels to control yaw
        # if abs(self.mlr_diff) > 0.2:
        #     auto_command[2] = 10.0*self.mlr_diff
        #     # print ("control yaw")
        # else:
        #     auto_command[2] = 0.0
        #     # print ("no")

        # Yaw control
        auto_command[2] = self.PID_yaw.computePID(self.yaw_command, self.states[5], 1./30)

        # Altitude control
        # k_alt = 1.0
        # hdot = k_alt*(5. - self.bot_diff)
        # self.alt_command += (1./30) * hdot
        # auto_command[3] = command[3] + self.alt_command
        # print ("altitude command: ", auto_command[3])

        return auto_command

    def image_filters(self, pixels):
        """Display camera image in a window. """
        # Get grayscale of new img.
        gray_img = cv2.cvtColor(pixels, cv2.COLOR_BGR2GRAY)

        # # Calulate the optical flow at our grid of points.
        # grid_points = (256, 256)
        # points, status, err = cv2.calcOpticalFlowPyrLK(self.prev_frame, gray_img, grid_points, None)
        # print ("points", points)

        pixels = self.compute_lr_diff(pixels, gray_img)
        pixels = self.compute_mlr_diff(pixels, gray_img)
        pixels = self.compute_bot_diff(pixels, gray_img)
        pixels = self.compute_ttc(pixels, gray_img)

        # Print command on screen
        # if self.lr_diff > 0:
        #     print ("Move Right!")
        # else:
        #     print ("Move Left!")
        # if self.mlr_diff > 0:
        #     print ("Yaw Right!")
        # else:
        #     print ("Yaw Left!")

        # Draw circles at points we are compute OF
        for x, y in self.mleft_pixels.reshape(-1,2):
            cv2.circle(pixels, (x, y), 2, (255, 0, 0), -1)
        for x, y in self.mright_pixels.reshape(-1,2):
            cv2.circle(pixels, (x, y), 2, (255, 0, 0), -1)
        for x, y in self.bot_pixels.reshape(-1,2):
            cv2.circle(pixels, (x, y), 2, (0, 0, 255), -1)

        # Draw rectangles
        cv2.rectangle(pixels, (512, 128), (384, 384), (0, 255, 0), 1)
        cv2.rectangle(pixels, (0, 128), (128, 384), (0, 255, 0), 1)


        # Display image in its own window.
        cv2.imshow('Holodeck Image', pixels)

    def compute_lr_diff(self, pixels, gray_img):
        # params for ShiTomasi corner detection
        feature_params = dict( maxCorners = 100,
                               qualityLevel = 0.3,
                               minDistance = 7,
                               blockSize = 7 )
        # Parameters for lucas kanade optical flow
        lk_params = dict( winSize  = (15,15),
                          maxLevel = 2,
                          criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))
        # Create some random colors
        color = np.random.randint(0,255,(100,3))

        # Compute left OF.
        p1_left, st, err = cv2.calcOpticalFlowPyrLK(self.prev_frame, gray_img, self.left_pixels.astype(np.float32), None, **lk_params)
        diff_left = p1_left - self.left_pixels
        left_px_mean = np.sum(diff_left[:,0,0])/len(diff_left[:,0,0])
        left_py_mean = np.sum(diff_left[:,0,1])/len(diff_left[:,0,0])
        # print ("\nLeft means: \n", left_px_mean, "\n", left_py_mean)


        # Compute right OF.
        p1_right, st, err = cv2.calcOpticalFlowPyrLK(self.prev_frame, gray_img, self.right_pixels.astype(np.float32), None, **lk_params)
        diff_right = p1_right - self.right_pixels
        right_px_mean = np.sum(diff_right[:,0,0])/len(diff_right[:,0,0])
        right_py_mean = np.sum(diff_right[:,0,1])/len(diff_right[:,0,0])
        # print ("\nright means: \n", right_px_mean, "\n", right_py_mean)

        # Compute difference of OF
        right_mean_mag = np.sqrt(right_px_mean**2 + right_py_mean**2)
        left_mean_mag = np.sqrt(left_px_mean**2 + left_py_mean**2)
        self.lr_diff = (left_mean_mag - right_mean_mag)/(left_mean_mag + right_mean_mag)
        # self.lr_diff = (left_px_mean - right_px_mean)/(left_px_mean + right_px_mean)
        # print ("LR diff: ", self.lr_diff)

        # Draw OF vector
        scale = 10.
        cv2.arrowedLine(pixels, (64, 256), (int(64 + scale*left_px_mean), int(256 + scale*left_py_mean)), (0, 255, 0), 2)
        cv2.arrowedLine(pixels, (512-64, 256), (int(512 - 64 + scale*right_px_mean), int(256 + scale*right_py_mean)), (0, 255, 0), 2)

        return pixels

    def compute_mlr_diff(self, pixels, gray_img):
        # params for ShiTomasi corner detection
        feature_params = dict( maxCorners = 100,
                               qualityLevel = 0.3,
                               minDistance = 7,
                               blockSize = 7 )
        # Parameters for lucas kanade optical flow
        lk_params = dict( winSize  = (15,15),
                          maxLevel = 2,
                          criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))

        # Compute left OF.
        p1_left, st, err = cv2.calcOpticalFlowPyrLK(self.prev_frame, gray_img, self.mleft_pixels.astype(np.float32), None, **lk_params)
        diff_left = p1_left - self.mleft_pixels
        left_px_mean = np.sum(diff_left[:,0,0])/len(diff_left[:,0,0])
        left_py_mean = np.sum(diff_left[:,0,1])/len(diff_left[:,0,0])
        # print ("\nLeft means: \n", left_px_mean, "\n", left_py_mean)


        # Compute right OF.
        p1_right, st, err = cv2.calcOpticalFlowPyrLK(self.prev_frame, gray_img, self.mright_pixels.astype(np.float32), None, **lk_params)
        diff_right = p1_right - self.mright_pixels
        right_px_mean = np.sum(diff_right[:,0,0])/len(diff_right[:,0,0])
        right_py_mean = np.sum(diff_right[:,0,1])/len(diff_right[:,0,0])
        # print ("\nright means: \n", right_px_mean, "\n", right_py_mean)

        # Compute difference of OF
        right_mean_mag = np.sqrt(right_px_mean**2 + right_py_mean**2)
        left_mean_mag = np.sqrt(left_px_mean**2 + left_py_mean**2)
        # self.lr_diff = (left_mean_mag - right_mean_mag)/(left_mean_mag + right_mean_mag)
        self.mlr_diff = (abs(left_px_mean) - abs(right_px_mean))/(abs(left_px_mean) + abs(right_px_mean))
        # print ("LR diff: ", self.lr_diff)

        # Draw OF vector
        scale = 10.
        cv2.arrowedLine(pixels, (192, 256), (int(192 + scale*left_px_mean), int(256 + scale*left_py_mean)), (255, 0, 0), 2)
        cv2.arrowedLine(pixels, (256+64, 256), (int(256 + 64 + scale*right_px_mean), int(256 + scale*right_py_mean)), (255, 0, 0), 2)

        return pixels

    def compute_bot_diff(self, pixels, gray_img):
        # params for ShiTomasi corner detection
        feature_params = dict( maxCorners = 100,
                               qualityLevel = 0.3,
                               minDistance = 7,
                               blockSize = 7 )
        # Parameters for lucas kanade optical flow
        lk_params = dict( winSize  = (15,15),
                          maxLevel = 2,
                          criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))

        # Compute left OF.
        p1_bot, st, err = cv2.calcOpticalFlowPyrLK(self.prev_frame, gray_img, self.bot_pixels.astype(np.float32), None, **lk_params)
        diff_bot = p1_bot - self.bot_pixels
        bot_px_mean = np.sum(diff_bot[:,0,0])/len(diff_bot[:,0,0])
        bot_py_mean = np.sum(diff_bot[:,0,1])/len(diff_bot[:,0,0])
        # print ("\nLeft means: \n", left_px_mean, "\n", left_py_mean)


        # # Compute difference of OF
        bot_mean_mag = np.sqrt(bot_px_mean**2 + bot_py_mean**2)
        # left_mean_mag = np.sqrt(left_px_mean**2 + left_py_mean**2)
        # # self.lr_diff = (left_mean_mag - right_mean_mag)/(left_mean_mag + right_mean_mag)
        # self.mlr_diff = (abs(left_px_mean) - abs(right_px_mean))/(abs(left_px_mean) + abs(right_px_mean))
        self.bot_diff = bot_mean_mag
        # print ("Bot y: ", bot_mean_mag)
        #
        # # Draw OF vector
        scale = 10.
        cv2.arrowedLine(pixels, (256, 384+64), (int(256 + scale*bot_px_mean), int(384 + 64 + scale*bot_py_mean)), (0, 0, 255), 2)
        # cv2.arrowedLine(pixels, (256+64, 256), (int(256 + 64 + scale*right_px_mean), int(256 + scale*right_py_mean)), (255, 0, 0), 2)

        return pixels

    def compute_ttc(self, pixels, gray_img):
        # params for ShiTomasi corner detection
        feature_params = dict( maxCorners = 100,
                               qualityLevel = 0.3,
                               minDistance = 7,
                               blockSize = 7 )
        # Parameters for lucas kanade optical flow
        lk_params = dict( winSize  = (15,15),
                          maxLevel = 2,
                          criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))

        # Compute left OF.
        p1_col, st, err = cv2.calcOpticalFlowPyrLK(self.prev_frame, gray_img, self.col_pixels.astype(np.float32), None, **lk_params)
        diff_col = p1_col - self.col_pixels
        # print ("ttc: ", diff_col)
        # print ("ttc pix: ", self.col_pixels)

        sx = (self.col_pixels[:,0] - 255)
        sxdot = diff_col[:,0]*30.
        ttc = abs(np.divide(sx, sxdot))
        # print ("sx: ",sx)
        # print ("sx dot: ", sxdot)
        print ("ttc: ", ttc.mean())
        self.ttc = ttc.mean()

        # col_px_mean = np.sum(diff_col[:,0,0])/len(diff_col[:,0,0])
        # # bot_py_mean = np.sum(diff_bot[:,0,1])/len(diff_bot[:,0,0])
        # # print ("\nLeft means: \n", left_px_mean, "\n", left_py_mean)
        #
        #
        # # # Compute difference of OF
        # bot_mean_mag = np.sqrt(bot_px_mean**2 + bot_py_mean**2)
        # # left_mean_mag = np.sqrt(left_px_mean**2 + left_py_mean**2)
        # # # self.lr_diff = (left_mean_mag - right_mean_mag)/(left_mean_mag + right_mean_mag)
        # # self.mlr_diff = (abs(left_px_mean) - abs(right_px_mean))/(abs(left_px_mean) + abs(right_px_mean))
        # self.bot_diff = bot_mean_mag
        # print ("Bot y: ", bot_mean_mag)
        # #
        # # # Draw OF vector
        # scale = 10.
        # cv2.arrowedLine(pixels, (256, 384+64), (int(256 + scale*bot_px_mean), int(384 + 64 + scale*bot_py_mean)), (0, 0, 255), 2)
        # cv2.arrowedLine(pixels, (256+64, 256), (int(256 + 64 + scale*right_px_mean), int(256 + scale*right_py_mean)), (255, 0, 0), 2)

        return pixels

    def process_sensors(self, state):
        """Put sensor data into state vector. """
        # Orientation Sensor
        orientation = state[Sensors.ORIENTATION_SENSOR]

        # Make orientation 4x4 homogenous matrix.
        homo_orientation = np.diag([1., 1., 1., 1.])
        homo_orientation[0:3, 0:3] = orientation

        # Convert homo_orientation to roll, pitch, yaw as we know.
        rpy = tf.euler_from_matrix(homo_orientation, axes='rxyz')

        # Position Sensor
        position = state[Sensors.LOCATION_SENSOR]

        # Velocity Sensor
        vel = state[Sensors.VELOCITY_SENSOR]

        # Rotate velocities to get vx, vy
        psi_rot = tf.euler_matrix(0, 0, rpy[2])
        rotated_vel = (psi_rot[0:3,0:3].dot(vel))/100.

        # IMU Sensor
        imu = state[Sensors.IMU_SENSOR]

        # Save off states in array.
        self.states[0:3] = np.resize(position, (3,))
        self.states[3:6] = np.resize(rpy, (3,))
        self.states[6:9] = np.resize(rotated_vel, (3,))
        self.states[9:12] = np.resize(imu[3:6], (3,))
    # Lets Fly :)
    dt = 0.01
    t = 0.0

    # Altitude Hold
    throttle_eq = quad.mass * 9.81 / quad.max_F
    alt_controller_ = PID(p=0.6, i=0.00, d=-0.4)
    alt_c = 10.0

    for i in range(10000):
        t += dt
        phi_c = 0.0
        theta_c = 0.0
        psirate_c = 0.0
        throttle_c = alt_controller_.computePID(
            alt_c, -quad.x[2], dt) + throttle_eq  # eq force = -28.01736
        quad.force_and_moments(phi_c, theta_c, psirate_c, throttle_c, dt)
        if (i % 10 == 0):
            plotter.plot(quad.x)
            print "--------------------"
            print "iteration #", i
            print "pos:", quad.x[0], quad.x[1], quad.x[2], quad.x[5]
            print "rot:", quad.x[6], quad.x[7], quad.x[8]
            print "time: ", t
            print "reward: ", quad.reward
        else:
            time.sleep(0.01)
        if quad.reward > 1000:
            alt_c = 0.
        if quad.reward > 2000:
            alt_c = 20.
class Quadcopter:
    def __init__(self, waypoints, x_init=None):

        # Waypoints
        self.waypoints = waypoints
        self.current_wp = waypoints[0, :]
        self.wps_complete = 0
        self.reward = 0.
        self.wp_accept = 0.25  # acceptable distance to pass off waypoint

        # Dynamics params
        self.mass = 2.856
        self.linear_mu = 0.2
        self.angular_mu = 0.3
        self.ground_effect = [-55.3516, 181.8265, -203.9874, 85.3735, -7.6619]

        self.max_l = 6.5080
        self.max_m = 5.087
        self.max_n = 0.25
        self.max_F = 59.844

        self.Jx = 0.07
        self.Jy = 0.08
        self.Jz = 0.12

        self.J = np.diag([self.Jx, self.Jy, self.Jz])

        # Filter Outputs
        self.tau_up_l = 0.1904
        self.tau_up_m = 0.1904
        self.tau_up_n = 0.1644
        self.tau_up_F = 0.1644
        self.tau_down_l = 0.1904
        self.tau_down_m = 0.1904
        self.tau_down_n = 0.2164
        self.tau_down_F = 0.2164

        # Wind
        self.wind_n = 0.0
        self.wind_e = 0.0
        self.wind_d = 0.0

        # Definite intial conditions
        if x_init == None:
            self.x = np.zeros((12, 1))

        # Init controllers
        self.roll_controller_ = PID(p=0.1, i=0.0, d=0.0)
        self.pitch_controller_ = PID(p=0.1, i=0.0, d=0.0)
        self.yaw_controller_ = PID(p=0.1, i=0.0, d=0.0)

        # Forces
        self.desired_forces_ = Forces()
        self.actual_forces_ = Forces()
        self.applied_forces_ = Forces()

    def force_and_moments(self, phi_c, theta_c, psi_rate_c, throttle, dt):

        # unpack state
        phi = self.x[6]
        theta = self.x[7]
        # psi = self.x[8]
        p = self.x[9]
        q = self.x[10]
        r = self.x[11]

        # Compute desired forces
        self.desired_forces_.l = self.roll_controller_.computePID(
            phi_c, phi, dt, p)
        self.desired_forces_.m = self.roll_controller_.computePID(
            theta_c, theta, dt, q)
        self.desired_forces_.n = self.roll_controller_.computePID(
            psi_rate_c, r, dt)
        self.desired_forces_.Fz = throttle * self.max_F

        # Calc acutal output with low-pass filters
        taul = self.tau_up_l if (self.desired_forces_.l >
                                 self.applied_forces_.l) else self.tau_down_l
        taum = self.tau_up_m if (self.desired_forces_.m >
                                 self.applied_forces_.m) else self.tau_down_m
        taun = self.tau_up_n if (self.desired_forces_.n >
                                 self.applied_forces_.n) else self.tau_down_n
        tauF = self.tau_up_F if (self.desired_forces_.Fz >
                                 self.applied_forces_.Fz) else self.tau_down_F

        # Calc alpha for filter
        alphal = dt / (taul + dt)
        alpham = dt / (taum + dt)
        alphan = dt / (taun + dt)
        alphaF = dt / (tauF + dt)

        # Apply discrete first-order filter
        self.applied_forces_.l = self.sat(
            (1 - alphal) * self.applied_forces_.l +
            alphal * self.desired_forces_.l, self.max_l, -1.0 * self.max_l)
        self.applied_forces_.m = self.sat(
            (1 - alpham) * self.applied_forces_.m +
            alpham * self.desired_forces_.m, self.max_m, -1.0 * self.max_m)
        self.applied_forces_.n = self.sat(
            (1 - alphan) * self.applied_forces_.n +
            alphan * self.desired_forces_.n, self.max_n, -1.0 * self.max_n)
        self.applied_forces_.Fz = self.sat(
            (1 - alphaF) * self.applied_forces_.Fz +
            alphaF * self.desired_forces_.Fz, self.max_F, 0.0)

        # print "applied", self.applied_forces_.n
        # TODO add ground effect
        ground_effect = 0.0

        # TODO add Wind effect
        ur = 0.0
        vr = 0.0
        wr = 0.0

        # Apply other forces (i.e. wind)
        self.actual_forces_.Fx = -1.0 * self.linear_mu * ur
        self.actual_forces_.Fy = -1.0 * self.linear_mu * vr
        self.actual_forces_.Fz = -1.0 * self.linear_mu * wr - self.applied_forces_.Fz - ground_effect
        self.actual_forces_.l = -1.0 * self.angular_mu * p + self.applied_forces_.l
        self.actual_forces_.m = -1.0 * self.angular_mu * q + self.applied_forces_.m
        self.actual_forces_.n = -1.0 * self.angular_mu * r + self.applied_forces_.n
        # print "actual", self.actual_forces_.n
        k1 = self.derivatives(self.x)
        k2 = self.derivatives(self.x + (dt / 2.) * k1)
        k3 = self.derivatives(self.x + (dt / 2.) * k2)
        k4 = self.derivatives(self.x + (dt) * k3)

        self.x += (dt / 6.) * (k1 + 2 * k2 + 2 * k3 + k4)

        # Compute Reward

        # Compute distance to current waypoint
        dist = sqrt((self.current_wp[0] - self.x[0])**2 +
                    (self.current_wp[1] - self.x[1])**2 +
                    (self.current_wp[2] - self.x[2])**2)

        if (dist < self.wp_accept):
            if self.wps_complete == (len(self.waypoints[:, 0]) - 1):
                self.reward = 1e10
            else:
                self.wps_complete += 1
                self.current_wp = self.waypoints[self.wps_complete, :]
                self.reward = self.wps_complete * 1000.
        else:
            self.reward = self.wps_complete * 1000. + (100. - dist) * 10.

    def derivatives(self, state):

        # Unpack state
        pn = state[0]
        pe = state[1]
        pd = state[2]
        u = state[3]
        v = state[4]
        w = state[5]
        phi = state[6]
        theta = state[7]
        psi = state[8]
        p = state[9]
        q = state[10]
        r = state[11]

        # pos = np.array([[pn], [pe], [pd]])
        pos = np.array([pn, pe, pd])
        vel = np.array([u, v, w])
        # att = np.array([phi, theta, psi])
        ang_vel = np.array([p, q, r])
        force = np.array([
            self.actual_forces_.Fx, self.actual_forces_.Fy,
            self.actual_forces_.Fz
        ])
        torque = np.array([
            self.actual_forces_.l / self.Jx, self.actual_forces_.m / self.Jy,
            self.actual_forces_.n / self.Jz
        ])
        force = np.reshape(force, (3, 1))

        # position dot
        # Calc trigs
        cp = cos(phi)
        sp = sin(phi)
        ct = cos(theta)
        st = sin(theta)
        tt = tan(theta)
        cpsi = cos(psi)
        spsi = sin(psi)

        # calc rotation matrix
        # R_body_veh = np.array([[ct*cpsi, ct*spsi, -st],
        #                 [sp*st*cpsi-cp*spsi, sp*st*spsi+cp*cpsi, sp*ct],
        #                 [cp*st*cpsi+sp*spsi, cp*st*spsi-sp*cpsi, cp*ct]])

        grav = 9.81
        fg_body = np.array([
            -self.mass * grav * st, self.mass * grav * ct * sp,
            self.mass * grav * ct * cp
        ])
        fg_body = np.reshape(fg_body, (3, 1))

        rot_posdot = np.array([[
            ct * cpsi, sp * st * cpsi - cp * spsi, cp * st * cpsi + sp * spsi
        ], [ct * spsi, sp * st * spsi + cp * cpsi, cp * st * spsi - sp * cpsi],
                               [st, -sp * ct, -cp * ct]])

        pos_dot = np.matmul(rot_posdot, vel)
        vel_dot = np.array([
            r * v - q * w, p * w - r * u, q * u - p * v
        ]) + (1. / self.mass) * force + (1. / self.mass) * fg_body
        vel_dot = np.reshape(vel_dot, (3, 1))
        rot_attdot = np.array([[1., sp * tt, cp * tt], [0., cp, -sp],
                               [0., (sp / ct), (cp / ct)]])
        att_dot = np.matmul(rot_attdot, ang_vel)
        ang_veldot = np.array([((self.Jy - self.Jz) / self.Jx) * q * r,
                               ((self.Jz - self.Jx) / self.Jy) * p * r,
                               ((self.Jx - self.Jy) / self.Jz) * p * q
                               ]) + torque

        # xdot
        xdot = np.zeros((12, 1))
        xdot[0:3] = pos_dot
        xdot[2] = -xdot[2]  # convert from hdot to pddot
        xdot[3:6] = vel_dot
        xdot[6:9] = att_dot
        xdot[9:] = ang_veldot

        return xdot

    def sat(self, x, _max, _min):
        if (x > _max):
            # print "SAT MAX"
            return _max
        elif (x < _min):
            # print "SAT MIN"
            return _min
        else:
            return x
Example #4
0
class Controller(object):
    def __init__(self):

        # load ROS params

        # PID gains
        u_P = rospy.get_param('~u_P', 0.2)
        u_I = rospy.get_param('~u_I', 0.0)
        u_D = rospy.get_param('~u_D', 0.01)

        v_P = rospy.get_param('~v_P', 0.2)
        v_I = rospy.get_param('~v_I', 0.0)
        v_D = rospy.get_param('~v_D', 0.01)

        w_P = rospy.get_param('~w_P', 3.0)
        w_I = rospy.get_param('~w_I', 0.05)
        w_D = rospy.get_param('~w_D', 0.5)

        x_P = rospy.get_param('~x_P', 0.5)
        x_I = rospy.get_param('~x_I', 0.01)
        x_D = rospy.get_param('~x_D', 0.1)

        y_P = rospy.get_param('~y_P', 0.5)
        y_I = rospy.get_param('~y_I', 0.01)
        y_D = rospy.get_param('~y_D', 0.1)

        z_P = rospy.get_param('~z_P', 1.0)
        z_I = rospy.get_param('~z_I', 0.1)
        z_D = rospy.get_param('~z_D', 0.4)

        psi_P = rospy.get_param('~psi_P', 0.5)
        psi_I = rospy.get_param('~psi_I', 0.0)
        psi_D = rospy.get_param('~psi_D', 0.0)

        tau = rospy.get_param('~tau', 0.04)

        # quadcopter params
        self.max_thrust = rospy.get_param('dynamics/max_F', 60.0)
        self.mass = rospy.get_param('dynamics/mass', 3.0)
        self.thrust_eq = (9.80665 * self.mass) / self.max_thrust
        self.drag_constant = rospy.get_param('dynamics/linear_mu', 0.1)

        # initialize state variables
        self.pn = 0.0
        self.pe = 0.0
        self.pd = 0.0

        self.phi = 0.0
        self.theta = 0.0
        self.psi = 0.0

        self.u = 0.0
        self.v = 0.0
        self.w = 0.0

        self.p = 0.0
        self.q = 0.0
        self.r = 0.0

        self.ax = 0.0
        self.ay = 0.0
        self.az = 0.0

        self.throttle = 0.0

        # initialize command variables
        self.xc_pn = 0.0
        self.xc_pe = 0.0
        self.xc_pd = 0.0

        self.xc_phi = 0.0
        self.xc_theta = 0.0
        self.xc_psi = 0.0

        self.xc_u = 0.0
        self.xc_v = 0.0
        self.xc_pd = 0.0
        self.xc_r = 0.0

        self.xc_ax = 0.0
        self.xc_ay = 0.0
        self.xc_az = 0.0
        self.xc_r = 0.0

        self.xc_throttle = 0.0

        # initialize saturation values
        self.max_roll = rospy.get_param('~max_roll', 0.15)
        self.max_pitch = rospy.get_param('~max_pitch', 0.15)
        self.max_yaw_rate = rospy.get_param('~max_yaw_rate', np.radians(45.0))
        self.max_throttle = rospy.get_param('~max_throttle', 1.0)
        self.max_u = rospy.get_param('~max_u', 1.0)
        self.max_v = rospy.get_param('~max_v', 1.0)
        self.max_w = rospy.get_param('~max_x', 1.0)

        # initialize PID controllers
        self.PID_u = PID(u_P, u_I, u_D, None, None, tau)
        self.PID_v = PID(v_P, v_I, v_D, None, None, tau)
        self.PID_w = PID(w_P, w_I, w_D, None, None, tau)
        self.PID_x = PID(x_P, x_I, x_D, None, None, tau)
        self.PID_y = PID(y_P, y_I, y_D, None, None, tau)
        self.PID_z = PID(z_P, z_I, z_D, None, None, tau)
        self.PID_psi = PID(psi_P, psi_I, psi_D, None, None, tau)

        # initialize other class variables
        self.prev_time = 0.0
        self.is_flying = False
        self.control_mode = 4  # MODE_XPOS_YPOS_YAW_ALTITUDE
        self.ibvs_active = False

        self.command = Command()

        # dynamic reconfigure
        self.server = Server(ControllerConfig, self.reconfigure_callback)

        # initialize subscribers
        self.state_sub = rospy.Subscriber('estimate', Odometry,
                                          self.state_callback)
        self.is_flying_sub = rospy.Subscriber('is_flying', Bool,
                                              self.is_flying_callback)
        self.cmd_sub = rospy.Subscriber('high_level_command', Command,
                                        self.cmd_callback)
        self.ibvs_active_sub = rospy.Subscriber('ibvs_active', Bool,
                                                self.ibvs_active_callback)

        # initialize publishers
        self.command_pub = rospy.Publisher('command', Command, queue_size=10)

        # initialize timer
        self.update_rate = 200.0
        self.update_timer = rospy.Timer(rospy.Duration(1.0 / self.update_rate),
                                        self.send_command)

    def send_command(self, event):

        # t = time.time()  # for seeing how fast this runs

        if self.prev_time == 0:
            self.prev_time = rospy.get_time()
            return

        # get dt
        now = rospy.get_time()
        dt = now - self.prev_time
        self.prev_time = now

        if dt <= 0:
            return

        if self.is_flying:
            self.compute_control(dt)
            self.command_pub.publish(self.command)
        else:
            reset_integrators()
            self.prev_time = rospy.get_time()

        # elapsed = time.time() - t
        # hz_approx = 1.0/elapsed
        # print(hz_approx)

    def state_callback(self, msg):

        # this should already be coming in NED
        self.pn = msg.pose.pose.position.x
        self.pe = msg.pose.pose.position.y
        self.pd = msg.pose.pose.position.z

        self.u = msg.twist.twist.linear.x
        self.v = msg.twist.twist.linear.y
        self.w = msg.twist.twist.linear.z

        # convert quaternion to RPY
        quaternion = (msg.pose.pose.orientation.x, msg.pose.pose.orientation.y,
                      msg.pose.pose.orientation.z, msg.pose.pose.orientation.w)

        euler = tf.transformations.euler_from_quaternion(quaternion)
        self.phi = euler[0]
        self.theta = euler[1]
        self.psi = euler[2]

        self.p = msg.twist.twist.angular.x
        self.q = msg.twist.twist.angular.y
        self.r = msg.twist.twist.angular.z

    def is_flying_callback(self, msg):

        self.is_flying = msg.data

    def ibvs_active_callback(self, msg):

        self.ibvs_active = msg.data

    def cmd_callback(self, msg):

        mode = msg.mode

        if mode == Command.MODE_XPOS_YPOS_YAW_ALTITUDE:
            self.xc_pn = msg.x
            self.xc_pe = msg.y
            self.xc_pd = msg.F
            self.xc_psi = msg.z
            self.control_mode = mode

        elif mode == Command.MODE_XVEL_YVEL_YAWRATE_ALTITUDE:
            self.xc_u = msg.x
            self.xc_v = msg.y
            self.xc_pd = msg.F
            self.xc_r = msg.z
            self.control_mode = mode

        elif mode == Command.MODE_XACC_YACC_YAWRATE_AZ:
            self.xc_ax = msg.x
            self.xc_ay = msg.y
            self.xc_az = msg.F
            self.xc_r = msg.z
            self.control_mode = mode

        elif mode == Command.MODE_ROLL_PITCH_YAWRATE_THROTTLE:
            self.xc_phi = msg.x
            self.xc_theta = msg.y
            self.xc_throttle = msg.F
            self.xc_r = msg.z
            self.control_mode = mode

        else:
            print('roscopter/controller: Unhandled command message of type {}'.
                  format(mode))

    def reconfigure_callback(self, config, level):

        tau = config.tau
        P = config.u_P
        I = config.u_I
        D = config.u_D
        self.PID_u.setGains(P, I, D, tau)

        P = config.v_P
        I = config.v_I
        D = config.v_D
        self.PID_v.setGains(P, I, D, tau)

        P = config.w_P
        I = config.w_I
        D = config.w_D
        self.PID_w.setGains(P, I, D, tau)

        P = config.x_P
        I = config.x_I
        D = config.x_D
        self.PID_x.setGains(P, I, D, tau)

        P = config.y_P
        I = config.y_I
        D = config.y_D
        self.PID_y.setGains(P, I, D, tau)

        P = config.z_P
        I = config.z_I
        D = config.z_D
        self.PID_z.setGains(P, I, D, tau)

        P = config.psi_P
        I = config.psi_I
        D = config.psi_D
        self.PID_psi.setGains(P, I, D, tau)

        self.max_roll = config.max_roll
        self.max_pitch = config.max_pitch
        self.max_yaw_rate = config.max_yaw_rate
        self.max_throttle = config.max_throttle
        self.max_u = config.max_u
        self.max_v = config.max_v
        self.max_w = config.max_w

        print('roscopter/controller: new gains')

        self.reset_integrators()

        return config

    def compute_control(self, dt):

        if dt <= 0.0000001:  # messes up derivative calculation in PID controllers
            return

        mode_flag = self.control_mode

        if mode_flag == Command.MODE_XPOS_YPOS_YAW_ALTITUDE:

            # figure out desired velocities (in inertial frame)
            # by running the position controllers
            pndot_c = self.PID_x.computePID(self.xc_pn, self.pn, dt)
            pedot_c = self.PID_y.computePID(self.xc_pe, self.pe, dt)

            # calculate desired yaw rate
            # first, determine the shortest direction to the commanded psi
            if abs(self.xc_psi + 2.0 * np.pi - self.psi) < abs(self.xc_psi -
                                                               self.psi):
                self.xc_psi += 2.0 * np.pi
            elif abs(self.xc_psi - 2.0 * np.pi - self.psi) < abs(self.xc_psi -
                                                                 self.psi):
                self.xc_psi -= 2.0 * np.pi

            self.xc_r = self.saturate(
                self.PID_psi.computePID(self.xc_psi, self.psi, dt),
                self.max_yaw_rate, -self.max_yaw_rate)

            # rotate into body frame
            # TODO: include pitch and role in this mapping
            self.xc_u = self.saturate(
                pndot_c * np.cos(self.psi) + pedot_c * np.sin(self.psi),
                self.max_u, -self.max_u)
            self.xc_v = self.saturate(
                -pndot_c * np.sin(self.psi) + pedot_c * np.cos(self.psi),
                self.max_v, -self.max_v)

            mode_flag = Command.MODE_XVEL_YVEL_YAWRATE_ALTITUDE

        if mode_flag == Command.MODE_XVEL_YVEL_YAWRATE_ALTITUDE:

            max_ax = np.sin(np.arccos(self.thrust_eq))
            max_ay = np.sin(np.arccos(self.thrust_eq))
            self.xc_ax = self.saturate(
                self.PID_u.computePID(self.xc_u, self.u, dt) +
                self.drag_constant * self.u / (9.80665 * self.mass), max_ax,
                -max_ax)
            self.xc_ay = self.saturate(
                self.PID_v.computePID(self.xc_v, self.v, dt) +
                self.drag_constant * self.v / (9.80665 * self.mass), max_ay,
                -max_ay)

            # nested loop for altitude
            pddot = -np.sin(self.theta) * self.u + np.sin(self.phi) * np.cos(
                self.theta) * self.v + np.cos(self.phi) * np.cos(
                    self.theta) * self.w

            # check to see if IBVS is active
            if self.ibvs_active:
                pddot_c = self.saturate(
                    self.xc_pd, self.max_w, -self.max_w
                )  # this term should be coming in as w and here we are assuming w is close enough to pddot
            else:
                pddot_c = self.saturate(
                    self.PID_w.computePID(self.xc_pd, self.pd, dt, pddot),
                    self.max_w, -self.max_w)

            self.xc_az = self.PID_z.computePID(pddot_c, pddot, dt)
            # print statement if you want
            mode_flag = Command.MODE_XACC_YACC_YAWRATE_AZ

        if mode_flag == Command.MODE_XACC_YACC_YAWRATE_AZ:

            # Model inversion (m[ax;ay;az] = m[0;0;g] + R'[0;0;-T]
            # This model tends to pop the MAV up in the air when a large change
            # in control is commanded as the MAV rotates to it's commanded attitude while also ramping up throttle.
            # It works quite well, but it is a little oversimplified.
            total_acc_c = np.sqrt(
                (1.0 - self.xc_az) * (1.0 - self.xc_az) +
                self.xc_ax * self.xc_ax + self.xc_ay * self.xc_ay)  #(in g's)
            # print('roscopter/controller: total_acc = {}'.format(total_acc_c))
            if total_acc_c > 0.001:
                self.xc_phi = np.arcsin(self.xc_ay / total_acc_c)
                self.xc_theta = -1.0 * np.arcsin(self.xc_ax / total_acc_c)
            else:
                self.xc_phi = 0.0
                self.xc_theta = 0.0

            # calculate actual throttle (saturate az to be falling at 1 g)
            max_az = 1.0 / self.thrust_eq
            self.xc_az = self.saturate(self.xc_az, 1.0, -max_az)
            total_acc_c = np.sqrt(
                (1.0 - self.xc_az) * (1.0 - self.xc_az) +
                self.xc_ax * self.xc_ax + self.xc_ay * self.xc_ay)  #(in g's)
            self.xc_throttle = total_acc_c * self.thrust_eq  # calculate the total thrust in normalized units

            # print '\nxc_az:', self.xc_az, '\nmax_az:', max_az, '\ntotal_acc_c:', total_acc_c, '\nthrottle:', self.xc_throttle

            mode_flag = Command.MODE_ROLL_PITCH_YAWRATE_THROTTLE

        if mode_flag == Command.MODE_ROLL_PITCH_YAWRATE_THROTTLE:

            # pack up and send the command
            self.command.mode = Command.MODE_ROLL_PITCH_YAWRATE_THROTTLE
            self.command.F = self.saturate(self.xc_throttle, self.max_throttle,
                                           0.0)
            self.command.x = self.saturate(self.xc_phi, self.max_roll,
                                           -self.max_roll)
            self.command.y = self.saturate(self.xc_theta, self.max_pitch,
                                           -self.max_pitch)
            self.command.z = self.saturate(self.xc_r, self.max_yaw_rate,
                                           -self.max_yaw_rate)

    def reset_integrators(self):

        self.PID_u.clearIntegrator()
        self.PID_v.clearIntegrator()
        self.PID_w.clearIntegrator()
        self.PID_x.clearIntegrator()
        self.PID_y.clearIntegrator()
        self.PID_z.clearIntegrator()
        self.PID_psi.clearIntegrator()

    def saturate(self, x, maximum, minimum):
        if (x > maximum):
            rVal = maximum
        elif (x < minimum):
            rVal = minimum
        else:
            rVal = x

        return rVal