import numpy as np
import matplotlib.pyplot as plt


x = np.loadtxt(open("data/logistic_x.txt","r")) #input array
y = np.loadtxt(open("data/logistic_y.txt","r")) #target array

m = x.shape[0]                                  #number of training examples
x = np.c_[np.ones(m),x]                         #append intercept term to x
y = np.expand_dims(y, axis=1)                   #reshape y for array broadcasting
theta = np.zeros((x.shape[1],1))                #theta: parameter array

def sigmoid_function(x):
    x = np.clip( x, -500, 500 )                 #prevent overflow
    x = 1.0/( 1 + np.exp( -x ))
    return x

def gradient_J(theta,x,y):
    #gradJ: gradient of the logistic cost function with respect to the parameter array theta
    a = np.multiply(y,x)
    b = 1-sigmoid_function(np.multiply(y,np.dot(x,theta)))
    grad_J = (-1/m)*np.sum(np.multiply(a,b),axis=0)
    if theta.shape[0] != grad_J.shape[0]:
        print("The dimensions of the gradient of the cost function J with respect to the vector theta are incorrect.")
    return grad_J

def hessian_J(theta,x,y):
    a = sigmoid_function(y*np.dot(x,theta))
    right_term = a*(1-a)
    left_term = np.einsum("ij,ik->ijk",x,x)
    H = np.einsum('ijk,il->jk',left_term,right_term)
    return H/m

def rel_error(x, y):
  """ returns relative error """
  return np.max(np.abs(x - y) / (np.maximum(1e-8, np.abs(x) + np.abs(y))))

def newton(theta,x,y,tolerance=0.01,itercount=False):
    niter=0
    while True:
        G = gradient_J(theta,x,y)
        H = hessian_J(theta,x,y)
        c = np.expand_dims(np.dot(np.linalg.inv(H),G),axis=1)
        theta_1 = theta - c
        if (rel_error(theta_1,theta)<tolerance):
            break
        theta =  theta_1
        niter+=1
    if itercount:
        print('Number of loop iterations:',niter)
    return theta
theta = newton(theta,x,y)
print("parameter vector theta:",theta)

#Plot Training data and decision boundary fit by logistic regression

#plt.title("Training data and decision boundary fit by logistic regression")
plus = np.where(y>0)[0]
minus = np.where(y<0)[0]
plt.scatter(x[plus,1],x[plus,2],color = "r", marker="+",label="y= 1")
plt.scatter(x[minus,1],x[minus,2],color="b", marker="_",label="y= -1")
h = np.dot(x,theta)
plt.xlabel("x1")
plt.ylabel("x2")
def boundary(theta,x):
    a = -theta[1]/theta[2]
    b = -theta[0]/theta[2]
    return a*x + b 
# define bounds of the domain
min1, max1 = x[:, 1].min()-1, x[:, 1].max()+1
min2, max2 = x[:, 2].min()-1, x[:, 2].max()+1
#plot decision boundary
xbound = np.arange(min1,max1,0.1)
ybound = boundary(theta,xbound)
plt.plot(xbound,ybound,label="decision boundary")
plt.legend()