import matplotlib.pyplot as plt
from matplotlib import style
import numpy as np
style.use('ggplot')


class Support_Vector_Machine:
    def _init_(self, visualization=True):               # you use 'self' on variables that can be referenced anywhere within the class (or object)
        self.visualization = visualization
        self.colors = {1:'r',-1:'b'}
        if self.visualization:
            self.fig = plt.figure()
            self.ax = self.fig.add_subplot(1,1,1)

    def fit(self, data):                                # this will be used to train our SVM. it is the optimization step
        self.data = data                                # this sets self.data to our data. and now we can reference it anywhere else within our class
        opt_dict = {}                                   # this is an optimization dictionary that contains all the optimization values
            # the dictionary will be { ||w|| : [w,b] }, and we'll choose the values of w and b for whichever one in the dict. that has the smallest key value (||w||)
        transforms = [[1,1],                            # we set these transforms to make sure we test every version of the vector possible
                      [-1,1],
                      [-1,-1],
                      [1,-1]]
        all_data = []                                  # reference our training data to pick some decent starting values
        for yi in self.data:
             for featureset in self.data[yi]:
                 for feature in featureset:
                     all_data.append(feature)
        self.max_feature_value = max(all_data)
        self.min_feature_value = min(all_data)
        all_data = None                                 # no need to keep this memory, so get rid of it

        step_sizes = [self.max_feature_value * 0.1,     # this sets the step size that we want to make when cycling through all our data. for our first cycle here, we'll take big steps (10%)
                      self.max_feature_value * 0.01,    # for our next cycles, we'll take smaller and smaller steps to continue finding the minimum. the more you step down, the more precise you get
                      self.max_feature_value * 0.001]

        b_range_multiple = 5                            # these variables help us take steps with b, but we don't care nearly as much about 'b' precision as we do 'w', so we take much larger steps
        b_multiple = 5
        latest_optimum = self.max_feature_value * 10

        for step in step_sizes:
            w = np.array([latest_optimum, latest_optimum])
            optimized = False                           # we reset this for each major step, and when we've checked all steps this will be true
            while not optimized:
                for b in np.arange(-1*(self.max_feature_value*b_range_multiple),    # here, we begin iterating thorugh possible b values with a constant step size
                                   self.max_feature_value*b_range_multiple,
                                   step*b_multiple):
                    for transformation in transforms:   # now we iterate through each of the transformations. 
                        w_t = w*transformation
                        found_option = True

                        # *** add a break here later *** -- b/c if any of the featuresets don't meet the constraints, then you toss out that variable as it doesn't fit. but this means we'd have to toss out our 'w' and 'b' values also since one bad value is enough to ruin it
                        for i in self.data:
                            for xi in self.data[i]:
                                yi = i
                                if not yi*(np.dot(w_t,xi)+b) >= 1:
                                           found_option = False
                        if found_option:
                            opt_dict[np.linalg.norm(w_t)] = [w_t,b]
                if w[0] < 0:                            # if w < 0, there's no reason to continue since we tested the negatives via the transformation
                    optimized = True
                    print ('Optimized a step.')
                else:
                    w = w - step

            norms = sorted([n for n in opt_dict])
            # ||w||: [w,b]
            opt_choice = opt_dict[norms[0]]
            self.w = opt_choice[0]
            self.b = opt_choice[1]
            latest_optimum = opt_choice[0][0]+step*2


    def predict(self, features):                        # this will predict the value of a new featureset once we've trained the classifier. This is the sign( x.w+b ) once we know what w and b are
        classification = np.sign(np.dot(np.array(features),self.w)+self.b)      # this is just sign( xi.w+b )
        if classification != 0 and self.visualization:
            self.ax.scatter(features[0],features[1],s=200,marker='*', c=self.colors[classification])
        else:
            print('featureset',features,'is on the decision boundary')
        return classification

    def visualize(self):
        [[self.ax.scatter(x[0],x[1],s=100,color=self.colors[i]) for x in data_dict[i]] for i in data_dict]      # this just graphs all our data points

        def hyperplane(x,w,b,v):
            return (-w[0]*x-b+v) / w[1]
        datarange = (self.min_feature_value*0.9,self.max_feature_value*1.1)
        hyp_x_min = datarange[0]
        hyp_x_min = datarange[1]

        # graphing the support vector hyperplane:
        # w.v + b = 1
        # pos sv hyperplane
        psv1 = hyperplane(hyp_x_min, self.w, self.b, 1)
        psv2 = hyperplane(hyp_x_max, self.w, self.b, 1)
        self.ax.plot([hyp_x_min,hyp_x_max], [psv1,psv2], "k")
        
        # then do the next two hyperplanes:
        # w.x + b = -1
        # negative sv hyperplane
        nsv1 = hyperplane(hyp_x_min, self.w, self.b, -1)
        nsv2 = hyperplane(hyp_x_max, self.w, self.b, -1)
        self.ax.plot([hyp_x_min,hyp_x_max], [nsv1,nsv2], "k")

        # w.x + b = 0
        # decision
        db1 = hyperplane(hyp_x_min, self.w, self.b, 0)
        db2 = hyperplane(hyp_x_max, self.w, self.b, 0)
        self.ax.plot([hyp_x_min,hyp_x_max], [db1,db2], "y--")

        plt.show()

data_dict = {-1:np.array([[1,7],
                          [2,8],
                          [3,8],]),
             1:np.array([[5,1],
                         [6,-1],
                         [7,3],])}

svm = Support_Vector_Machine()
svm.fit(data=data_dict)

predict_us = [[0,10],
              [1,3],
              [3,4],
              [3,5],
              [5,5],
              [5,6],
              [6,-5],
              [5,8]]

for p in predict_us:
    svm.predict(p)

svm.visualize()
