import numpy as np

import time
import util as u

# simulation parameters
dt = 0.1       # time step
T = 3000       # number of time steps

# parameters of the system
gamma = .00002 # gravitational coefficient
m1 = 300       # mass of the first planet
m2 = 1         # mass of the second planet

# initial conditions: state of the system in the first two time steps
x0 = np.array([0, 0, 1, 0])
x1 = np.array([0, 0, 1, 0.005])

# initializing the vector of system states
xs = [x0, x1]

# defining the function that calculates the forces acting on the planets at a time step
def F(x):
    r1 = x[:2]   # position of the first planet
    r2 = x[2:4]  # position of the second planet

    posdiff_vec = r2 - r1                 # vector of difference of the positions
    dist = np.linalg.norm(posdiff_vec)    # distance between the planets

    # Newton's law of universal gravitation
    F1 = gamma * m2 * posdiff_vec / (dist**2)    # force acting on the first planet
    F2 = - gamma * m1 * posdiff_vec / (dist**2)  # force acting on the second planet

    return np.concatenate([F1, F2], axis=0)

for i in range(T):
    # getting previous values of the iteration
    Lx = xs[-1]
    LLx = xs[-2]

    # approximating the second derivative with finite differences
    F_prev = F(Lx)                  # force acting at the previous time step
    x = dt**2*F_prev + 2*Lx - LLx   # estimated position at the next time step

    xs.append(x)

####################################################

x, y = np.split(np.array(xs), 2, axis=1)

u.plotAnim(x, y, T, isSaveVideo=False)

# this is slow and not working properly
#u.plotScatter(x, y)


from math import sqrt
import numpy as np
from collections import Counter
import warnings
import pandas as pd
import random

def k_nearest_neighbors(data, predict, k=3) :
	if len(data) >= k:
		warnings.warn('K is set to value less than total voting groups!')
	distances = []
	for group in data :
		for features in data[group]:
			eculidean_distance = np.linalg.norm(np.array(features)- np.array(predict))
			distances.append([eculidean_distance, group])

	votes = [i[1] for i in sorted(distances)[:k]]
	# print(Counter(votes).most_common(1))
	vote_result = Counter(votes).most_common(1)[0][0]
	return vote_result

df = pd.read_csv("breast-cancer-wisconsin.data")
df.replace("?", -99999, inplace = True)
df.drop(['id'], 1, inplace = True)
full_data = df.astype(float).values.tolist()
# print(full_data[:10])
# print(30*"-")
random.shuffle(full_data)

test_size = 0.2
train_set = {2:[], 4:[]}
test_set = {2:[], 4:[]}
train_data = full_data[:-int(test_size* len(full_data))]
test_data = full_data[-int(test_size* len(full_data)):]

for i in train_data:
	train_set[i[-1]].append(i[:-1])

for i in test_data:
	test_set[i[-1]].append(i[:-1])

# print(full_data[:10])

correct = 0
total = 0

for group in test_set:
	for data in test_set[group]:
		vote = k_nearest_neighbors(train_set, data, k =5)
		if group == vote:
			correct += 1
		total += 1

print("accuracy", correct/total)


import numpy as np
from scipy import stats

class GaussianMixtureModel():
    """Density estimation with Gaussian Mixture Models (GMM).

    You can add new functions if you find it useful, but **do not** change
    the names or argument lists of the functions provided.
    """
    def __init__(self, X, K):
        """Initialise GMM class.

        Arguments:
          X -- data, N x D array
          K -- number of mixture components, int
        """
        self.X = X
        self.n = X.shape[0]
        self.D = X.shape[1]
        self.K = K

    def E_step(self, mu, S, pi):
        """Compute the E step of the EM algorithm.

        Arguments:
          mu -- component means, K x D array
          S -- component covariances, K x D x D array
          pi -- component weights, K x 1 array

        Returns:
          r_new -- updated component responsabilities, N x K array
        """
        # Assert that all arguments have the right shape
        assert(mu.shape == (self.K, self.D) and\
               S.shape  == (self.K, self.D, self.D) and\
               pi.shape == (self.K, 1))
        r_new = np.zeros((self.n, self.K))

        # Task 1: implement the E step and return updated responsabilities
        # Write your code from here...
        for i in range(self.n):
            som=0
            for k in range(self.K):
                som+=pi[k]*stats.multivariate_normal.pdf(self.X[i,:],mu[k,:],S[k,:,:])
            for k in range(self.K):
                r_new[i,k]=pi[k]*stats.multivariate_normal.pdf(self.X[i,:],mu[k,:],S[k,:,:])/som

        # ... to here.
        assert(r_new.shape == (self.n, self.K))
        return r_new

    def M_step(self, mu, r):
        """Compute the M step of the EM algorithm.

        Arguments:
          mu -- previous component means, K x D array
          r -- previous component responsabilities,  N x K array

        Returns:
          mu_new -- updated component means, K x D array
          S_new -- updated component covariances, K x D x D array
          pi_new -- updated component weights, K x 1 array
        """
        assert(mu.shape == (self.K, self.D) and\
               r.shape  == (self.n, self.K))
        mu_new = np.zeros((self.K, self.D))
        S_new  = np.zeros((self.K, self.D, self.D))
        pi_new = np.zeros((self.K, 1))

        # Task 2: implement the M step and return updated mixture parameters
        # Write your code from here...
        for k in range(self.K):
            Nk=0
            som1=np.zeros((self.K, self.D))
            som2=np.zeros((self.K, self.D, self.D))
            for i in range(self.n):
                Nk+=r[i,k]
                som1[k,:]+=r[i,k]*self.X[i,:]
            mu_new[k,:]=som1[k,:]/Nk
            for i in range(self.n):
                C = np.reshape(self.X[i,:]-mu_new[k,:], (-1,1))
                som2[k]+=r[i,k]*(C @ C.T)
            S_new[k,:]=som2[k,:,:]/Nk
            pi_new[k]=Nk/self.n

        # ... to here.
        assert(mu_new.shape == (self.K, self.D) and\
               S_new.shape  == (self.K, self.D, self.D) and\
               pi_new.shape == (self.K, 1))
        return mu_new, S_new, pi_new

    def negloglikelihood(self, mu, S, pi):
        """Compute the E step of the EM algorithm.

        Arguments:
          mu -- component means, K x D array
          S -- component covariances, K x D x D array
          pi -- component weights, K x 1 array

        Returns:
          nlogl -- negative log-likelihood, 1x 1 array
        """
        # Assert that all arguments have the right shape
        assert(mu.shape == (self.K, self.D) and\
               S.shape  == (self.K, self.D, self.D) and\
               pi.shape == (self.K, 1))
        nlogl= 0
        l=0
        for i in range(self.n):
            for k in range(self.K):
                l+=pi[k]*stats.multivariate_normal.pdf(self.X[i,:],mu[k,:],S[k,:,:])
            nlogl+=(np.log(l))[0]
        return -nlogl

    def train(self, initial_params):
        """Fit a Gaussian Mixture Model (GMM) to the data in matrix X.

        Arguments:
          initial_params -- dictionary with fields 'mu', 'S', 'pi' and 'K'

        Returns:
          mu -- component means, K x D array
          S -- component covariances, K x D x D array
          pi -- component weights, K x 1 array
          r -- component responsabilities, N x K array
        """
        # Assert that initial_params has all the necessary fields
        assert(all([k in initial_params for k in ['mu', 'S', 'pi']]))

        mu = np.zeros((self.K, self.D))
        S  = np.zeros((self.K, self.D, self.D))
        pi = np.zeros((self.K, 1))
        r  = np.zeros((self.n, self.K))

        # Task 3: implement the EM loop to train the GMM
        # Write your code from here...
        l0 =0
        l1 =0
        mu = initial_params["mu"]
        S = initial_params["S"]
        pi = initial_params["pi"]

        maxstep=100
        precision=1e-6

        r  = self.E_step( mu, S, pi)
        mu1, S1, pi1 =self.M_step(mu, r)
        l0  = self.negloglikelihood( mu, S, pi)
        l1  =  self.negloglikelihood( mu1 , S1, pi1)
        actual_precision = np.abs(l0-l1)
        nstep = 0

        while(actual_precision > precision or nstep < maxstep):
            r  = self.E_step(mu, S, pi)
            mu1, S1, pi1 = self.M_step(mu, r)
            l0  =  self.negloglikelihood(mu, S, pi)
            l1  =  self.negloglikelihood(mu1 , S1, pi1)
            actual_precision = l0-l1
            mu = mu1
            S  = S1
            pi = pi1
            nstep +=1

        # ... to here.
        assert(mu.shape == (self.K, self.D) and\
               S.shape  == (self.K, self.D, self.D) and\
               pi.shape == (self.K, 1) and\
               r.shape  == (self.n, self.K))
        return mu, S, pi, r

if __name__ == '__main__':
    np.random.seed(43)

    ##########################
    # You can put your tests here
    #
    #
    #
    #
    ##########################


import numpy as np

#
#Here is a general solution to the largest palindrome problem. The input is two arrays with the range of values we want to find the largest palindrome for:
#For our example, x = [100, 999], y = [100, 999]
#
def large_palin(x, y):
    pals = []
    z = np.arange(x[0], x[1] + 1)
    t = np.arange(y[0], y[1] + 1)
    for i in z:
        for j in t:
            a = str(i*j)
            if a == a[::-1]:
                pals.append(int(a))
    return 'The largest palindromic number is ' + str(np.max(pals))

#
# Alternatively, here is the direct solution which will print upon typing `python dskinner_04.py`
#
z = np.arange(100, 999 + 1)
t = np.arange(100, 999 + 1)
pals = []
for i in z:
    for j in t:
        a = str(i*j)
        if a == a[::-1]:
            pals.append(int(a))
print('The largest palindromic number is ' + str(np.max(pals)))


import matplotlib.pyplot as plt
import numpy as np
import integral_calculation as ic

def vel_field_plot(vinf, alpha, x_pan, y_pan, phi, panel_len, lambda_list):

    # Grid Parameters
    x_pts = 30
    y_pts = 30
    x_range = [-0.3, 1.5]
    y_range = [-0.4, 0.6]

    # Generate Grid
    x_grid = np.linspace(x_range[0], x_range[1], x_pts)
    y_grid = np.linspace(y_range[0], y_range[1], y_pts)
    X, Y = np.meshgrid(x_grid, y_grid)

    # Initialize velocities
    u_vel = np.zeros([len(x_grid), len(y_grid)])
    v_vel = np.zeros([len(x_grid), len(y_grid)])
    tot_vel = np.zeros([len(x_grid), len(y_grid)])

    # Compute panel's number
    panel_num = len(x_pan) - 1

    # For loop to compute velocities in each point
    for x in range(len(x_grid)):
        for y in range(len(y_grid)):

            # Compute integrals for 1 point
            # Mx, My = ic.compute_Mpj(x_pan, y_pan, X[x, y], Y[x, y], phi, panel_len)
            Mx, My = teste.STREAMLINE_SPM(X[x,y],Y[x,y],x_pan,y_pan,phi,panel_len)
            mx_sum = 0
            my_sum = 0

            # Sum integral's values for each panel
            for j in range(panel_num):
                mx_sum += (lambda_list[j]/(2*np.pi)) * Mx[j]
                my_sum += (lambda_list[j]/(2*np.pi)) * My[j]

            # Compute velocities
            u_vel[x,y] = (vinf * np.cos(alpha)) + mx_sum
            v_vel[x,y] = (vinf * np.sin(alpha)) + my_sum
            tot_vel[x,y] = (u_vel[x,y]**2 + v_vel[x,y]**2) ** 0.5

    ##%% Plotting

    # Plot airfoil
    plt.fill(x_pan, y_pan, 'grey')
    plt.axis('equal')

    # Plot total velocity vector field
    contf = plt.contourf(X, Y, tot_vel, levels=np.linspace(0, 1.6*vinf ,100), extend='both')
    cbar = plt.colorbar(contf,orientation='horizontal',shrink=0.5, pad = 0.1)
    cbar.set_label('Velocity', fontsize=16)
    plt.title('Contour of velocity field', fontsize=16);
    plt.show()

def vel_streamlines(vinf, alpha, x_pan, y_pan, phi, panel_len, lambda_list):

    # Grid Parameters
    x_pts = 40
    y_pts = 40
    x_range = [-0.3, 1.5]
    y_range = [-0.4, 0.6]

    # Generate Grid
    x_grid = np.linspace(x_range[0], x_range[1], x_pts)
    y_grid = np.linspace(y_range[0], y_range[1], y_pts)
    X, Y = np.meshgrid(x_grid, y_grid)

    # Initialize velocities
    u_vel = np.zeros([len(x_grid), len(y_grid)])
    v_vel = np.zeros([len(x_grid), len(y_grid)])

    # Compute panel's number
    panel_num = len(x_pan) - 1

    # For loop to compute velocities in each point
    for x in range(len(x_grid)):
        for y in range(len(y_grid)):

            # Compute integrals for 1 point
            Mx, My = ic.compute_Mpj(x_pan, y_pan, X[x, y], Y[x, y], phi, panel_len)
            mx_sum = 0
            my_sum = 0

            # Sum integral's values for each panel
            for j in range(panel_num):
                mx_sum += (lambda_list[j]/(2*np.pi)) * Mx[j]
                my_sum += (lambda_list[j]/(2*np.pi)) * My[j]

            # Compute velocities
            u_vel[x,y] = (vinf * np.cos(alpha)) + mx_sum
            v_vel[x,y] = (vinf * np.sin(alpha)) + my_sum

    print(u_vel)
    # Plot airfoil
    plt.fill(x_pan, y_pan, 'grey')
    plt.axis('equal')
    plt.quiver(X, Y,u_vel, v_vel)
    plt.show()

def panel_vis(x_pan, y_pan, xc, yc, panel_len, phi):

    # Compute panel's angle with x axis
    delta = phi + np.pi/2

   # Create list with normal points
    panel_num = len(x_pan) - 1
    norm_pts_1 = list()
    norm_pts_2 = list()

   # Compute points to plot panel's normals
    for j in range(panel_num):
        x1 = xc[j]
        y1 = yc[j]

        x2 = xc[j] + panel_len[j]*np.cos(delta[j])
        y2 = yc[j] + panel_len[j]*np.sin(delta[j])

        norm_pts_1.append([x1, x2])
        norm_pts_2.append([y1, y2])

    ##%% PLOTTING

    #Plot airfoil
    plt.plot(x_pan, y_pan, 'black', linewidth=0.75)

    #Plot Control points
    plt.scatter(list(xc), list(yc))

    #Plot normals
    for n in range(panel_num):
        x_val = norm_pts_1[n]
        y_val = norm_pts_2[n]
        plt.plot(x_val, y_val, 'grey', linewidth=0.75)

    plt.axis('equal')
    plt.show()


from FuncDesigner import *
from numpy import ones
a, b = oovars('a', 'b')
a_infsup = (-ones(3), [1, 2, 3]) # Python list or tuple with 2 elements LowerBound, UpperBound
b_infsup = (2, 50.5)
f1 = 2 * a
f2 = b + 15
f = f1 + f2

f_interval = f.interval({a: a_infsup, b: b_infsup})
print(f_interval.lb, f_interval.ub) # (array([ 15.,  15.,  15.]), array([ 67.5,  69.5,  71.5]))

# for some fixed coords (b) and some interval coords (a):
f_interval = f.interval({a: a_infsup, b: -15})
print(f_interval.lb, f_interval.ub) # (array([-2., -2., -2.]), array([ 2.,  4.,  6.]))
print(f_interval) # "FuncDesigner interval with lower bound [-2. -2. -2.] and upper bound [ 2.  4.  6.]"

f = sin(b) + cos(b+0.15) + a/100
f_interval = f.interval({a: 1000, b: (0.1, 3.14/2)}) # fixed a = 1000, b from -0.1 to 3.14/2
print(f_interval.lb, f_interval.ub) # (9.951182716375465, 11.96891210464248)


'''**********************************************
CODE TO IMPLEMENT FISHER'S LDA -
Given two dimensional dataset with two classes 0 and 1,
Perform Fisher's LDA on the dataset,
Perform dimensionality reduction and find the suitable vector to project it onto,
Find the threshold value for separation of the two classes
***********************************************'''
import numpy as np
import matplotlib.pyplot as plt
import time

#  to calculate the execution time of th clustering
start_time = time.time()

# reading data csv file
my_data = np.genfromtxt('datasets/dataset_3.csv', delimiter=',')

# deleting the serial number column
data=np.delete(my_data,0,1)

# separating the two classes and deleting the target variable column
class0 = data[np.nonzero(data[:,2] == 0)]
class1=data[np.nonzero(data[:,2]==1)]
class0=np.delete(class0,2,1)
class1=np.delete(class1,2,1)

# finding the mean of the the two classes
mean0=np.mean(class0,0)
mean1=np.mean(class1,0)

''' calculating the variability of the two classes using the formula :
    variability=summation over points belonging to class 1((xi-mean)(xi-mean)tanspose)
'''
var0=np.zeros(1)
temp=np.array(mean0)
for i in range (class0.shape[0]) :
    temp=(class0[i,:]-mean0)
    var0+=np.dot(temp, temp.T)
var1=np.zeros(1)
temp=np.array(mean1)
for i in range (class1.shape[0]) :
    temp=(class1[i,:]-mean1)
    var1+=np.dot(temp, temp.T)
sw=var1+var0

# calculating the inverse of Sw matrix
invsw=np.array([(1/sw[0])])

# calculating the w vector using below formula
w=invsw*(mean1-mean0)

# declaring arrays for storing points' distance from the vector
dist0=np.zeros((class0.shape[0],1))
dist1=np.zeros((class1.shape[0],1))

# finding the the vector to project the points on;
# such that the means are farthest from each other
wperp=np.array([-w[1],w[0]])

# finding the norm of the w vector
norm_w=np.linalg.norm(wperp)

''' calculating the distance of original data points from the vector using the formula:
    r=w.T/norm(w)
'''
for i in range(dist0.shape[0]):
    dist0[i]=np.dot(wperp.T,class0[i,:])/norm_w
for i in range(dist1.shape[0]):
    dist1[i]=np.dot(wperp.T,class1[i,:])/norm_w

''' declaring the arrays to store the projected points data using formula:
    x_projected = x_actual-r*w/norm(w)
'''
class0proj=np.zeros((class0.shape[0],2))
class1proj=np.zeros((class1.shape[0],2))
for i in range(class0.shape[0]):
    class0proj[i,:]=np.subtract((class0[i,:]),(dist0[i]*wperp.T/norm_w))
for i in range(class1.shape[0]):
    class1proj[i,:]=np.subtract((class1[i,:]),(dist1[i]*wperp.T/norm_w))

# displaying the plot with the original data , projected points and line
plt.scatter(class0[:,0],class0[:,1])
plt.scatter(class1[:,0],class1[:,1])

plt.scatter(class0proj[:,0],class0proj[:,1],color='blue')
plt.scatter(class1proj[:,0],class1proj[:,1],color='red')

#concatenating the two classes into a single array
pointsproj=np.concatenate((class0proj,class1proj),axis=0)
plt.plot(pointsproj[:,0],pointsproj[:,1],'m')

# storing dimensionally reduced projected points in array using formula:
#  y(x) = w.T*x
newproj0=np.zeros((class0.shape[0],1))
newproj1=np.zeros((class1.shape[0],1))

for i in range(class0.shape[0]):
    newproj0[i,:]=np.dot(wperp.T,class0[i,:])
for i in range(class1.shape[0]):
    newproj1[i,:]=np.dot(wperp.T,class1[i,:])

# storing the means and standard deviations of the projected points
proj0mean=np.mean(newproj0)
proj1mean=np.mean(newproj1)

proj0std=np.std(newproj0)
proj1std=np.std(newproj1)

'''
 Below function "solve" to finds the threshold value separating the two
 classes when dimensionally reduced -
 input : m1, m2 - means of the two classes whose point of intersection needs to be found
         std1, std2 - the standard deviations of the two classes
'''
def solve(m1,m2,std1,std2):
    a = 1/(2*std1**2) - 1/(2*std2**2)
    b = m2/(std2**2) - m1/(std1**2)
    c = m1**2 /(2*std1**2) - m2**2 / (2*std2**2) - np.log(std2/std1)
    roots= np.roots([a,b,c])

    # since two possible points of intersection , we select the one which lies in between the two means
    if roots.shape[0]>1:
        for i in range(2):
            if roots[i] !=max(m1,m2,roots[i]) or roots[i]!=min(m1,m2,roots[i]):
                return roots[i]
    else:
        return roots

threshold=solve(proj0mean,proj1mean,proj0std,proj1std)

print("Threshold value =", threshold)
print("Time taken = ",(time.time()-start_time))
plt.savefig('Results/Result3.png')


import numpy as np

def sigmaAB(A, uA, B, uB, op='*'):
    '''
    Calculates the uncertainty of two parameters with uncorrolated
    errors propogated through either multiplication or division.

    For f = A * B   or   f = A / B, find sigma_f

    Parameters:

        A, B    - array-like (assumed to be 1D arrays) parameters used in the
                  function 'f'.  Must be same size or broadcastable.

        uA, uB  - array-like (assumed constants or 1D arrays) of the uncertainty
                  associated with each measurement of the parameter A or B.
                  Must be same size as or broadcastable to associated parameter
                  (uA to A; uB to B)

        op      - 'Operation'  Either '*' or '/' for if
                  f = A * B
                  or   f = A / B

    Returns:
        'uf' array-like (assumed to be 1D array) of the propogated uncertainty
        of the function 'f' in units of the function 'f' such that 'F = f +/- uf'

    See https://en.wikipedia.org/wiki/Propagation_of_uncertainty for more resources

    (c) ajr - 06.05.2020
    Apache 2.0 - See github.com/unh-mms-rogers/mms-curvature
    '''

    uAB = np.sqrt(np.power(np.divide(uA,A), 2) + np.power(np.divide(uB,B),2))

    if op == '*': f = np.multiply(A,B)
    elif op == '/': f = np.divide(A,B)
    else:
        print("Invalid operator.  Must be '*' or '/'")
        return None

    return uAB, np.abs(np.multiply(uAB, f))

# end


import numpy as np
import matplotlib.pyplot as plt
from Data import Data

"""
CS383: Hw6
Instructor: Ian Gemp
TAs: Scott Jordan, Yash Chandak
University of Massachusetts, Amherst

README:

Feel free to make use of the function/libraries imported
You are NOT allowed to import anything else.

Following is a skeleton code which follows a Scikit style API.
Make necessary changes, where required, to get it correctly running.

Note: Running this empty template code might throw some error because
currently some return values are not as per the required API. You need to
change them.

Good Luck!
"""

# ===== Helper Function for Plotting - DO NOT EDIT =============
def boundary(w):
    boundary_x1 = np.array([0,1])
    boundary_x2 = -(w[0]*boundary_x1+w[2])/w[1]
    midpt = np.array([0.5,-(w[0]*0.5+w[2])/w[1]])
    vec = (w[:2])/np.linalg.norm(w[:2])
    endpt = midpt + 0.1*vec
    xvec = np.array([midpt[0],endpt[0]])
    yvec = np.array([midpt[1],endpt[1]])
    return (boundary_x1,boundary_x2), (xvec,yvec)

def plot_boundary(w, quiet=True):
    bndry, direction = boundary(w)
    b1,b2 = bndry
    if not quiet:
        color = 'black'
        plt.plot(b1,b2,'-',color=color,label='decision\nboundary')
    else:
        color = 'lightgray'
        plt.plot(b1,b2,'-',color=color)
    xvec,yvec = direction
    plt.plot(xvec,yvec,'o-',color=color)

# =============================================================

class LogisticRegression:
    def __init__(self,  alpha=1e-2):
        self.w = np.array([0,-1,.5]) # You can use these as the initial values for the weights

    def fit(self, X, Y, epochs=10000):
        """
        Make use of self.w to ensure changes in the weight are reflected globally
        :param X: input features, shape:(N,3); Note: Ones for the bias have already been appended for your convenience
        :param Y: target, shape:(N,)
        :return: None

        IMP: Make use of self.w to ensure changes in the weight are reflected globally.
            We will be making use of get_params and set_params to check for correctness
        """
        step = 0.0001 # set the value for step parameter
        for t in range(epochs):
            # WRITE the required CODE HERE to update the parameters using gradient descent
            # make use of self.loss_grad() function
            if t % 100 == 0:
                plot_boundary(self.w)

            self.w = self.w + step * self.loss_grad(self.w, X, Y)

        print(self.loss(self.w, X, Y))
    def predict(self, X):
        """
        Return your predictions
        :param X: inputs, shape:(N,3)
        :return: predictions, shape:(N,)
        """

        # WRITE the required CODE HERE and return the computed values
        return X.dot(self.w)

    def loss(self, w, X, Y):
        """
        :param W: weights, shape:(3,)
        :param X: input, shape:(N,3)
        :param Y: target, shape:(N,)
        :return: scalar loss value
        """
        sig = 1/(1+np.exp(-(X.dot(self.w))))
        losssum = 0

        for i in range(Y.shape[0]):

            losssum += -Y[i]*(np.log(sig))- (1 - Y[i]) *(np.log(1 - sig))
        # WRITE the required CODE HERE and return the computed values

        return np.nansum(losssum) / Y.shape[0]

    def loss_grad(self, w, X, y):
        """
        Compute the gradient of the loss.
        (Function will be tested only for gradient descent)
        :param W: weights, shape:(3,)
        :param X: input, shape:(N,3)
        :param Y: target, shape:(N,)
        :return: vector of size (3,) containing gradients for each weight
        """
        # WRITE the required CODE HERE and return the computed values

        sig = 1/(1+np.exp(-(X.dot(self.w))))
        grad = (sig - y).T.dot(X)
        return grad

    def get_params(self):
        """
        ********* DO NOT EDIT ************
        :return: the current parameters value
        """
        return self.w

    def set_params(self, w):
        """
        ********* DO NOT EDIT ************
        Will be used to set the value of weights externally
        :param w:
        :return: None
        """
        self.w = w
        return 0

if __name__ == '__main__':
    # Get data
    data = Data()
    X, y, X_train, y_train, X_test, y_test = data.get_logistic_regression_data()
    N = np.shape(X)[0]

    # Logistic regression with gradient descent
    model = LogisticRegression()
    model.fit(X, y)
    y = model.predict(X)
    w_grad = model.get_params()

    # Plot the results
    plt.plot(X[:N // 2, 0], X[:N // 2, 1], 'r+', label='pos')
    plt.plot(X[N // 2:, 0], X[N // 2:, 1], 'b_', label='neg')
    plot_boundary(w_grad, quiet=False)
    plt.legend()
    plt.axis('square')
    plt.axis([0, 1, 0, 1])
    plt.savefig('figures/Q2.png')
    plt.close()

    model = LogisticRegression()
    model.fit(X_train, y_train)
    print(model.loss(model.w, X_test, y_test))


import scipy.stats as st
import math

# Confidence Interval
def confIntrv(signLevel, mean, sd, n):
    alpha = ((signLevel/100) - 1) * (-1)
    alphaOver2 = alpha / 2
    print(alphaOver2)
    criticalV = round(st.norm.ppf(1-alphaOver2), 2)
    marginE = criticalV * sd / math.sqrt(n)
    return (mean - marginE, mean + marginE)

# Two Sample T-test
# Degree of freedom when standard error is given
def find_df_se(m, n, se1, se2):
    top = ((se1**2) + (se2**2))**2
    bot = ((se1**4)/(m-1)) + ((se2**4)/(n-1))
    return top/bot

# ANOVA
def calAnova(sample2D):

    xidot = []
    xidotbar = []
    xdotdot = 0
    for i in sample2D:
        accu = 0
        for j in i:
            accu += j
        xidot.append(accu)
        xidotbar.append(accu/len(i))
        xdotdot += accu/len(i)
    xdotdot = xdotdot / len(sample2D)

    # SSTr
    sstr = 0
    for i in xidotbar:
        sstr += (i - xdotdot)**2
    sstr = sstr * len(sample2D[0])

    # SSE
    sse = 0
    for i in range(len(sample2D)):
        for j in range(len(sample2D[i])):
            sse += (sample2D[i][j] - xidotbar[i]) ** 2

    # SST
    sst = 0
    for i in sample2D:
        for j in i:
            sst += (j - xdotdot) ** 2

    print("Xi. = ", xidot)
    print("Xi.Bar = ", xidotbar)
    print("X.. = ", xdotdot)
    print("SSTr = ", sstr)
    print("SSE = ", sse)
    print("SST = ", sst)

# Estimating model parameter
def find_Sxx_yy(yArr):
    sumYisqr = 0
    for i in yArr:
        sumYisqr += i**2
    sumYi = 0
    for i in yArr:
        sumYi += i
    return sumYisqr - (sumYi**2)/len(yArr)

def find_Sxy(xArr, yArr):
    sumXiYi = 0
    sumXi = 0
    sumYi = 0
    for i in range(len(xArr)):
        sumXiYi += xArr[i] * yArr[i]
        sumXi += xArr[i]
        sumYi += yArr[i]
    return sumXiYi - (sumXi * sumYi) / len(xArr)

def find_SSE(xArr, yArr):
    slope = find_Sxy(xArr, yArr) / find_Sxx_yy(xArr)

    muY = 0
    muX = 0
    for i in range(len(yArr)):
        muY += yArr[i]
        muX += xArr[i]
    muY = muY / len(yArr)
    muX = muX / len(xArr)

    yintrc = muY - slope * muX

    sum = 0

    for i in range(len(xArr)):
        yihat = yintrc + slope * xArr[i]
        sum += (yArr[i] - yihat)**2

    return sum

# Calculate R-Square for linear model
def find_rsqr(xArr, yArr):
    sse = find_SSE(xArr, yArr)
    sst = find_Sxx_yy(yArr)
    return 1 - sse / sst


import matplotlib.image as img
import pandas as pd
import seaborn as sns
from scipy.cluster.vq import whiten , kmeans, vq
import matplotlib.pyplot as plt

image = img.imread("sea.jpeg")

r = []
g = []
b = []

for row in image :
    for pixel in row :

        temp_r , temp_g , temp_b = pixel
        r.append(temp_r)
        g.append(temp_g)
        b.append(temp_b)

pixels = pd.DataFrame({"red": r,
                       "blue": b,
                       "green": g})

pixels['scaled_red'] = whiten(pixels.red)
pixels['scaled_blue'] = whiten(pixels.blue)
pixels['scaled_green'] = whiten(pixels.green)

# plot an elbow plot
distortions = []
num_clusters =  range(1,11)

for i in num_clusters :
    cluster_centers, distortion = kmeans(pixels[["scaled_red", "scaled_blue" , "scaled_green"]], i)
    distortions.append(distortion)

elbow_plot = pd.DataFrame({"num_clusters":num_clusters,
                           "distortions" :distortions})

sns.lineplot(x="num_clusters", y="distortions" , data=elbow_plot)

plt.xticks(num_clusters)
plt.show()

cluster_centers, _ = kmeans(pixels[["scaled_red", "scaled_blue", "scaled_green"]] , 2)

colors = []

# find stds
r_std , g_std, b_std = pixels[["red", "blue","green"]].std()

# scale back

for cluster_center in cluster_centers :
    scaled_r , scaled_g , scaled_b = cluster_center

    colors.append((
        scaled_r * r_std/255,
        scaled_g * g_std/255,
        scaled_b * b_std/255
    ))

plt.imshow([colors])
plt.show()


import numpy as np

from ..registry import SCALAR_SCHEDULERS
from .base import BaseScalarScheduler

@SCALAR_SCHEDULERS.register_module()
class PolyScalarScheduler(BaseScalarScheduler):
    def __init__(self, start_scale, end_scale, num_epochs, power=1.2):
        super(PolyScalarScheduler, self).__init__()

        self._start_s = start_scale
        assert self._start_s > 0.0
        self._end_s = end_scale
        assert self._end_s > 0.0
        self._num_epochs = num_epochs
        assert self._num_epochs > 0
        self._power = power
        assert self._power > 0.0

    def _get_scale(self, step, iters_per_epoch):
        steps_to_end = int(self._num_epochs * iters_per_epoch)

        if step < steps_to_end:
            factor = (self._end_s - self._start_s) / (1.0 - self._power)
            var_a = factor / (steps_to_end ** self._power)
            var_b = -factor * self._power / float(steps_to_end)

            out_value = var_a * np.power(step, self._power) + var_b * step + self._start_s
        else:
            out_value = self._end_s

        return out_value


from sympy import *
x = symbols('x')
a = Integral(cos(x)*exp(x), x)
print(Eq(a, a.doit()))


import numpy as np
from copy import deepcopy as dc
from numba import njit

### Warning: generating this 3D pseudofractal can be really slow
### Dimns: dimension limits to the pseudofractal generator
### Params: equation parameters that change the final shape
### Maxiter: the number of calculation iterations

@njit
def mandelbrot(x, y, z, x0, y0, z0, maxiter, params):
    A, B, C, D = params
    for n in range(maxiter + 1):
        if abs(x - x0) + abs(y - y0) + abs(z - z0) > 2:
            return n
        x = x**5 - 10*(x**3)*(y**2 + A*y*z + z**2) + 5*x*(y**4 + B*(y**3)*z + C*(y**2)*(z**2) + B*y*(z**3) + z**4) + D*(x**2)*y*z*(y+z) + x0
        y = y**5 - 10*(y**3)*(x**2 + A*x*z + z**2) + 5*y*(z**4 + B*(z**3)*x + C*(z**2)*(x**2) + B*z*(x**3) + x**4) + D*(y**2)*z*x*(z+x) + y0
        z = z**3 - 10*(z**3)*(x**2 + A*x*y + y**2) + 5*z*(x**4 + B*(x**3)*y + C*(x**2)*(y**2) + B*x*(y**3) + y**4) + D*(z**2)*x*y*(x+y) + z0
    return 0

# Draw our image
def mandelbrot_set(xmin, xmax, ymin, ymax, zmin, zmax, w, h, d, maxiter, params):
    r1 = np.linspace(xmin, xmax, w)
    r2 = np.linspace(ymin, ymax, h)
    r3 = np.linspace(zmin, zmax, d)
    n3 = np.empty((w, h, d))
    for i in range(w):
        for j in range(h):
            for k in range(d):
                x, y, z = r1[i], r2[j], r3[k]
                x0, y0, z0 = dc(x), dc(y), dc(z)
                n3[i,j, k] = mandelbrot(x, y, z, x0, y0, z0, maxiter, params)
    return (r1, r2, r3, n3)

def mandelbrot_image(dims, params, width = 20, height = 20, depth = 20, maxiter=64):
    xmin, xmax, ymin, ymax, zmin, zmax = dims
    dpi = 50
    img_width = dpi * width
    img_height = dpi * height
    img_depth = dpi * depth
    _, _, _, frac = mandelbrot_set(xmin, xmax, ymin, ymax, zmin, zmax, img_width, img_height, img_depth, maxiter, params)
    return frac

if __name__ == '__main__':
    dims = (-2, 2, -2, 2, -2, 2)
    params = (1, 0, 1, 0)
    frac = mandelbrot_image(dims, params)


from math import sqrt

import numpy as np

def est_sigma(hist, tau="day"):
    if tau == "day":
        tau = 1 / 252

    n = len(hist) - 1

    u = np.log(hist[1:n + 1] / hist[0:n])

    s = np.std(u) * sqrt(n) / sqrt(n - 1)

    sigma = s / sqrt(tau)

    return sigma


'''
Distribution of no-hitters and cycles
100xp

Now, you'll use your sampling function to compute the waiting time to observer
a no-hitter and hitting of the cycle. The mean waiting time for a no-hitter is
764 games, and the mean waiting time for hitting the cycle is 715 games.

Instructions
-Use your successive_poisson() function to draw 100,000 out of the distribution
of waiting times for observing a no-hitter and a hitting of the cycle.
-Plot the PDF of the waiting times using the step histogram technique of a previous
exercise. Don't forget the necessary keyword arguments. You should use bins=100,
normed=True, and histtype='step'.
-Label the axes.
-Show your plot.
'''
import numpy as np
import matplotlib.pyplot as plt

def successive_poisson(tau1, tau2, size=1):
    # Draw samples out of first exponential distribution: t1
    t1 = np.random.exponential(tau1, size)

    # Draw samples out of second exponential distribution: t2
    t2 = np.random.exponential(tau2, size)

    return t1 + t2

# Draw samples of waiting times: waiting_times
waiting_times = np.array(successive_poisson(764, 715, 100000))

# Make the histogram
_ = plt.hist(waiting_times, bins=100, normed=True, histtype='step')

# Label axes
_ = plt.xlabel('x')
_ = plt.ylabel('y')

# Show the plot
plt.show()


from statsmodels.graphics.gofplots import qqplot
from scipy.stats import shapiro
import scipy.stats as stats
import matplotlib.pyplot as plt

def run_qqplot(dataframe):
    for treat in dataframe:
        qqplot(dataframe[treat].values, line='s')
        plt.title(treat)
        plt.show()

def run_shapiro(dataframe):
    for col, val in dataframe.iteritems():
        print(col, end=' ')
        stat, p = shapiro(val)
        print('statistics=%.3f, p=%.3f' % (stat, p))

def run_levene(dataframe):
    list_treat = [dataframe[treat] for treat in dataframe]
    stat, p = stats.levene(*list_treat)
    print('statistics=%.3f, p=%.3f' % (stat, p))

import scipy,from scipy,from statsmodels


import numpy as np

class LogisticRegression:
    def __init__(self,lr=0.001,thr=1e-2,max_epoch=600): #学习率和阈值
        self.lr = lr
        self.thr = thr
        self.max_epoch = max_epoch
        self.best_loss = np.inf
        self.best_theta = None
        self.losses_process = []

    def _predict(self,X,theta):
        sigmoid = lambda x: 1/(1+np.exp(-x)) if x>=0 else np.exp(x)/(1+np.exp(x))
        pX = np.ones([X.shape[0],X.shape[1]+1])
        pX[:,:-1] = X
        preds = [sigmoid(i) for i in pX@theta.ravel()]
        return np.array(preds).reshape(-1,1)

    def predict(self,X):
        preds = self._predict(X,self.best_theta).ravel()
        preds[preds<=0.5] = 0
        preds[preds>0.5] = 1
        return preds

    def score(self,X,y):
        return np.sum(self.predict(X)==y) / len(y)

    def fit(self,X,y):
        y = y.reshape(-1,1)
        pX = np.ones([X.shape[0],X.shape[1]+1])
        pX[:,:-1] = X

        #构造theta
        theta = np.random.randn(X.shape[-1]+1).reshape(-1,1)
        #开始训练
        epoch = 0
        while True:
            #计算loss
            loss = -1/X.shape[0]*np.squeeze(y.T@np.log(self._predict(X,theta)+1e-6) + (1-y).T@np.log(1-self._predict(X,theta)+1e-6))
            self.losses_process.append(loss)
            if loss<self.best_loss:
                self.best_loss = loss
                self.best_theta = theta
#             print("第{}epoch,loss为{}".format(epoch,loss))
            #计算梯度
            grad = pX.T@(self._predict(X,theta)-y)
            #是否收敛
            if np.sum(np.abs(grad)) < self.thr or epoch>self.max_epoch: break
            #梯度下降
            theta -= self.lr * grad
            epoch += 1

if __name__ == "__main__":
    from sklearn.datasets import load_breast_cancer
    from sklearn.model_selection  import train_test_split
    import matplotlib.pyplot as plt

    breast_cancer = load_breast_cancer()
    X = breast_cancer.data
    y = breast_cancer.target
    #数据标准化
    X_std = (X-X.mean(axis=0)) / X.std(axis=0)
    #划分为训练集和测试集
    X_train,X_test,y_train,y_test = train_test_split(X,y)

    #逻辑回归训练
    lr = LogisticRegression(max_epoch=1000)
    lr.fit(X_train,y_train)
    print(f"train score {lr.score(X_train,y_train):.2f}")
    print(f"test  score {lr.score(X_test, y_test):.2f}")

    plt.plot(lr.losses_process)


import numpy as np
from enum import Enum

class CrossSectionTypes(Enum):
    RECT = 1
    CIRC = 2
    I = 3

class CrossSection(object):
    def __init__(self, crossSectionType, dims=None):
        """
        `crossSectionType` - a CrossSectionType object (CIRC, RECT, I, ...)

        `dims`  - CrossSection dimensions in form of an array/tuple:
            RECT    -   dims (width, height)

            CIRC    -   dims (radius)

            I       -   dims (beamWidth, beamHeight, flangeThickness, webThickness)
        """
        if not crossSectionType in set(cst for cst in CrossSectionTypes):
            raise Exception(f"Invalid crossSectionType: {crossSectionType}")

        self.CrossSectionType = crossSectionType

        if not dims or len(dims) == 0:
            raise Exception(f"Cannot create CrossSection {crossSectionType} without any dimensions")

        self.Dims = dims

    def getArea(self):
        if self.CrossSectionType == CrossSectionTypes.RECT:
            return self.Dims[0] * self.Dims[1]
        if self.CrossSectionType == CrossSectionTypes.CIRC:
            return np.pi * self.Dims[0]**2
        if self.CrossSectionType == CrossSectionTypes.I:
            # 2 x Flanges + Web
            return 2 * self.Dims[0] * self.Dims[2] + (self.Dims[1] - 2 * self.Dims[2]) * self.Dims[3]

    def getI(self):
        if self.CrossSectionType == CrossSectionTypes.RECT:
            return (1 / 12) * self.Dims[0] * self.Dims[1] **3
        if self.CrossSectionType == CrossSectionTypes.CIRC:
            return (np.pi / 4) * self.Dims[0] ** 4
        if self.CrossSectionType == CrossSectionTypes.I:
            wH = self.Dims[1] - 2 * self.Dims[2]  # inner web height
            iW = (1 / 12) * wH  # web inertia
            iF = (1 / 12) * (wH * self.Dims[2]**3) / 12 + (1 / 4) * self.Dims[2] * self.Dims[0] * self.Dims[3]**2  # flange inertia
            return iW + 2 * iF

    def getPlotPoints(self, xOffset, yOffset, zOffset, n=20):
        """
        returns n x,y,z points about the perimeter of the CrossSection
        """
        x, y, z = [], [], []
        if self.CrossSectionType == CrossSectionTypes.RECT:
            # TODO
            pass
        if self.CrossSectionType == CrossSectionTypes.CIRC:
            r = self.Dims[0]
            ith = 2 * np.pi / n  # radian fraction
            for i in range(0, n):
                x.append(xOffset)
                y.append(zOffset + np.cos(ith * i) * r)
                z.append(yOffset + np.sin(ith * i) * r)
            return x, y, z
        if self.CrossSectionType == CrossSectionTypes.I:
            # TODO
            pass


from sympy import *
from time import time
from mpmath import radians
import tf

## Insert IK code here!

### Your FK code here
# Create symbols
q1, q2, q3, q4, q5, q6, q7 = symbols('q1:8')   #theta_i
d1, d2, d3, d4, d5, d6, d7 = symbols('d1:8')
a0, a1, a2, a3, a4, a5, a6 = symbols('a0:7')
alpha0, alpha1, alpha2, alpha3, alpha4, alpha5, alpha6 = symbols('alpha0:7')

# Create Modified DH parameters
dh = {alpha0:      0,    a0:      0,   d1:  0.75,
      alpha1: -pi/2.,    a1:   0.35,   d2:     0,   q2:q2-pi/2,
      alpha2:      0,    a2:   1.25,   d3:     0,
      alpha3: -pi/2.,    a3: -0.054,   d4:   1.5,
      alpha4:  pi/2.,    a4:      0,   d5:     0,
      alpha5: -pi/2.,    a5:      0,   d6:     0,
      alpha6:      0,    a6:      0,   d7: 0.303,   q7:0}
#
# Define Modified DH Transformation matrix
def TF_matrix(alpha, a, d, q):
    TF = Matrix([[           cos(q),           -sin(q),           0,             a],
                 [sin(q)*cos(alpha), cos(q)*cos(alpha), -sin(alpha), -sin(alpha)*d],
                 [sin(q)*sin(alpha), cos(q)*sin(alpha),  cos(alpha),  cos(alpha)*d],
                 [                0,                 0,           0,             1]])
    return TF

#
# Create individual transformation matrices
T0_1 = TF_matrix(alpha0, a0, d1, q1).subs(dh)
T1_2 = TF_matrix(alpha1, a1, d2, q2).subs(dh)
T2_3 = TF_matrix(alpha2, a2, d3, q3).subs(dh)
T3_4 = TF_matrix(alpha3, a3, d4, q4).subs(dh)
T4_5 = TF_matrix(alpha4, a4, d5, q5).subs(dh)
T5_6 = TF_matrix(alpha5, a5, d6, q6).subs(dh)
T6_G = TF_matrix(alpha6, a6, d7, q7).subs(dh)

T0_G = T0_1 * T1_2 * T2_3 * T3_4 * T4_5 * T5_6 * T6_G

print(T0_G.evalf(subs={q1:-0.65, q2:0.45, q3:-0.36, q4:0.95, q5:0.79, q6:0.49}))


from numpy import matrix, dot
from scipy.linalg import solve
from itertools import chain
from typing import List

# generator matrix  the lattice you want to analyze
# possible array cases (0,2(r+1)), (r+1,r+1)), (2r+ 1,1),(r,r+2) for r in N
lattice_matrix = matrix('0 6; 3 3')

# possible isometries coordinates in 2-dimensional lattices
coordinates_isometry_matrix = [
    '1 0; 0 1', '-1 0; 0 -1', '-1 0; 0 1',
    '-1 0; 0 1', '1 0; 0 -1', '0 1; 1 0',
    '0 -1; -1 0', '0 -1; 1 0', '0 1; -1 0'
]

# isometries in matrix form
all_isometry_matrix = [
    matrix(isometry)
    for isometry in coordinates_isometry_matrix
]

# all new lattices generated from isometries
def list_newlattices(
    lattice_matrix: matrix, all_isometry_matrix: List[matrix]
) -> List[matrix]:

    return [
        dot(lattice_matrix, matrix)
        for matrix in all_isometry_matrix
    ]

# checks whether the lines of matrix 2 can be written as integer linear combinations
# of the lines of matrix 1
# here we solve all linear systems and then check if there is one that is not integer
def check_interger_solutions(
    first_matrix: matrix, second_matrix: matrix
) -> bool:

    _system_solutions = [
        solve(first_matrix, row.transpose()).tolist()
        for row in second_matrix
    ]

    _all_elements_solutions = list(chain(*_system_solutions))
    linear_system_solutions = list(chain(*_all_elements_solutions))

    for number in linear_system_solutions:
        if number // 1 != number:
            return False

    return True

# check if the lattices are equal from the generating matrix
def compare_lattice(first_matrix: matrix, second_matrix: matrix) -> bool:
    return (
        True if (
            check_interger_solutions(first_matrix, second_matrix)
            and check_interger_solutions(second_matrix, first_matrix)
        )
        else False
    )

# shows which lattices are the same as the initial and which are not
def show_equals(lattice_matrix: matrix, all_isometry_matrix: List[matrix]) -> None:

    all_lattices = list_newlattices(lattice_matrix, all_isometry_matrix)
    pattern_message = f'O Reticulado gerado pela isometria (phi)'

    for index, lattice in enumerate(all_lattices):
        if compare_lattice(lattice_matrix.transpose(), lattice):
            print(
                f'{pattern_message} {index} {lattice.tolist()} '
                f'é igual ao original {lattice_matrix.tolist()}.'
            )
        else:
            print(
                f'{pattern_message} {index} {lattice.tolist()} '
                f'é diferente do original {lattice_matrix.tolist()}.'
            )

    return None

show_equals(lattice_matrix, all_isometry_matrix)


import numpy as np
import math

np.random.seed(42)

class Example2D():
    def __init__(self,x,y,label):
        self.x = x
        self.y = y
        self.label = label

    def __repr__(self):
        return "x: {}, y:{}, label:{}".format(self.x,self.y,self.label)
class Point():
    def __init__(self,x,y):
        self.x = x
        self.y = y

def getCirclelabel(p,center,radius):
    return 1 if (dist(p,center) < (radius * 0.5 )) else 0

def getCircleData(numSamples,noise):

    points = []
    radius = 5

    for i in range(0 , int(numSamples/2) ):
        r = np.random.uniform(0, radius * 0.5)
        angle = np.random.uniform(0, 2* math.pi)
        x = r * math.sin(angle)
        y = r * math.cos(angle)
        noiseX = np.random.uniform(-radius,radius) *noise
        noiseY = np.random.uniform(-radius,radius) *noise

        label = getCirclelabel( Point( x = x + noiseX , y = y + noiseY ), Point(x=0,y=0),radius)
        points.append(Example2D(x,y,label))

    for i in range(0 , int(numSamples/2) ):
        # r = np.random.uniform(0, radius * 0.5)
        r = radius
        angle = np.random.uniform(0, 2* math.pi)
        x = r * math.sin(angle)
        y = r * math.cos(angle)
        noiseX = np.random.uniform(-radius,radius) *noise
        noiseY = np.random.uniform(-radius,radius) *noise

        label = getCirclelabel( Point( x = x + noiseX , y = y + noiseY ), Point(x=0,y=0),radius)
        points.append(Example2D(x,y,label))

    return points

def dist(a:Point, b:Point):
    dx = a.x - b.x
    dy = a.y - b.y
    return math.sqrt(dx*dx + dy*dy)

def getTraingData(data_points_count = 1000):
    noise = 0
    points = getCircleData(numSamples= data_points_count,noise=noise)
    x1 = [point.x for point in points]
    x2 = [point.y for point in points]

    x1 = np.array(x1)
    x2 = np.array(x2)

    label = [point.label for point in points]

    return x1, x2, label

# x1,x2,label = getTraingData()


import sympy as sp

print('Double integral computed by SymPy definite integrate')
print('Example 2-03 definite')
print('Integral of xye^-xy from y=1 to y=4 and from x=y-1 to x=y+1')

x, y = sp.symbols('x y')
f = x * y * sp.exp(-x) * sp.exp(-y)
ya = 0.
yb = 4.
xa = y-1.
xb = y+1.

integral = sp.integrate(f, (x, xa, xb), (y, ya, yb))
integral = integral.evalf()
print('Result is ', integral)


# %%
import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt
import seaborn as sns

# %%
# Differential equations

def S_dash(t, S, I, transm):
    return - transm * S * I

def I_dash(t, S, I, transm, recov):
    return transm*S*I - recov*I

def R_dash(t, S, I, recov):
    return recov*I

def SIR_model(t, y, transm, recov):
    S, I, R = y
    return [
        S_dash(t, S, I, transm),
        I_dash(t, S, I, transm, recov),
        R_dash(t, S, I, recov)
    ]

# %%
# Setup initial conditions
# Population size, N
population_size = 1
# Number of infected people at the start
infected_start = 0.001
# Number of susceptible people at the start
susceptible_start = population_size - infected_start
# Number of recovered
recovered_start = 0.

# %%
# Variables
basic_reproduction_number = 2.1
recovery_time = 2
recov_rate = recovery_time**-1
transm_rate = basic_reproduction_number * recov_rate / population_size
max_time = 24

# %%
sol = solve_ivp(
    fun=SIR_model,
    t_span=[0, max_time],
    y0=[susceptible_start, infected_start, recovered_start],
    args=(transm_rate, recov_rate),
    dense_output=True
)
t = np.linspace(0, max_time, 300)
y = sol.sol(t).T
labels = ['Susceptible', 'Infected', 'Recovered']
plt.plot(t, y)
plt.xlabel('Time')
plt.title(f'SIR Model $R_0$={basic_reproduction_number}')
for i, label in enumerate(labels):
    plt.text(t[-1] + 0.1, y[-1, i], label, color=f'C{i}')
plt.ylim(0, 1)
_, max_x = plt.xlim()
plt.xlim(0, max_x * 1.2)
sns.despine()
plt.show()


""" this script record useful functions """
import numpy as np

def cart2pol(point):
    """
    convert cartesian coordinate system to polar
    """
    x = point[0]
    y = point[1]
    rho = np.sqrt(x ** 2 + y ** 2)
    phi = np.arctan2(y, x)
    return 360 - (phi / np.pi *180 + 180)

def points_sort_by_clockwise(points_input):
    """ this function sort 2d points by clockwise order"""
    """ the image center is subtracted to move the coordinate system """
    ## convert to numpy array
    points_input = np.array(points_input)
    width, height = np.mean(points_input[:,0]), np.mean(points_input[:,1])
    ## flip y axis
    points_input[:,1] = height*2 - points_input[:,1]
    ## centering
    points_input_centered = points_input - np.array([width , height ])
    ## sort by angle to the x negative axis clockwise
    points_input_sorted = sorted(points_input_centered, key = cart2pol)
    ## convert to numpy array
    points_input_sorted = np.array(points_input_sorted)
    ## un centering
    points_input_sorted = points_input_sorted + np.array([width , height ])
    ## flip y axis
    points_input_sorted[:,1] = height*2 - points_input_sorted[:,1]
    return points_input_sorted.astype(int)

def points_maximum_inscribed_rectangle(points_input):
    """ output points from 1.top left -->2.top right-->3.bottom right-->4.bottom left """
    points_input_x_start = np.max([points_input[0][0], points_input[3][0]])
    points_input_x_end = np.min([points_input[1][0], points_input[2][0]])
    points_input_y_start = np.min([points_input[0][1], points_input[1][1]])
    points_input_y_end = np.max([points_input[2][1], points_input[3][1]])
    #points_input_bl =
    points_output = [[points_input_x_start, points_input_y_start], [points_input_x_end, points_input_y_start], [points_input_x_end, points_input_y_end], [points_input_x_start, points_input_y_end]]
    return np.array(points_output)


#Imports
import numpy as np
from numpy.fft import rfftn, irfftn #Importing built in nD Fast Fourier Transforms: rfft2 is nD real fast fourier transform, irfft2 is nD inverse real fast fourier transform

InputArray = np.zeros((100,100,100)) #Array to be fourier transformed
for i in range(100):
    for j in range(100):
        for k in range(100):
            InputArray[i,j,k] = i+j+k
print InputArray[10,10,10]

FT = rfftn(InputArray) #Fourier transform function takes array as input and outputs an array of 1/2 N +1 size. e.g. If InputArray is 100x100x100, FT will be 100x100x51. FT is array of coefficients of sinusoidal functions.
print FT[10,10,10]

#FT array can be manipulated e.g. Removing coefficients for smoothing

IFT = irfftn(FT) #Inverse fourier transform function takes array as input and outputs an array of 2(N-1) size e.g. If FT is 100x100x51, IFT will be 100x100x100
print IFT[10,10,10]

#An explicit Discrete Fourier Transform code is supplied in the slides, but is more complex and less efficient than the in built fast fourier transforms.


# re-implementation of k-means algorithm
# author Yan Shu

import numpy as np
import matplotlib.pyplot as plt

# task 1  separate 19 samples into two clusters
# input
x_list=[0,1,0,1,2,1,2,3,6,7,8,7,8,9,7,8,9,8,9]
y_list=[0,0,1,1,1,2,2,2,6,6,6,7,7,7,8,8,8,9,9]

sample=[]
for i in range(len(x_list)):
    x=x_list[i]
    y=y_list[i]
    num=np.array([[x,y]])
    print(num)
    sample.append(num)
"""
plt.scatter(x_list,y_list,marker='s',s=50)
for x, y in zip(x_list, y_list):
    plt.annotate('(%s,%s)'%(x,y), xy=(x,y),xytext = (0, -10),textcoords='offset points')
plt.show()
"""

K=2 # number of cluster
#initialize cluster center
u1=np.random.rand(1,2)
u2=np.random.rand(1,2)

flag=True
T=np.random.rand(1,19)

sum=0
last_temp=0
index1_list,index2_list=[],[]
while (flag == True):   #until yi is unvariable
    index1_list, index2_list = [], []
    sum=sum+1
    y_label=[]
    for i in range(len(sample)):
        distance1=np.linalg.norm(sample[i]-u1,2)
        distance2=np.linalg.norm(sample[i]-u2,2)
        distance=np.array([distance1,distance2])    # update yi

        y=np.argmin(distance)
        y_label.append(y)

    y_label=np.array(y_label)
    temp = np.linalg.norm(y_label - T, 2)
    if last_temp == temp:
        flag=False
    last_temp=temp

    index1=np.argwhere(y_label==0)
    index2=np.argwhere(y_label==1)
    index1_list.append(index1)
    index2_list.append(index2)

    # update u1,u2
    # calculate u1
    sum1=np.array([[0,0]])
    for i in range(len(index1)):
        index=index1[i][0]
        sum1=sum1+sample[index]
    u1=sum1/len(index1)
    print("u1",u1)

    # calculate u2
    sum2=np.array([[0,0]])
    for i in range(len(index2)):
        index=index2[i][0]
        sum2=sum2+sample[index]

    u2=sum2/len(index2)
    print("u2",u2)

cluster1x,cluster1y=[],[]
for i in  index1_list:
    for j in i:
        print(j[0])
        x=x_list[j[0]]
        cluster1x.append(x)
        y=y_list[j[0]]
        cluster1y.append(y)

cluster2x,cluster2y=[],[]
for i in  index2_list:
    for j in i:
        print(j[0])
        x=x_list[j[0]]
        cluster2x.append(x)
        y=y_list[j[0]]
        cluster2y.append(y)

plt.scatter(cluster1x,cluster1y,marker='s',s=50)
plt.scatter(cluster2x,cluster2y,marker='o',s=50)
plt.show()


import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng()

mu, sigma = 1, 0.5  # Mean and standard deviation
s = rng.normal(mu, sigma, 1000)

if __name__ == '__main__':
    # Verify the mean and the variance:
    print(abs(mu - np.mean(s)))  # Difference should be close to 0.0
    print(abs(sigma - np.std(s, ddof=1)))  # Difference should be close to 0.0

    count, bins, ignored = plt.hist(s, 30, density=True)
    plt.plot(bins, 1 / (sigma * np.sqrt(2 * np.pi)) *
             np.exp(- (bins - mu) ** 2 / (2 * sigma ** 2)),
             linewidth=2, color='r')
    plt.show()


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Fri Jul 19 13:05:23 2019

@author: TempestGuerra
"""

import numpy as np
import math as mt
from HerfunChebNodesWeights import chebpolym, cheblb

def computeChebyshevDerivativeMatrix(DIMS):

       # Get data from DIMS
       ZH = DIMS[2]
       NZ = DIMS[4]

       # Initialize grid and make column vector
       xi, wcp = cheblb(NZ)

       # Get the Chebyshev transformation matrix
       CTD = chebpolym(NZ-1, -xi)

       # Make a diagonal matrix of weights
       W = np.diag(wcp)

       # Compute scaling for the forward transform
       S = np.eye(NZ)

       for ii in range(NZ - 1):
              temp = W.dot(CTD[:,ii])
              temp = ((CTD[:,ii]).T).dot(temp)
              S[ii,ii] = temp ** (-1)

       S[NZ-1,NZ-1] = 1.0 / mt.pi

       # Compute the spectral derivative coefficients
       SDIFF = np.zeros((NZ,NZ))
       SDIFF[NZ-2,NZ-1] = 2.0 * NZ

       for ii in reversed(range(NZ - 2)):
              A = 2.0 * (ii + 1)
              B = 1.0
              if ii > 0:
                     c = 1.0
              else:
                     c = 2.0

              SDIFF[ii,:] = B / c * SDIFF[ii+2,:]
              SDIFF[ii,ii+1] = A / c

       # Chebyshev spectral transform in matrix form
       temp = CTD.dot(W)
       STR_C = S.dot(temp);
       # Chebyshev spatial derivative based on spectral differentiation
       # Domain scale factor included here
       temp = (CTD).dot(SDIFF)
       DDM = - (2.0 / ZH) * temp.dot(STR_C);

       return DDM, STR_C


import numpy as np
import dykstra

# Define projection functions
def p_box(x,l,u):
    return np.minimum(np.maximum(x,l), u)

def p_ball(x,c,r):
    return c + (r/np.max([np.linalg.norm(x-c),r]))*(x-c)

def p_halfspace(x,a,b):
    return x - a*np.maximum(a.dot(x) - b,0)/a.dot(a)

# Generate projection functions
ball_c = [1,1]
ball_r = 2
pball = lambda x: p_ball(x,np.array(ball_c),ball_r)

box_l = np.array([-1,-1])
box_u = np.array([1,1])
pbox = lambda x: p_box(x,box_l,box_u)

a = [1,1]
bconst = 0.2
phalf = lambda x: p_halfspace(x,np.array(a),bconst)

# Use Dykstra's algorithm to project onto the intersection
#  np.random.seed(12)
start = np.random.uniform(low=-10,high=80, size=(2,))
x_pred = dykstra.project([pbox,pball,phalf], start, max_iter=10000, tol=1.0e-20)
print("Start:", start)
print("Pred:", x_pred)

if x_pred[0]+x_pred[1] <= 0.2 and (box_l[0] <= x_pred[0] <= box_u[0] and box_l[1] <= x_pred[1] <= box_u[1]) and ((x_pred[0]-1)**2 + (x_pred[1]-1)**2 <= 2**2):
    print("In constraint set")
else:
    # Print out constraints and values x_pred obtains to determine the error
    print(f'x+y = {x_pred[0]+x_pred[1]} (0.2), point: ({x_pred[0]},{x_pred[1]}), box upper: {box_u}, box lower: {box_l}, (x-1)^2+(y-1)^2 = {(x_pred[0]-1)**2 + (x_pred[1]-1)**2} (<=4)')


import GPy
import numpy as np
from dotdic import DotDic
from loader import save_to

def genData(Ntrain, Ntest, Mbuild, D, lengthscale, sig2_noise, sig2_0,lower,upper, seed=123):
    np.random.seed(seed)

    k = GPy.kern.RBF(input_dim=D,lengthscale=lengthscale)
    k.variance = sig2_0

    range_ = upper - lower

    RRm = np.random.rand(Mbuild,D) * range_ + lower
    Xtrain = np.random.rand(Ntrain,D) * range_ + lower
    Xtest = np.random.rand(Ntest,D) * range_ + lower

    mu = np.zeros((Mbuild)) # vector of the means
    C = k.K(RRm,RRm) # covariance matrix
    w = np.random.multivariate_normal(mu,C,1).transpose()

    iKrr = np.linalg.inv(C + np.eye(Mbuild)*1e-7)
    iKrr_w = np.dot(iKrr, w)

    Kxtr = k.K(Xtrain, RRm)
    Kxpr = k.K(Xtest, RRm)

    Ftrain = np.dot(Kxtr, iKrr_w)
    Ftest = np.dot(Kxpr, iKrr_w)

    Y_train = Ftrain + np.random.randn(Ntrain,1) * np.sqrt(sig2_noise)
    Y_test = Ftest + np.random.randn(Ntest,1) * np.sqrt(sig2_noise)

    return Xtrain, Xtest, Y_train, Y_test, Ftrain, Ftest

def save(D,lengthscale,variance,gaussian_noise,
         Ntrain=100000,Ntest=10000,M_build=2000,
         lower = -1,upper = 1,seed = 123):

    Xtrain, Xtest, Ytrain, Ytest, Ftrain, Ftest = \
        genData(Ntrain=Ntrain,
                Ntest=Ntest,
                Mbuild=M_build,
                D=D,
                lengthscale=lengthscale,
                sig2_0=variance,
                sig2_noise=gaussian_noise,
                lower=lower,
                upper=upper,
                seed = seed)

    data = {}
    data['Xtrain'] = Xtrain
    data['Xtest'] = Xtest
    data['Ytrain'] = Ytrain
    data['Ytest'] = Ytest
    data['Ftrain'] = Ftrain
    data['Ftest'] = Ftest

    data['lengthscale'] = np.repeat(lengthscale, D)
    data['variance'] = variance
    data['gaussian_noise'] = gaussian_noise

    data['lower'] = lower
    data['upper'] = upper
    data['D'] = D

    data = DotDic(data)

    name = "gendata_{0}_{1}_{2}_{3}_{4}_{5}_{6}_{7}".format(
        D,
        Ntrain,
        Ntest,
        lengthscale,
        variance,
        gaussian_noise,
        lower,
        upper)

    print("Saving {0}".format(name))
    save_to(obj=data,
            object_str='data',
            path='./data/{0}/'.format(name))


from numpy import sqrt, arctan, pi, exp, random, shape
I = 1.0j
# A package for beam optics in python

def spot_size(z, zR, w0):
    """Calculate spot size at z, given zR, and w0"""
    return w0 * sqrt(1 + (z/zR)*(z/zR))

def radius_curvature(z, zR):
    """calculate R(z)"""
    # This could be smarter, just adding epsilon to avoid nan's
    if (z == 0):
        z += 1e-31
    return z * (1 + (zR/z)*(zR/z))

def guoy_phase(z, zR):
    """really just atan(z/zR)"""
    return arctan(z/zR)

def rayleigh_range(w0, wavelambda):
    """Calculate rayleigh range"""
    return pi*w0*w0/wavelambda

def gaussian_beam(x, y, z, E0, wavelambda, w0, k):
    """full gaussian beam at x, y, z given the beam parameters\n
    E0 is the electric field amplitude
    wavelambda is the wavelength
    w0 is the beam waist (1/e field radius and 1/e^2 intensity radius)
    k is a tuple of [kx,ky,kz]"""

    r = sqrt(x*x + y*y)
    zR = rayleigh_range(w0, wavelambda)
    w = spot_size(z, zR, w0)
    R = radius_curvature(z, zR)
    eta = guoy_phase(z, zR)
    return E0 * w0/w * exp(- r*r/(w*w)) *\
        exp(-I*k[2]*z - I*k[2]*r*r/(2*R) + I*eta)*exp(I*k[0]*x + I*k[1]*y) #\
        #+ sqrt(E0)*random.random([max(shape(x)), max(shape(y))])
    # The exp(ikz) term in this definition causes extra phase accumulation
    # compared to the BPM. I need to sort this out for sure.
    # The following agrees with BPM:
    # return w0/w * exp(- r*r/(w*w)) * exp(- 1j*k*r*r/(2*R) + 1j*eta)

def plane_wave_beam(x, y, z, A, k):
    """a simple plane wave mostly used for testing"""
    #dphase = 1/(2*A**2)
    #noiseamp = sqrt(A) * (random.random() - 0.5)
    #noisephase = exp(2*pi*1j*dphase*(random.random() - 0.5))
    # noise = random.normal(0, 0.5) * exp(1j*2*pi*random.random())
    # return (A + noise) * exp(I*k[0]*x + I*k[1]*y + I*k[2]*z)
    return (A) * exp(I*k[0]*x + I*k[1]*y + I*k[2]*z)
    #return A * exp(I*k[0]*x + I*k[1]*y + I*k[2]*z)
    # this is more accurate, the best way to model is to let
    # the amplitude have noise.


"""Module for solution to
https://www.codewars.com/kata/mean-variance-and-standard-deviation-of-a-probability-distribution-for-discrete-variables."""

from numpy import mean
from math import sqrt

def stats_disc_distr(ll):
    """Return a list with 3 values, mean, var and standard deviation based on the input.
    The input is a list of lists and each list contains the value of a variable x
    and it's probability P(x)."""
    out = sanitize_input(ll)
    if out:
        return out
    avg = mean([l[0] for l in ll])
    variance = sum([(l[0] - avg) ** 2 * l[1] for l in ll])
    stddev = sqrt(variance)
    return [avg, variance, stddev]

def sanitize_input(ll):
    """Return an alert based on issues with dataset. Return None if no issue."""
    p = sum([l[1] for l in ll])
    if not all([l[0] == int(l[0]) for l in ll]):
        if round(p, 5) != 1:
            return "It's not a valid distribution and furthermore, one or more variable value are not integers"
        else:
            return "All the variable values should be integers"
    if round(p, 5) != 1:
        return "It's not a valid distribution"


import numpy as np

def angular_distance(ra1, dec1, ra2, dec2):
    """
    Returns the angular distance between two points, two sets of points, or a set of points and one point.

    :param ra1: array or float, longitude of first point(s)
    :param dec1: array or float, latitude of first point(s)
    :param ra2: array or float, longitude of second point(s)
    :param dec2: array or float, latitude of second point(s)
    :return: angular distance(s) in degrees
    """

    # Vincenty formula, slower than the Haversine formula in some cases, but stable also at antipodes

    lon1 = np.deg2rad(ra1)
    lat1 = np.deg2rad(dec1)
    lon2 = np.deg2rad(ra2)
    lat2 = np.deg2rad(dec2)

    sdlon = np.sin(lon2 - lon1)
    cdlon = np.cos(lon2 - lon1)
    slat1 = np.sin(lat1)
    slat2 = np.sin(lat2)
    clat1 = np.cos(lat1)
    clat2 = np.cos(lat2)

    num1 = clat2 * sdlon
    num2 = clat1 * slat2 - slat1 * clat2 * cdlon
    denominator = slat1 * slat2 + clat1 * clat2 * cdlon

    return np.rad2deg(np.arctan2(np.sqrt(num1 ** 2 + num2 ** 2), denominator))


import numpy
import time
import matplotlib.pyplot as plt

def merge_sort(myarray):
	if len(myarray) > 1:
		#Diving the array elements in two halfs
		S1 = myarray[:len(myarray)//2]
		S2 = myarray[len(myarray)//2:]

		#sorting the two halves

		merge_sort(S1)
		merge_sort(S2)

	#array = ['x' for m in range( (len(S1) + len(S2)) )]
		i = j = k = 0
		p = len(S1)
		q = len(S2)
		while i < p and j < q:
			if S1[i] <= S2[j]:
				myarray[k] = S1[i]
			#print(array)
				i = i +1
			else:
				myarray[k] = S2[j]
				j = j+1
			#print(array)
			k = k+1
		#print(S1)
		#print(S2)

		if i == p:
			myarray[k:(p+q)] = S2[j:(q)]
		else:
			myarray[k:(p+q)] = S1[i:(p)]
		return(myarray)

# def create_n_random_arrays(n, mysize):
# 	list_of_arrays = []
# 	for i in range(n):
# 		created_array = numpy.random.randint(-200, 200, size=mysize, dtype=int)
# 		list_of_arrays.append(created_array)
# 	return list_of_arrays

# list_of_avarages = []

# for i in [5,10, 50, 100, 500, 1000]:
# 	lenght_i = create_n_random_arrays(10, i)
# 	attempt = []
# 	for array in lenght_i:
# 		start_time = time.time()
# 		merge_sort_2(array)
# 		end_time = time.time()
# 		time_taken = end_time - start_time
# 		attempt.append(time_taken)
# 	attempt_avarage_i = sum(attempt) / len(attempt)
# 	list_of_avarages.append(attempt_avarage_i)

# #print(list_of_avarages)

# #simple check

# #if all(list_of_avarages[i] < list_of_avarages[i+1] for i in range(len(list_of_avarages)-1)):
# #	print("TRUE")

# size = [5,10, 50, 100, 500, 1000]

# avarage = list_of_avarages

# plt.plot(size, avarage)
# plt.xlabel('Size')
# plt.ylabel('Avarage')
# plt.show()

# #size = [5,10, 50, 100, 500, 1000, 5000]

# #plt.plot(size, list_of_avarages_bubble , 'b-', label='bubble sort')
# #plt.plot(size, list_of_avarages_merge, 'r-', label='merge sort')

# #plt.xlabel('Size')
# #plt.ylabel('Avarage')
# #plt.show()


# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: percent
#       format_version: '1.3'
#       jupytext_version: 1.13.6
#   kernelspec:
#     display_name: Python 3 (ipykernel)
#     language: python
#     name: python3
# ---

# %% [markdown]
# ---
# # SciPy -  [integrate](https://docs.scipy.org/doc/scipy/reference/integrate.html)
#
# ### Solve ordinary differential equations (ODEs).
#
#
# ---

# %% [markdown]
#
# ## Standard import

# %%
from scipy import integrate as intg

# %% [markdown]
# Notice that NumPy is not necessary. SciPy will convert native Python arrays to NumPy arrays internally.

# %% [markdown]
# ---
# #  Initial value problems: [`solve_ivp`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html#scipy.integrate.solve_ivp)
# ---
#
# An *initial value problem* (IVP) consists of an ODE:
#
# \begin{equation} \frac{d\textbf{x}}{dt} = f(t,\textbf{x}) \end{equation}
#
# and an *intial condition*,
#
# \begin{equation} \textbf{x}(t_0) = \textbf{x}_0 \end{equation}
#
#
#
# + The function $f(t,\textbf{x})$ gives us the rate of change of $\textbf{x}$, as a function of $t$ and the current value of $\textbf{x}$.
# + $\textbf{x}$ is a list containing all of the variables of the ODE.
#
# The steps are,
# 1. Define $f(t,\textbf{x})$.
# 2. Define the time span.
# 3. Define the initial condition.
# 4. Call `solve_ivp(fun, t_span, x_initial, optional parameters ...)`.
#
# The optional parameters include:
# + `method`: choose the solver. Default is RK45.
# + `args`: Pass in parameters for the ODE.
# + `max_step`: Parameters for controlling the step size.
# + `t_eval`: Time vector used for reporting.

# %% [markdown]
# ## Example: Lotka-Volterra model of preditor-prey dynamics
#
# + $x_1$ ... number of prey (rabbits)
# + $x_2$ ... number of predators (foxes)
#
# \begin{align}
# &\dot{x}_1 = \alpha x_1 - \beta x_1 x_2 \\
# &\dot{x}_2 = \delta x_1 x_2 -\gamma x_2
# \end{align}
#
# + The growth rate of the population of prey ($\dot{x}_1$) is
#     + increased by the size of the population times the birth rate ($\alpha x_1$)
#     + decreased by the rate of predation ($\beta x_1 x_2$)
#
# + The growth rate of the population of predators ($\dot{x}_2$) is
#     + increased by the availability of food, which is proportional to predation ($\delta x_1 x_2$)
#     + decreased by the rate of death of predators ($\gamma x_2 $)
#
# Use these values for the parameters:
# \begin{align}
# \alpha &= 1.5 \\
# \beta &= 1 \\
# \gamma &= 3 \\
# \delta &= 1
# \end{align}
#
#

# %%
# 1. Define the function f(t,x,parameters)
def f(t,X,a,b,c,d):
    x1, x2 = X
    x1dot = a*x1 - b*x1*x2
    x2dot = d*x1*x2 - c*x2
    return [x1dot,x2dot]

# 2. Time span
t_span = [0,10]

# 3. Initial condition
Xo = [25,5]

# 4. Parameters
a=1.5
b=1
c=3
d=1

# 5. Call the solver, obtain the solution
sol = intg.solve_ivp(f, t_span, Xo, args=(a,b,c,d))

# %%
sol

# %% [markdown]
# `sol` will contain information about the result.
# + whether or not it succeeded: `sol.success`,
# + the solution: `sol.t`, `sol.y`
#
#
# ### Plot the solution

# %%
import matplotlib.pyplot as plt

plt.figure(figsize=(10,3))
plt.plot(sol.t,sol.y[0],label='prey')
plt.plot(sol.t,sol.y[1],label='predators')
plt.grid()
plt.xlim(t_span)
plt.ylim([0,30])
plt.legend()
plt.xlabel('time')
plt.ylabel('population')

# %% [markdown]
# ### A somewhat nicer plot

# %%
def plotsol(sol):
    plt.figure(figsize=(10,3))
    plt.plot(sol.t,sol.y[0],label='prey')
    plt.fill_between(sol.t,sol.y[0],0,alpha=0.3)
    plt.plot(sol.t,sol.y[1],label='predator')
    plt.fill_between(sol.t,sol.y[1],0,alpha=0.3)
    plt.grid()
    plt.xlim(t_span)
    plt.ylim([0,30])
    plt.legend(fontsize=16)
    plt.xlabel('time',fontsize=16)
    plt.ylabel('population',fontsize=16)

plotsol(sol)

# %% [markdown]
# The plot is too coarse. We have three options:
# 1. Reduce the maximum step size,
# 2. Provide the time axis to the solver,
# 3. Request a 'continuous' solution. (we won't cover this).
#
# ## Reduce the maximum step size: `max_step`

# %%
sol = intg.solve_ivp(f, t_span, Xo, args=(a,b,c,d), max_step=.01 )

plotsol(sol)

# %% [markdown]
# ## Provide the time axis to the solver: `t_eval`
# `t_eval` specifies the times where a solution will be returned.

# %%
sol = intg.solve_ivp(f, t_span, Xo, args=(a,b,c,d), t_eval=[0,1,4,6,10])

plotsol(sol)


# Import the libraries we need for this lab

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits import mplot3d

# The class for plotting the diagrams

class plot_error_surfaces(object):

    # Constructor
    def __init__(self, w_range, b_range, X, Y, n_samples=30, go=True):
        W = np.linspace(-w_range, w_range, n_samples)
        B = np.linspace(-b_range, b_range, n_samples)
        w, b = np.meshgrid(W, B)
        Z = np.zeros((30, 30))
        count1 = 0
        self.y = Y.numpy()
        self.x = X.numpy()
        for w1, b1 in zip(w, b):
            count2 = 0
            for w2, b2 in zip(w1, b1):
                Z[count1, count2] = np.mean((self.y - w2 * self.x + b2) ** 2)
                count2 += 1
            count1 += 1
        self.Z = Z
        self.w = w
        self.b = b
        self.W = []
        self.B = []
        self.LOSS = []
        self.n = 0
        if go == True:
            plt.figure()
            plt.figure(figsize=(7.5, 5))
            plt.axes(projection='3d').plot_surface(self.w, self.b, self.Z, rstride=1, cstride=1, cmap='viridis',
                                                   edgecolor='none')
            plt.title('Loss Surface')
            plt.xlabel('w')
            plt.ylabel('b')
            plt.show()
            plt.figure()
            plt.title('Loss Surface Contour')
            plt.xlabel('w')
            plt.ylabel('b')
            plt.contour(self.w, self.b, self.Z)
            plt.show()

    # Setter
    def set_para_loss(self, W, B, loss):
        self.n = self.n + 1
        self.W.append(W)
        self.B.append(B)
        self.LOSS.append(loss)

    # Plot diagram
    def final_plot(self):
        ax = plt.axes(projection='3d')
        ax.plot_wireframe(self.w, self.b, self.Z)
        ax.scatter(self.W, self.B, self.LOSS, c='r', marker='x', s=200, alpha=1)
        plt.figure()
        plt.contour(self.w, self.b, self.Z)
        plt.scatter(self.W, self.B, c='r', marker='x')
        plt.xlabel('w')
        plt.ylabel('b')
        plt.show()

    # Plot diagram
    def plot_ps(self):
        plt.subplot(121)
        plt.ylim()
        plt.plot(self.x, self.y, 'ro', label="training points")
        plt.plot(self.x, self.W[-1] * self.x + self.B[-1], label="estimated line")
        plt.xlabel('x')
        plt.ylabel('y')
        plt.title('Data Space Iteration: ' + str(self.n))
        plt.subplot(122)
        plt.contour(self.w, self.b, self.Z)
        plt.scatter(self.W, self.B, c='r', marker='x')
        plt.title('Loss Surface Contour')
        plt.xlabel('w')
        plt.ylabel('b')
        plt.show()

# Import PyTorch library

import torch
torch.manual_seed(1)

# Generate the data with noise and the line

X = torch.arange(-3, 3, 0.1).view(-1, 1)
f = 1 * X - 1
Y = f + 0.1 * torch.randn(X.size())

# Plot the line and the data

plt.plot(X.numpy(), Y.numpy(), 'rx', label = 'y')
plt.plot(X.numpy(), f.numpy(), label = 'f')
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.show()

# Define the prediction function

def forward(x):
    return w * x + b

# Define the cost function

def criterion(yhat, y):
    return torch.mean((yhat - y) ** 2)

# Create a plot_error_surfaces object.

get_surface = plot_error_surfaces(15, 13, X, Y, 30)

# Define the function for training model

w = torch.tensor(-15.0, requires_grad = True)
b = torch.tensor(-10.0, requires_grad = True)
lr = 0.1
LOSS_BGD = []

def train_model_BGD(epochs):
    for epoch in range(epochs):
        Yhat = forward(X)
        loss = criterion(Yhat, Y)
        LOSS_BGD.append(loss)
        get_surface.set_para_loss(w.data.tolist(), b.data.tolist(), loss.tolist())
        get_surface.plot_ps()
        loss.backward()
        w.data = w.data - lr * w.grad.data
        b.data = b.data - lr * b.grad.data
        w.grad.data.zero_()
        b.grad.data.zero_()

# Run train_model_BGD with 10 iterations

train_model_BGD(10)

# Create a plot_error_surfaces object.

get_surface = plot_error_surfaces(15, 13, X, Y, 30, go = False)

# Import libraries

from torch.utils.data import Dataset, DataLoader

# Create class Data

class Data(Dataset):

    # Constructor
    def __init__(self):
        self.x = torch.arange(-3, 3, 0.1).view(-1, 1)
        self.y = 1 * X - 1
        self.len = self.x.shape[0]

    # Getter
    def __getitem__(self, index):
        return self.x[index], self.y[index]

    # Get length
    def __len__(self):
        return self.len

# Create Data object and DataLoader object

dataset = Data()
trainloader = DataLoader(dataset = dataset, batch_size = 1)

# Define train_model_SGD function

w = torch.tensor(-15.0, requires_grad = True)
b = torch.tensor(-10.0, requires_grad = True)
LOSS_SGD = []
lr = 0.1
def train_model_SGD(epochs):
    for epoch in range(epochs):
        Yhat = forward(X)
        get_surface.set_para_loss(w.data.tolist(), b.data.tolist(), criterion(Yhat, Y).tolist())
        get_surface.plot_ps()
        LOSS_SGD.append(criterion(forward(X), Y).tolist())
        for x, y in trainloader:
            yhat = forward(x)
            loss = criterion(yhat, y)
            get_surface.set_para_loss(w.data.tolist(), b.data.tolist(), loss.tolist())
            loss.backward()
            w.data = w.data - lr * w.grad.data
            b.data = b.data - lr * b.grad.data
            w.grad.data.zero_()
            b.grad.data.zero_()
        get_surface.plot_ps()

# Run train_model_SGD(iter) with 10 iterations

train_model_SGD(10)

# Create a plot_error_surfaces object.

get_surface = plot_error_surfaces(15, 13, X, Y, 30, go = False)

# Create DataLoader object and Data object

dataset = Data()
trainloader = DataLoader(dataset = dataset, batch_size = 5)

# Define train_model_Mini5 function

w = torch.tensor(-15.0, requires_grad = True)
b = torch.tensor(-10.0, requires_grad = True)
LOSS_MINI5 = []
lr = 0.1

def train_model_Mini5(epochs):
    for epoch in range(epochs):
        Yhat = forward(X)
        get_surface.set_para_loss(w.data.tolist(), b.data.tolist(), criterion(Yhat, Y).tolist())
        get_surface.plot_ps()
        LOSS_MINI5.append(criterion(forward(X), Y).tolist())
        for x, y in trainloader:
            yhat = forward(x)
            loss = criterion(yhat, y)
            get_surface.set_para_loss(w.data.tolist(), b.data.tolist(), loss.tolist())
            loss.backward()
            w.data = w.data - lr * w.grad.data
            b.data = b.data - lr * b.grad.data
            w.grad.data.zero_()
            b.grad.data.zero_()

# Run train_model_Mini5 with 10 iterations.

train_model_Mini5(10)

# Create a plot_error_surfaces object.

get_surface = plot_error_surfaces(15, 13, X, Y, 30, go = False)

# Create DataLoader object

dataset = Data()
trainloader = DataLoader(dataset = dataset, batch_size = 10)

# Define train_model_Mini5 function

w = torch.tensor(-15.0, requires_grad = True)
b = torch.tensor(-10.0, requires_grad = True)
LOSS_MINI10 = []
lr = 0.1

def train_model_Mini10(epochs):
    for epoch in range(epochs):
        Yhat = forward(X)
        get_surface.set_para_loss(w.data.tolist(), b.data.tolist(), criterion(Yhat, Y).tolist())
        get_surface.plot_ps()
        LOSS_MINI10.append(criterion(forward(X),Y).tolist())
        for x, y in trainloader:
            yhat = forward(x)
            loss = criterion(yhat, y)
            get_surface.set_para_loss(w.data.tolist(), b.data.tolist(), loss.tolist())
            loss.backward()
            w.data = w.data - lr * w.grad.data
            b.data = b.data - lr * b.grad.data
            w.grad.data.zero_()
            b.grad.data.zero_()

# Run train_model_Mini5 with 10 iterations.

train_model_Mini10(10)

# Plot out the LOSS for each method

plt.plot(LOSS_BGD,label = "Batch Gradient Descent")
plt.plot(LOSS_SGD,label = "Stochastic Gradient Descent")
plt.plot(LOSS_MINI5,label = "Mini-Batch Gradient Descent, Batch size: 5")
plt.plot(LOSS_MINI10,label = "Mini-Batch Gradient Descent, Batch size: 10")
plt.legend()

# Practice: Perform mini batch gradient descent with a batch size of 20.

dataset = Data()


from abc import ABC, abstractmethod
from typing import List, Dict, Tuple

import numpy
from matplotlib import pylab
from numpy import random
from sklearn.datasets import load_digits
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

from config import logging_configurator
from paddles.paddle_0402.config.paddle_configurator import nmist_raw_ann_config

logger = logging_configurator.get_logger(__name__)

"""
See Adventures in Machine Learning: neural-networks-tutorial https://adventuresinmachinelearning.com/neural-networks-tutorial/
for a great tutorial that goes thru all this math
"""

def plot_sigmoid() -> None:
    """
    Will plot a sigmoid activation function without offset
    :return: None
    """
    x = numpy.arange(-8, 8, 0.1)
    f = 1 / (1 + numpy.exp(-x))
    pylab.plot(x, f)
    pylab.xlabel('x')
    pylab.ylabel('f(x)')
    pylab.show()

def plot_cost_function(iterations_costs: list) -> None:
    """
    will plot the cost function so you can see how cost decreases from iteration to iteration.  The elbow
    will be the sweet spot
    :param iterations_costs: a list whose elements are the cost: norm(y_prediction, actual) for every iteration
    of the artifical neural network
    :return:  None
    """
    pylab.plot(iterations_costs)
    pylab.ylabel('Average Cost Per Iteration')
    pylab.xlabel('Iteration number')
    pylab.show()

def sigmoid_activation(aggregate: numpy.ndarray) -> float:
    """

    :param aggregate: an array whose elements are the aggregate of all the inputs from the previous layer.
    For instance, the first element of the array is the aggregate of all input coming into the first node,
    the second element of the array is the aggregate of all input coming into the second node, and so on.
    By aggregate, we just mean the Z value.  (The sum of all inputs into a node scaled by the weights)
    :return: an array of same size as aggregate whose elements are the applied sigmoid on the aggregate
    """
    # note, numpy.exp can operate on arrays.  It will take the exponential of each element in the array
    return 1 / (1 + numpy.exp(-aggregate))

def sigmoid_derivative(value: numpy.ndarray) -> float:
    """

    :param value: array of numbers
    :return: array of same size as input, with applied sigmoid derivative.  This is used during back propogation
    """
    return sigmoid_activation(value) * (1 - sigmoid_activation(value))

def brute_force_feed_forward(input: numpy.ndarray,
                             layer_weights: Dict[int, numpy.ndarray],
                             bias_weights: Dict[int, numpy.ndarray]) -> Tuple[
    Dict[int, numpy.ndarray], Dict[int, numpy.ndarray]]:
    """
    :param input:          input_n x 1 array.  1D array for the input.
    :param layer_weights:  a dict of weight matrices for every layer.  So, the matrix for l=1 will be
    the weight matrix for the weights on the input layer, and so on.
    Each row of the weight matrix represents all the weights going into a single node into layer l+1.
    Each col represents all the weights going out of a single node from layer l.
    As an example, for a 3-layer network (input, one hidden layer, output) where input layer has 3
    nodes, hidden layer has 3 nodes, and output layer has a single node will have two weight matrices:
    layer_weights = [w1, w2]  where w_i_j notation means i==> node in next layer, j ==> node in current layer.
    W_layer_(l)_to_(l+1) = [ w11    w12    w13]
                           [ w21    w22    w23]
                           [ w31    w32    w33]

    :param bias_weights: dict of 1D arrays for the bias weights of the layer.  For our 3-layer network:
    bias_weights = [b1, b2] where
    b1 = [a b c]^T
    and b2 = [d]
    :return: a 2-tuple whose first element is a dict by layer of 1D array of the layer outputs and whose
    second element is a dict by layer of the 1-D array of aggregated inputs for each layer (for convenience)
    """

    # according to tradition, the input layer is layer 1 (not zero), which causes no end of headaches for indexing
    outputs_by_layer = {1: input}
    aggregates_by_layer = {}
    num_layers = len(layer_weights) + 1
    output_from_next_layer = None
    for layer_index in range(1, num_layers):
        layer_weight_matrix = layer_weights[layer_index]  # weights going from layer (l) to (l+1)
        layer_bias_weights = bias_weights[layer_index]  # bias terms going into (l+1) layer
        layer_input = input if output_from_next_layer is None else output_from_next_layer
        aggregates_next_layer = layer_weight_matrix.dot(layer_input) + layer_bias_weights
        output_from_next_layer = sigmoid_activation(aggregates_next_layer)
        aggregates_by_layer[layer_index + 1] = aggregates_next_layer
        outputs_by_layer[layer_index + 1] = output_from_next_layer
    return outputs_by_layer, aggregates_by_layer

def scale_data(input_data: numpy.ndarray) -> numpy.ndarray:
    """
    It is common to scale the input data for a neural network to be between  [0,1] or [-1,1].  This is because
    it is seen the neural network back propogation converges faster (and needs less data) when the input data
    is on the same scale.  Otherwise, weights will vary wildly from input data element to input data element and
    this leads to longer convergence time.
    :param input_data: 2d numpy array of shape (num_samples, num_features)
    :return: 2d numpy array where features have had their mean subtracted and normalized to their standard deviation.
             In other words, most features will end up being between [-1, 1] with some outliers.
    """
    scaler = StandardScaler()
    return scaler.fit_transform(input_data.data)

def generate_random_weights(network_layer_sizes: List[int]) -> Dict[int, numpy.ndarray]:
    """
    :param network_layer_sizes: list of sizes for each network layer
    :return: a dict whose keys are the layers (layer 1 is the input layer) and whose
    values are the weight matrix between layer l and layer l+1
    """
    num_layers = len(network_layer_sizes)
    weight_matrix_by_layer = {}
    for layer_index in range(0, num_layers - 1):
        # each row are the incoming weights coming into a single node in layer l+1
        # each column are all the weights coming out of a single node from layer l
        num_nodes_this_layer = network_layer_sizes[layer_index]
        num_nodes_next_layer = network_layer_sizes[layer_index + 1]
        weights = numpy.random.random_sample((num_nodes_next_layer, num_nodes_this_layer))

        # it is common nomenclature to refer to the first layer as layer 1 (rather than layer zero)
        weight_matrix_by_layer[layer_index + 1] = weights
    return weight_matrix_by_layer

def generate_random_biases(network_layer_sizes: List[int]) -> Dict[int, numpy.ndarray]:
    """
    :param network_layer_sizes: list of sizes for each network layer
    :return: a dict whose keys are the layers (layer 1 is the input layer) and whose
    values are the bias vectors for the layer
    """
    num_layers = len(network_layer_sizes)
    bias_vectors_by_layer = {}
    for layer_index in range(1, num_layers):
        # notation for bias terms is a bit odd.  b^l is the bias vector from layer l-1 going into layer l
        bias_vectors_by_layer[layer_index] = numpy.random.random_sample((network_layer_sizes[layer_index],))
    return bias_vectors_by_layer

def initialize_gradient_weight_accumulation_matrices(network_layer_sizes: List[int]) -> Dict[int, numpy.ndarray]:
    """
    :param network_layer_sizes:  list of sizes for each network layer
    :return: zeroed out weight matrices for each each layer.  Meant to be used in backpropagation to
    accumulate partial derivative values for each weight
    """
    num_layers = len(network_layer_sizes)
    weight_matrix_by_layer = {}
    for layer_index in range(0, num_layers - 1):
        # each row are the incoming weights coming into a single node in layer l+1
        # each column are all the weights coming out of a single node from layer l
        num_nodes_this_layer = network_layer_sizes[layer_index]
        num_nodes_next_layer = network_layer_sizes[layer_index + 1]
        weights = numpy.zeros((num_nodes_next_layer, num_nodes_this_layer))

        # it is common nomenclature to refer to the first layer as layer 1 (rather than layer zero)
        weight_matrix_by_layer[layer_index + 1] = weights
    return weight_matrix_by_layer

def initialize_bias_accumulation_vectors(network_layer_sizes: List[int]) -> Dict[int, numpy.ndarray]:
    """
    :param network_layer_sizes:  list of sizes for each network layer
    :return: zeroed out bias vectors for each each layer.  Meant to be used in backpropagation to
    accumulate partial derivate values for each bias
    """
    num_layers = len(network_layer_sizes)
    bias_vectors_by_layer = {}
    for layer_index in range(1, num_layers):
        # notation for bias terms is a bit odd.  b^l is the bias vector from layer l-1 going into layer l
        bias_vectors_by_layer[layer_index] = numpy.zeros((network_layer_sizes[layer_index],))
    return bias_vectors_by_layer

def calculate_outer_layer_gradient(actual: numpy.ndarray, predicted: numpy.ndarray,
                                   incoming_aggregation: numpy.ndarray) -> numpy.ndarray:
    """

    :param actual:  the array of values for the labeled data.  10 x 1
    :param predicted: the predicted values 10 x 1
    :param incoming_aggregation: the array of aggregated values coming into each node of the outer layer
           (weight*x * bias) 10 x 1
    :return: an array of the error residual multiplied by the deriviate of the activation function. 10x1
    The math shows that the gradient of the weights of the previous layer are proportional to this product.
    (this is the peculiar form when using sigmoid as the activation function)
    """
    return -(actual - predicted) * sigmoid_derivative(incoming_aggregation)

def calculate_previous_layer_gradient(gradient_next_layer: numpy.ndarray,
                                      weight_matrix: numpy.ndarray,
                                      incoming_aggregation: numpy.ndarray) -> numpy.ndarray:
    """
    The chain rule of partial derivatives starting from the outer layer going backwards shows that the
    weights going from layer l to (l+1) have a gradient proportional to the gradients of (l+1) times
    the derivative of the sigmoid evaluated at the aggregation values going into layer l.
    :param gradient_next_layer:  The gradient term from the layer after this one
    :param weight_matrix:        The weight matrix from this layer to the next layer
    :param incoming_aggregation: The aggregated valuues going into each node for this layer. (weight*x + bias)
    :return: The weight matrix * gradient_last_layer * sigmoid derivative at aggregation value
    """
    return numpy.dot(numpy.transpose(weight_matrix), gradient_next_layer) * sigmoid_derivative(incoming_aggregation)

def create_mini_batches(input_samples: numpy.ndarray, output_samples: numpy.ndarray, batch_size: int) -> List[
    Tuple[numpy.ndarray, numpy.ndarray]]:
    """
    :param input_samples: matrix whose rows are training input samples
    :param training_samples: matrix whose rows are training output samples
    :param batch_size: the number of samples per mini-batch
    :return: a list of mini-batches.  Each list element is a 2-tuple whose first element is the input samples and
    whose second element are the associated output samples.  Each mini-batches input/output samples are of size
    batch_size
    """
    num_samples = len(output_samples)

    # random_ids will be a random list of sample ids
    random_ids = random.choice(num_samples, num_samples, replace=False)

    # now to shuffle the samples
    input_samples_shuffled = input_samples[random_ids, :]
    output_samples_shuffled = output_samples[random_ids, :]
    mini_batches = [(input_samples_shuffled[index:index + batch_size],
                     output_samples_shuffled[index:index + batch_size])
                    for index in range(0, num_samples, batch_size)]
    return mini_batches

# load the nmist data
logger.info("Loading nmist data")
digits = load_digits()

# scaled data has shape (data_count, 64)
logger.info("Scaling nmist data")
scaled_data = scale_data(digits.data)

# lets split our data into training and test bins
logger.info("Splitting nmist data into training and test tests")
input_training, input_testing, label_training, label_testing = train_test_split(scaled_data, digits.target,
                                                                                test_size=0.4)

# lets convert our target labels to be in vector form.  The labels are just 3, 4, etc -- whatever the digit
# is.  Lets convert the output to a 10 deep array with all elements zero except for the digit which is 1
# output will have shape (data_count, 10)
output_training = numpy.zeros((len(label_training), 10))
for label_index, label in enumerate(label_training):
    output_training[label_index, label] = 1

output_testing = numpy.zeros((len(label_testing), 10))
for label_index, label in enumerate(label_testing):
    output_testing[label_index, label] = 1

class Ann(ABC):
    def __init__(self):
        self.avg_cost_for_iterations: list = []

        # lets initialize the weight matrices and the bias vectors for each layer
        logger.info("Initializing network topgrapy and initializing weight matrices and bias vectors for every layer")
        self.network_layer_sizes: list = nmist_raw_ann_config.network_size
        self.num_layers: int = len(self.network_layer_sizes)
        self.weight_matrix_by_layer: Dict[int, numpy.ndarray] = generate_random_weights(self.network_layer_sizes)
        self.bias_by_layer: Dict[int, numpy.ndarray] = generate_random_biases(self.network_layer_sizes)

    @abstractmethod
    def train(self) -> None:
        """
        Implementations will train the weight matrix by layer and the bias by layer on the nmist digit data
        :return:
        """
        pass

    def evalulate_network(self) -> None:
        # this is an array whose elements are the predicted digit
        predicted_labels = numpy.zeros((len(input_testing),))
        for index, test_sample in enumerate(input_testing):
            layer_outputs, layer_aggregates = \
                brute_force_feed_forward(numpy.transpose(test_sample), self.weight_matrix_by_layer, self.bias_by_layer)
            predicted_labels[index] = numpy.argmax(layer_outputs[self.num_layers])
        prediction_accuracy = accuracy_score(label_testing, predicted_labels) * 100
        logger.info(f"Prediction accuracy is {prediction_accuracy}%")


import numpy as np

def l21shrink(epsilon, x):
    """
    Args:
        epsilon: the shrinkage parameter
        x: matrix to shrink on
    Ref:
        wiki Regularization: {https://en.wikipedia.org/wiki/Regularization_(mathematics)}
    Returns:
            The shrunk matrix
    """
    output = x.copy()
    norm = np.linalg.norm(x, ord=2, axis=0)
    for i in range(x.shape[1]):
        if norm[i] > epsilon:
            for j in range(x.shape[0]):
                output[j,i] = x[j,i] - epsilon * x[j,i] / norm[i]
        else:
            output[:,i] = 0.
    return output


# -*- coding: utf-8 -*-
"""
Created on Thu Apr  4 2019

@author: Nahuel
"""
import scipy as sp

# Convolucion Lineal de scipy => sp.convolve(x,h)
def CheckLengthSignal(f,g):
    if (len(g) == len(f)):
        return True
    else:
        return False

def ConvolucionCircular(f, g):
    if (CheckLengthSignal(f,g)):
        y = sp.zeros(len(f));
        for k in range(len(f)):
            for p in range(len(f)):
                y[k] = y[k] + (f[p] * g[k-p])
    else:
        fog = len(f) + len(g) - 1 # condicion de aprox. conv lineal a circular
        cantZerosf = fog - len(f)
        cantZerosg = fog - len(g)
        f = sp.append(f,sp.zeros(cantZerosf)) # agrego los ceros que faltan
        g = sp.append(g,sp.zeros(cantZerosg))
        y = sp.zeros(fog);
        for k in range(fog):
            for p in range(fog):
                y[k] = y[k] + (f[p] * g[k-p])
    return y

""" Test 1
f = [2,5,0,4]
g = [4,1,3]
# Resultado: y = [8 22 11 31 4 12]
"""
""" # Test 2
f = [2,5,0,4]
g = [4,1,3,0]
# Resultado: y = [12 34 11 31]
"""
"""# Test 3
f = [0,2,2,2,1]
g = [2,2,2]
# Resultado: y = [0 4 8 12 10 6 2]
"""


# Copyright (C) 2020 by Landmark Acoustics LLC
r'''An autoregressive model for generating power-law noise.

The method here is outlined in Kasdin, N. J. and T. Walter. 1992. "Discrete
simulation of power law noise." Proc. 1992 IEEE Frequency Control Symposium.
Hershey, PA. pp 274-283.

'''

import itertools

import numpy as np

def generate_ar_coefficients(alpha: float) -> float:
    r'''Coefficients for a discrete autoregressive model of power law noise.

    Parameters
    ----------
    alpha : float
        The exponent of the power law. Its spectrum will have this slope on a
        log-log plot.

    Yields
    ------
    h : float
        The next coefficient in the model. Starts with h_1 because h_0 is one.

    '''

    h = 1.0
    yield h
    g = -1.0 + 0.5*alpha
    for k in itertools.count(1):
        g += 1.0
        h *= g/k
        yield -h

def coefficient_array(alpha: float, degree: int) -> np.ndarray:
    r'''Finds the array and returns it without h_0, which is always 1.

    Parameters
    ----------
    alpha : float
        see `generate_ar_coefficients`.

    degree : int
        The length that the input needs to be.

    Returns
    -------
    np.ndarray : the coefficents of a power-law autoregressive model

    The coefficients are in reverse order. The array's format is
        [h[degree], h[degree-1], ... h[2], h[1]].

    '''

    return np.array([x[0] for x in zip(generate_ar_coefficients(alpha),
                                       range(degree + 1))])


import numpy as np
import pandas as pd
from scipy.stats import norm, skew, kurtosis

def semi_deviation(returns: pd.DataFrame):
    """
    Standard deviation of negative returns
    :param returns: historical returns sequence
    :return: the semi-deviation metric
    """
    return returns[returns < 0].std(ddof=0)

def historic_VaR(returns, confidence_level=5):
    """
    Compute Historic Value at Risk
    :param returns: historical returns sequence
    :param confidence_level: percentile at which to calculate VaR
    :return: the Value at Risk
    """
    if isinstance(returns, pd.DataFrame):
        return returns.aggregate(historic_VaR, confidence_level=confidence_level)
    return -np.percentile(returns, confidence_level)

def parametric_VaR(returns, confidence_level=5, modified=True):
    """
    Compute parametric Value at Risk
    If 'modified is False, then it returns Gaussian VaR, otherwise Cornish-Fisher
    :param returns: a vector of returns
    :param confidence_level: percentile at which to calculate VaR
    :param modified: return modified VaR if True, otherwise normal VaR
    :return: the Value at Risk
    """
    # Z score
    z = norm.ppf(confidence_level / 100)
    if modified:
        # Modify Z score by skewness and kurtosis
        s = skew(returns)
        k = kurtosis(returns, fisher=True, bias=False)
        z += (z ** 2 - 1) * s / 6 + (z ** 3 - 3 * z) * k / 24 - (2 * z ** 3 - 5 * z) * (s ** 2) / 36
    return -(returns.mean() + z * returns.std(ddof=0))

def conditional_VaR(returns, confidence_level=5):
    """
    Compute the Conditional VaR of an array, Series, or DataFrame
    :param returns: A vector of returns
    :param confidence_level: percentile at which to calculate VaR
    :return: the Value at Risk
    """
    if isinstance(returns, pd.DataFrame):
        return returns.aggregate(conditional_VaR, confidence_level=confidence_level)
    is_beyond = returns <= - historic_VaR(returns, confidence_level)
    return -returns[is_beyond].mean()

def compute_drawdown(returns: pd.Series, initial_wealth=1000):
    """
    Takes a time series of asset returns
    :param returns: historical returns sequence
    :param initial_wealth: Initial wealth invested
    :rtype: Drawdown
    :return: a Drawdown object
    """
    wealth_index = initial_wealth * (1 + returns).cumprod()
    previous_peaks = wealth_index.cummax()
    drawdowns = (wealth_index - previous_peaks) / previous_peaks
    return Drawdown(wealth_index, previous_peaks, drawdowns)

class Drawdown:
    """
    History of drawdowns from a return sequence
    """

    def __init__(self, wealth, peaks, drawdowns):
        self.wealth = wealth
        self.peaks = peaks
        self.drawdowns = drawdowns
        self.__df = pd.DataFrame({
            "wealth": wealth,
            "peaks": peaks,
            "drawdown": drawdowns
        })
        self.max_drawdown = drawdowns.min()
        self.max_drawdown_index = drawdowns.idxmin()

    def as_data_frame(self):
        return self.__df