import numpy as np
import matplotlib.pyplot as plt

#Part a
#Text Exercise 6.10 a
#we can modify the number of loops to acheive different levels of accuracy
#in our case we achieved 10^-6 by setting the number of loops to 15
xa=1.0
numOfLoops = 15
for k in range(numOfLoops):
    xa = 1 - np.exp(-2*xa)

print("value of x with accuracy 10**-6:", xa)
print("value of 1 - exp(-2*x) with accuracy 10**-6:", 1 - np.exp(-2*xa))

#Text Exercise 6.10 b
#similar as above, we first reset the variables and instead of a constant c
#we generated a range of values from 0 to 300 with a stepsize of 0.01
x=1.0
numOfLoops = 15
c = np.arange(0, 3, 0.01)
for k in range(numOfLoops):
    x = 1 - np.exp(-c*x)

fig, graph = plt.subplots()
graph.plot(c, x)
graph.set(xlabel="c", ylabel="x",
       title="Solutions of x for a range of c=(0, 3, 0.01) (Q3a)")
graph.grid()
fig.savefig("q3_A.png")
plt.show()

#Part b
#Text Exercise 6.11 b
def F(c):
    """
    This function uses the similar method as above and find the iterations
    when it approaches the convergence.
    """
    sigdig = 10 ** -6
    iterations = 1
    def f(x):
        return 1 - np.exp(-c*x)

    def error(x1, x2):
        return (x1 - x2) / (1 - 1 / (c * np.exp(-c * x1)))

    x1 = 1.0 # starting value
    x2 = f(x1)
    while(abs(error(x1, x2)) > sigdig):
        x1, x2 = x2, f(x2)
        iterations += 1
    print('The minimum number of iterations for an accuracy of 10**-6 = ', iterations)
    print("value of x:", x2)
    print("value of 1 - exp(-2*x):", 1 - np.exp(-2*x2))

#Text Exercise 6.11 c
def F_over(c, w):
    """
    This function uses the over-relaxation method to increases the rate of
    convergence by artifically increasing the gradient of f(x)
    """
    sigdig = 10 ** -6
    iterations = 1
    def f(x):
        return 1 - np.exp(-c*x)

    def derivf(x):
        return c * np.exp(-c*x)

    def error(x1, x2):
        return (x1 - x2)/(1 - 1/((1 + w)*derivf(x1) - w))

    x1 = 1.0  # starting value
    x2 = (1 + w) * f(x1) - w * x1
    while abs(error(x1, x2)) > sigdig:
        x1, x2 = x2, (1 + w) * f(x2) - w * x2
        iterations += 1
    print('When we set omega to be ', w,'the minimum number of iterations for an accuracy of 10**-6 is ', iterations)
    print("value of x:", x2)
    print("value of 1 - exp(-2*x):", 1 - np.exp(-2*x2))

print()
print("part b answer")
F(2)
print()
F_over(2, 0.5)

#Part c
sigfig = 10 ** -6
a = 1
b = 2
def f(x, y):
    return y*(a+x**2)

def g(x, y):
    return b/(a+x**2)

def F(x,y):
    return np.sqrt(b/y-a)

def G(x,y):
    return x/(a+x**2)

def solution(f, g):
    iterations = 1
    def delta(x1, x2):
        return (x1 - x2) / x1
    x1 = 0.5
    y1 = 0.5
    x2 = f(x1, y1)
    y2 = g(x1, y1)

    while abs(delta(x1, x2)) > sigfig and abs(delta(y1, y2)) > sigfig:
        if iterations > 1000:
            return 'error'

        x1, x2, y1, y2 = x2, f(x2, y2), y2, g(x2, y2)
        iterations += 1
    print('The number of iterations = ', iterations, 'the solution for x ~', x2, 'the solution for y ~', y2)
    return [ x2, y2 ]

print()
print("part c answer")
print(solution(F,G))


import numpy as np
##Import other packages here##
import cupy as cp
import matplotlib.pyplot as plt
from cupy.fft import fftfreq, fft, ifft
import math
from cupyx import scipy

# cupy source
# https://docs-cupy.chainer.org/_/downloads/en/v7.0.0/pdf/

# 1a) install cupy
# fft.fftfreq to get frequency array corresponding to number of sample points
# cupy.fft.fftfreq #Return the FFT sample frequencies.

# compute the fft of the full signal
# cupy.fft.fft #Compute the one-dimensional FFT.

# invert the filtered fft with numpy.fft.ifft
# cupy.fft.ifft #Compute the one-dimensional inverse FFT.

# move the array to the host.
# plot -> plt.plot(cp.asnumpy(x), cp.asnumpy(y))

def cupy_fft():

    """
    Modify the code below so it calls the cupy fft function
    """

    pts = 1000
    L = 100
    w0 = 2.0 * cp.pi/L
    n1, n2, n3 = 10.0, 20.0, 30.0
    a1, a2, a3 = 1., 2., 3.

    #create signal data with 3 frequency components
    x = cp.linspace(0,L,pts)
    y1 = a1*cp.cos(n1*w0*x)
    y2 = a2*cp.sin(n2*w0*x)
    y3 = a3*cp.sin(n3*w0*x)
    y = y1 + y2 + y3

    #create signal including only 2 components
    y12 = y1 + y2

    #analytic derivative of signal
    dy = w0*(-n1*a1*cp.sin(n1*w0*x)
            +n2*a2*cp.cos(n2*w0*x)
            +n3*a3*cp.cos(n3*w0*x) )

    #use fft.fftfreq to get frequency array corresponding to number of sample points
    freqs = fftfreq(pts)
    #compute number of cycles and radians in sample window for each frequency
    nwaves = freqs*pts
    nwaves_2pi = w0*nwaves

    # compute the fft of the full signal
    fft_vals = fft(y)

    #mask the negative frequencies
    mask = freqs>0
    #double count at positive frequencies
    fft_theo = 2.0 * cp.abs(fft_vals/pts)
    #plot fft of signal
    plt.xlim((0,50))
    plt.xlabel('cycles in window')
    plt.ylabel('original amplitude')
    plt.plot(cp.asnumpy(nwaves[mask]), cp.asnumpy(fft_theo[mask]))
    plt.show()

    #create a copy of the original fft to be used for filtering
    fft_new = cp.copy(fft_vals)
    #filter out y3 by setting corr. frequency component(s) to zero
    fft_new[cp.abs(nwaves)==n3] = 0.
    #plot fft of filtered signal
    plt.xlim((0,50))
    plt.xlabel('cycles in window')
    plt.ylabel('filtered amplitude')
    plt.plot(cp.asnumpy(nwaves[mask]), cp.asnumpy(2.0*cp.abs(fft_new[mask]/pts)))
    plt.show()

    #invert the filtered fft with numpy.fft.ifft
    filt_data = cp.real(ifft(fft_new))
    #plot filtered data and compare with y12
    plt.plot(cp.asnumpy(x), cp.asnumpy(y12), label='original signal')
    plt.plot(cp.asnumpy(x), cp.asnumpy(filt_data), label='filtered signal')
    plt.xlim((0,50))
    plt.legend()
    plt.show()

    #multiply fft by 2*pi*sqrt(-1)*frequency to get fft of derivative
    dy_fft = 1.0j*nwaves_2pi*fft_vals
    #invert to reconstruct sampled values of derivative
    dy_recon = cp.real(ifft(dy_fft))
    #plot reconstructed derivative and compare with analuytical version
    plt.plot(cp.asnumpy(x), cp.asnumpy(dy), label='exact derivative')
    plt.plot(cp.asnumpy(x), cp.asnumpy(dy_recon), label='fft derivative')
    plt.xlim((0,50))
    plt.legend()
    plt.show()

def cupy_filter():

    """
    Implement code below to:
    Create noise consisting of an array of pts random values chosen from a uniform distribution over the interval [−3,3]
    Create a noisy signal by adding noise to the original signal: y_n = y + noise
    Compute and plot the frequency content of the noisy signal.
    Create and apply an appropriate filter to suppress noise in the frequency domain.
    Invert the filtered fft to obtain a "denoised signal".
    Plot and compare the original, noisy, and denoised signals.
    """

    # pass
    pts = 1000
    L = 100
    w0 = 2.0 * cp.pi/L
    n1, n2, n3 = 10.0, 20.0, 30.0
    a1, a2, a3 = 1., 2., 3.

    #create signal data with 3 frequency components
    x = cp.linspace(0,L,pts)
    y1 = a1*cp.cos(n1*w0*x)
    y2 = a2*cp.sin(n2*w0*x)
    y3 = a3*cp.sin(n3*w0*x)
    y = y1 + y2 + y3

    #noisy signal: uniform distribution range[-3 3]
    ns = cp.random.uniform(-3, 3, pts)

    # y_n = y + noise
    y_n = y + ns

    # #create signal including only 2 components
    # y12 = y1 + y2

    # #analytic derivative of signal
    # dy = w0*(-n1*a1*cp.sin(n1*w0*x)
    #         +n2*a2*cp.cos(n2*w0*x)
    #         +n3*a3*cp.cos(n3*w0*x) )

    #use fft.fftfreq to get frequency array corresponding to number of sample points
    freqs = fftfreq(pts)
    #compute number of cycles and radians in sample window for each frequency
    nwaves = freqs*pts
    # nwaves_2pi = w0*nwaves

    # compute the fft of the full signal
    fft_vals = fft(y)

    # with noisy signal:
    fft_vals_ns = fft(y_n)

    #mask the negative frequencies
    mask = freqs>0

    #double count at positive frequencies
    fft_theo = 2.0 * cp.abs(fft_vals/pts)

    # with noisy signal:
    fft_theo_ns = 2.0 * cp.abs(fft_vals_ns/pts)

# plot
    #plot fft of signal
    plt.xlim((0,50))
    plt.xlabel('cycles in window')
    plt.ylabel('original amplitude')
    plt.plot(cp.asnumpy(nwaves[mask]), cp.asnumpy(fft_theo[mask]))
    plt.show()

    #create a copy of the original fft to be used for filtering
    fft_new = cp.copy(fft_vals)
    #filter out y3 by setting corr. amplitude component(s) to zero  ##helping source # checked about single side spectrum
    fft_new[cp.abs(fft_new)<=a1/2] = 0.
    #plot fft of filtered signal

# plot fft noisy signal
    plt.xlim((0,50))
    plt.xlabel('cycles in window')
    plt.ylabel('filtered amplitude')
    # plt.plot(cp.asnumpy(nwaves[mask]), cp.asnumpy(2.0*cp.abs(fft_new[mask]/pts)))
    plt.plot(cp.asnumpy(nwaves[mask]),cp.asnumpy(fft_theo_ns[mask]))
    plt.show()

    #invert the filtered fft with numpy.fft.ifft
    filt_data = cp.real(ifft(fft_new))

    #plot filtered data and compare origin, noisy signal, denoised signal
    plt.plot(cp.asnumpy(x), cp.asnumpy(y), label='original signal')
    plt.plot(cp.asnumpy(x), cp.asnumpy(y_n), label='filtered signal')
    plt.plot(cp.asnumpy(x), cp.asnumpy(filt_data), label='denoised signal')
    plt.xlim((0,50))
    plt.legend()
    plt.show()

    # #multiply fft by 2*pi*sqrt(-1)*frequency to get fft of derivative
    # dy_fft = 1.0j*nwaves_2pi*fft_vals
    # #invert to reconstruct sampled values of derivative
    # dy_recon = cp.real(ifft(dy_fft))
    # #plot reconstructed derivative and compare with analuytical version
    # plt.plot(cp.asnumpy(x), cp.asnumpy(dy), label='exact derivative')
    # plt.plot(cp.asnumpy(x), cp.asnumpy(dy_recon), label='fft derivative')
    # plt.xlim((0,50))
    # plt.legend()
    # plt.show()

def cupy_eig(mat):
    """
    Returns the eigen values of square matrix mat
    """
    # https://docs-cupy.chainer.org/en/stable/reference/generated/cupy.linalg.eigh.html
    w, v = cp.linalg.eigh(mat)
    # w eigenvalues
    # v eigenvectors
    return w, v

def cupy_J(n):
    """
    Constructs the nxn matrix J (as described in q2)
    Returns J, leading eigenvalue of J, and the associated eigenvector
    """
    # main diagonal where each of the entries is 1/2
    # Use cupyx.scipy.spare.diags()
    # http://wwwens.aero.jussieu.fr/lefrere/master/SPE/docs-python/scipy-doc/generated/scipy.sparse.diags.html
    # https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.diags.html
    # 𝑛×𝑛 matrix 𝐽(𝑛) full of zeros except for the diagonals adjacent to the main diagonal where each of the entries is  1/2
    J = scipy.sparse.diags([1/2, 1/2], [1,-1], shape=(n,n)).toarray()
    w, v = cupy_eig(J)

    # max leading eigval (one with large magnitude)
    idx = cp.argmax(abs(w))

    return J, abs(w[idx]), v[idx]

def rand_mat_gauss(n):
    """
    Returns nxn size array of random numbers sampled from a normal distribution with a mean of 0 and standard deviation of 1
    (Uses cupy to generate the random matrix)
    """
    # 3a)
    #B 𝑁(0,1) , the normal distribution with mean zero and standard deviation 1.
    # cupy.random.normal #Returns an array of normally distributed samples.
    B = cp.random.normal(0, 1, (n,n))
    A = (1/cp.sqrt(2)) * (B + cp.transpose(B))
    return A

def rand_mat_plusminus(n):
    """
    Returns nxn size array of random numbers uniformly distributed on the 2-value set {-1,1}
    OPTIONAL
    """
    # p4a)
    # repeat problem 3 but with matrix entries chosen uniformly from  {−1,1}
    # instead of from a normal distribution
    # cupy.random.rand #Returns an array of uniform random values over the in- terval[0, 1).
    # cupy.random.randint #Returns a scalar or an array of integer values over [low, high).
    # cupy.random.random_integers #Returnascalaroranarrayofintegervaluesover[low, high]

    B = cp.random.random_integers(-1, 1, (n,n))
    A = (1/2) * (B + cp.transpose(B))
    return A

def rand_mat_uniform(n):
    """
    Returns nxn size array of random numbers uniformly distrubted on the range [-1,1]
    OPTIONAL
    """
    # p4b)
    # Repeat 4a but with matrix entries chosen randomly from a uniform distribution on the interval  [−1,1]
    # produces an  𝑛×𝑛  array that is symmetric and any individual element is chosen randomly from  [−1,1] .
    B = cp.random.uniform(-1, 1, (n,n))
    A = (1/2) * (B + cp.transpose(B))
    return A

# https://stackoverflow.com/questions/42908334/checking-if-a-matrix-is-symmetric-in-numpy
def check_symmetric(a, rtol=1e-05, atol=1e-08):
    return np.allclose(a, a.T, rtol=rtol, atol=atol)

if __name__ == "__main__":

    #Fill in code here to call functions
    # pass

# Problem1)
    # 1b)
    cupy_fft()

    # 1c)
    cupy_filter()

#
#
#

# Problem2)
    # 2) For n = 50
    n = 50
    J, w, v = cupy_J(n)
    print('For n = {0}, The eigenvalue of leading eigenvalue: {1}'.format(n, w))
    print('For n = {0}, The eigenvector that associate with the leading eigenvalue: \n{1}'.format(n, v))
    # For n = 500
    n = 500
    J, w, v = cupy_J(n)
    print('For n = {0}, The eigenvalue of leading eigenvalue: {1}'.format(n, w))
    print('For n = {0}, The eigenvector that associate with the leading eigenvalue: \n{1}'.format(n, v))

    # What is the value of the leading eigenvalue and how does it behave as  𝑛  becomes large?
    print('As n increases, the leading eigenvalue also increases')
    print('The magnitude of eigenvector matrix are shows symmetric')

#
#
#

# problem 3)
    # 3b)
    # 10x10 array
    # Verify symmetric matrix
    # plot histogram N
    n = 10

    Anxn = rand_mat_gauss(n)
    # verify symmetric matrix
    check = check_symmetric(Anxn)
    print('This is {} that the matrix is symmetric'.format(check))

    # plot histogram N
    mu, sig = cp.mean(Anxn), cp.std(Anxn)
    print('For n = {0}, mu = {1}, sig = {2}'.format(n, mu, sig))

# # https://www.tutorialspoint.com/python_data_science/python_normal_distribution.htm

    mu = cp.asnumpy(mu)
    sigma = cp.asnumpy(sig)
    s = cp.squeeze(Anxn)
    s = cp.asnumpy(s)

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

    # 3c)
    # Create a matrix m = rand_mat_gauss(n) with  𝑛=1000
    n = 1000
    m = rand_mat_gauss(n)

    # cupy to compute the eigenvalues of the matrix
    w, v = cupy_eig(m)
    # print('For n = {0}, in rand_mat_gauss: \neigval = \n{1}'.format(n, w))

    # plot the histrogram of the eigenvalues
    mu, sig = cp.mean(m), cp.std(m)
    mu = cp.asnumpy(mu)
    sigma = cp.asnumpy(sig)

    plt.figure()
    count, bins, ignored = plt.hist(cp.asnumpy(w), bins = 30)
    # plt.plot(bins, 1 / (sigma * np.sqrt(2 * np.pi)) * np.exp(- (bins - mu) ** 2 / (2 * sigma ** 2)), linewidth=3, color='y')
    plt.title('3c) n = ' + str(n))
    plt.show()

    print('For n = {0}, mu = {1}, sig = {2}'.format(n, mu, sig))
    print('The normal distribution of the eigenvalues smoothly follows in histogram\n\n')
    print('For n = {0}, in rand_mat_gauss: \neigval = \n{1}'.format(n, w))

    # 3d)
    # repeat 3c with  𝑛=2000  and  𝑛=4000
    # For n = 2000
    n = 2000 #200
    m = rand_mat_gauss(n)

    # cupy to compute the eigenvalues of the matrix
    w, v = cupy_eig(m)
    # print('For n = {0}, in rand_mat_gauss: \neigval = \n{1}'.format(n, w))

    # plot the histrogram of the eigenvalues
    mu, sig = cp.mean(m), cp.std(m)
    mu = cp.asnumpy(mu)
    sigma = cp.asnumpy(sig)

    plt.figure()
    count, bins, ignored = plt.hist(cp.asnumpy(w), bins = 30)
    # plt.plot(bins, 1 / (sigma * np.sqrt(2 * np.pi)) * np.exp(- (bins - mu) ** 2 / (2 * sigma ** 2)), linewidth=3, color='y')
    plt.title('3d) n = ' + str(n))
    plt.show()

    print('For n = {0}, mu = {1}, sig = {2}'.format(n, mu, sig))
    print('The normal distribution of the eigenvalues smoothly follows in histogram')
    print('For n = {0}, in rand_mat_gauss: \neigval = \n{1}'.format(n, w))

    # For n = 4000
    n = 4000
    m = rand_mat_gauss(n)

    # cupy to compute the eigenvalues of the matrix
    w, v = cupy_eig(m)
    # print('For n = {0}, in rand_mat_gauss: \neigval = \n{1}'.format(n, w))

    # plot the histrogram of the eigenvalues
    mu, sig = cp.mean(m), cp.std(m)
    mu = cp.asnumpy(mu)
    sigma = cp.asnumpy(sig)

    plt.figure()
    count, bins, ignored = plt.hist(cp.asnumpy(w), bins = 30)
    # plt.plot(bins, 1 / (sigma * np.sqrt(2 * np.pi)) * np.exp(- (bins - mu) ** 2 / (2 * sigma ** 2)), linewidth=3, color='y')
    plt.title('3d) n = ' + str(n))
    plt.show()

    print('For n = {0}, mu = {1}, sig = {2}'.format(n, mu, sig))
    print('The normal distribution of the eigenvalues smoothly follows in histogram')
    print('For n = {0}, in rand_mat_gauss: \neigval = \n{1}'.format(n, w))

# What features are independent of  𝑛 ?
    print('As shown in graph when n is increases, they are proportionally increases. \nThe distribution of the eigenvalues are independent of n. \nLeading value of the eigvalues are proprotional to n')

#
#
#

# problem4)
    # p4a)
    # epeat problem 3 but with matrix entries chosen uniformly from  {−1,1}
    # instead of from a normal distribution
    # a)
    # For n = 1000, 2000, 4000
# For n = 1000
    n = 1000
    m = rand_mat_plusminus(n)
    # cupy to compute the eigenvalues of the matrix
    w, v = cupy_eig(m)
    # print('For n = {0}, in rand_mat_gauss: \neigval = \n{1}'.format(n, w))
    # For this problem, exclude the largest eigenvalue before computing the histogram.
    # max leading eigval (one with large magnitude)
    idx = cp.argmax(abs(w))
    w = np.delete(cp.asnumpy(w), cp.asnumpy(idx))

    # plot the histrogram of the eigenvalues
    mu, sig = cp.mean(m), cp.std(m)
    mu = cp.asnumpy(mu)
    sigma = cp.asnumpy(sig)

    plt.figure()
    count, bins, ignored = plt.hist(cp.asnumpy(w), bins = 30)
    # plt.plot(bins, 1 / (sigma * np.sqrt(2 * np.pi)) * np.exp(- (bins - mu) ** 2 / (2 * sigma ** 2)), linewidth=3, color='y')
    plt.title('4a) n = ' + str(n))
    plt.show()

    print('For n = {0}, mu = {1}, sig = {2}'.format(n, mu, sig))
    print('For n = {0}, in rand_mat_gauss: \neigval = \n{1}'.format(n, w))

# For n = 2000
    n = 2000
    m = rand_mat_plusminus(n)
    # cupy to compute the eigenvalues of the matrix
    w, v = cupy_eig(m)
    # print('For n = {0}, in rand_mat_gauss: \neigval = \n{1}'.format(n, w))
    # For this problem, exclude the largest eigenvalue before computing the histogram.
    # max leading eigval (one with large magnitude)
    idx = cp.argmax(abs(w))
    w = np.delete(cp.asnumpy(w), cp.asnumpy(idx))

    # plot the histrogram of the eigenvalues
    mu, sig = cp.mean(m), cp.std(m)
    mu = cp.asnumpy(mu)
    sigma = cp.asnumpy(sig)

    plt.figure()
    count, bins, ignored = plt.hist(cp.asnumpy(w), bins = 30)
    # plt.plot(bins, 1 / (sigma * np.sqrt(2 * np.pi)) * np.exp(- (bins - mu) ** 2 / (2 * sigma ** 2)), linewidth=3, color='y')
    plt.title('4a) n = ' + str(n))
    plt.show()

    print('For n = {0}, mu = {1}, sig = {2}'.format(n, mu, sig))
    print('For n = {0}, in rand_mat_gauss: \neigval = \n{1}'.format(n, w))

# For n = 4000
    n = 4000
    m = rand_mat_plusminus(n)
    # cupy to compute the eigenvalues of the matrix
    w, v = cupy_eig(m)
    # print('For n = {0}, in rand_mat_gauss: \neigval = \n{1}'.format(n, w))
    # For this problem, exclude the largest eigenvalue before computing the histogram.
    # max leading eigval (one with large magnitude)
    idx = cp.argmax(abs(w))
    w = np.delete(cp.asnumpy(w), cp.asnumpy(idx))

    # plot the histrogram of the eigenvalues
    mu, sig = cp.mean(m), cp.std(m)
    mu = cp.asnumpy(mu)
    sigma = cp.asnumpy(sig)

    plt.figure()
    count, bins, ignored = plt.hist(cp.asnumpy(w), bins = 30)
    # plt.plot(bins, 1 / (sigma * np.sqrt(2 * np.pi)) * np.exp(- (bins - mu) ** 2 / (2 * sigma ** 2)), linewidth=3, color='y')
    plt.title('4a) n = ' + str(n))
    plt.show()

    print('For n = {0}, mu = {1}, sig = {2}'.format(n, mu, sig))
    print('For n = {0}, in rand_mat_gauss: \neigval = \n{1}'.format(n, w))

# When the distribution from which elements are chosen is changed, what is preserved and what changes?
    print('As shown in histgram, when n increases, histogram also increases.\n As n is increases the the distribution doesnt changing much.')

#
#
#
    # p4b)
    # b)
# Again explore the distribution of the eigenvalues and the dependence on  𝑛 . When the distribution is changed this time, what is preserved and what changes?
    # For n = 1000, 2000, 4000
# For n = 1000
    n = 1000
    m = rand_mat_uniform(n)
    # cupy to compute the eigenvalues of the matrix
    w, v = cupy_eig(m)
    # print('For n = {0}, in rand_mat_gauss: \neigval = \n{1}'.format(n, w))
    # For this problem, exclude the largest eigenvalue before computing the histogram.
    # max leading eigval (one with large magnitude)
    idx = cp.argmax(abs(w))
    w = np.delete(cp.asnumpy(w), cp.asnumpy(idx))

    # plot the histrogram of the eigenvalues
    mu, sig = cp.mean(m), cp.std(m)
    mu = cp.asnumpy(mu)
    sigma = cp.asnumpy(sig)

    plt.figure()
    count, bins, ignored = plt.hist(cp.asnumpy(w), bins = 30)
    # plt.plot(bins, 1 / (sigma * np.sqrt(2 * np.pi)) * np.exp(- (bins - mu) ** 2 / (2 * sigma ** 2)), linewidth=3, color='y')
    plt.title('4b) n = ' + str(n))
    plt.show()

    print('For n = {0}, mu = {1}, sig = {2}'.format(n, mu, sig))
    print('For n = {0}, in rand_mat_gauss: \neigval = \n{1}'.format(n, w))

# For n = 2000
    n = 2000
    m = rand_mat_uniform(n)
    # cupy to compute the eigenvalues of the matrix
    w, v = cupy_eig(m)
    # print('For n = {0}, in rand_mat_gauss: \neigval = \n{1}'.format(n, w))
    # For this problem, exclude the largest eigenvalue before computing the histogram.
    # max leading eigval (one with large magnitude)
    idx = cp.argmax(abs(w))
    w = np.delete(cp.asnumpy(w), cp.asnumpy(idx))

    # plot the histrogram of the eigenvalues
    mu, sig = cp.mean(m), cp.std(m)
    mu = cp.asnumpy(mu)
    sigma = cp.asnumpy(sig)

    plt.figure()
    count, bins, ignored = plt.hist(cp.asnumpy(w), bins = 30)
    # plt.plot(bins, 1 / (sigma * np.sqrt(2 * np.pi)) * np.exp(- (bins - mu) ** 2 / (2 * sigma ** 2)), linewidth=3, color='y')
    plt.title('4b) n = ' + str(n))
    plt.show()

    print('For n = {0}, mu = {1}, sig = {2}'.format(n, mu, sig))
    print('For n = {0}, in rand_mat_gauss: \neigval = \n{1}'.format(n, w))

# For n = 4000
    n = 4000
    m = rand_mat_uniform(n)
    # cupy to compute the eigenvalues of the matrix
    w, v = cupy_eig(m)
    # print('For n = {0}, in rand_mat_gauss: \neigval = \n{1}'.format(n, w))
    # For this problem, exclude the largest eigenvalue before computing the histogram.
    # max leading eigval (one with large magnitude)
    idx = cp.argmax(abs(w))
    w = np.delete(cp.asnumpy(w), cp.asnumpy(idx))

    # plot the histrogram of the eigenvalues
    mu, sig = cp.mean(m), cp.std(m)
    mu = cp.asnumpy(mu)
    sigma = cp.asnumpy(sig)

    plt.figure()
    count, bins, ignored = plt.hist(cp.asnumpy(w), bins = 30)
    # plt.plot(bins, 1 / (sigma * np.sqrt(2 * np.pi)) * np.exp(- (bins - mu) ** 2 / (2 * sigma ** 2)), linewidth=3, color='y')
    plt.title('4b) n = ' + str(n))
    plt.show()

    print('For n = {0}, mu = {1}, sig = {2}'.format(n, mu, sig))
    print('For n = {0}, in rand_mat_gauss: \neigval = \n{1}'.format(n, w))

# Again explore the distribution of the eigenvalues and the dependence on  𝑛 . When the distribution is changed this time, what is preserved and what changes?
    print('As shown in the graph we can see now that it does not change as n increases. It shows that distribution are most likely set')

import numpy,import scipy,import cupy,from cupy


# **************************************************************************** #
#                                                                              #
#                                                         :::      ::::::::    #
#    trainer.py                                         :+:      :+:    :+:    #
#                                                     +:+ +:+         +:+      #
#    By: alngo <alngo@student.42.fr>                +#+  +:+       +#+         #
#                                                 +#+#+#+#+#+   +#+            #
#    Created: 2020/02/24 09:43:48 by alngo             #+#    #+#              #
#    Updated: 2020/03/02 10:19:17 by alngo            ###   ########.fr        #
#                                                                              #
# **************************************************************************** #

import numpy as np
import sys
from .utils.csv import read_csv, write_csv, check_csv
from .utils.plot import plot_gradient_descent
from .utils.maths import mean_squared_error, normalize, denormalize_theta
import matplotlib.pyplot as plt

class Trainer:
    def __init__(self, data_path=None, out_path=None, epochs=1000, lrate=0.5,
                 plot=False):
        """Trainer Model"""
        self.data_path = data_path
        self.out = out_path
        self.epochs = epochs
        self.lrate = lrate
        if lrate < 0 or lrate > 1:
            print(f"Invalid learning rate: expect a value between 0 and 1")
            sys.exit(1)
        self.plot = plot
        self.data = read_csv(self.data_path)

    @check_csv('km', 'price')
    def gradient_descent(self):
        p0 = np.float32(0.0)
        p1 = np.float32(0.0)
        cost = []
        p0_array = []
        p1_array = []

        [X, Xmin, Xmax] = normalize(
            self.data.iloc[:, 0].to_numpy(dtype=np.float32))
        [y, ymin, ymax] = normalize(
            self.data.iloc[:, 1].to_numpy(dtype=np.float32))

        m = np.float32(len(X))
        d = np.float32(1.0 / m)
        lrate = np.float32(self.lrate)

        for i in range(self.epochs):
            hyp = p0 + (X * p1)
            tmp0 = p0 - lrate * (d * sum(hyp - y))
            tmp1 = p1 - lrate * (d * sum((hyp - y) * X))
            p0 = tmp0
            p1 = tmp1
            p0_array.append(p0)
            p1_array.append(p1)
            cost.append(mean_squared_error(p0, p1, X, y))
            print(f"theta0: {p0} - theta1: {p1}")

        print(f"Mean squared error: {mean_squared_error(p0, p1, X, y)}")

        p0, p1 = denormalize_theta(self.data, X, p0, p1, ymin, ymax)

        data = {'theta0': p0, 'theta1': p1}

        if (self.out):
            write_csv(data=data, output=self.out)

        if (self.plot):
            plot_gradient_descent(X, y, p0_array, p1_array, cost)

        return data


# --------------
# Code starts here

import numpy as np

# Code starts here

# Adjacency matrix
adj_mat = np.array([[0,0,0,0,0,0,1/3,0],
                   [1/2,0,1/2,1/3,0,0,0,0],
                   [1/2,0,0,0,0,0,0,0],
                   [0,1,0,0,0,0,0,0],
                  [0,0,1/2,1/3,0,0,1/3,0],
                   [0,0,0,1/3,1/3,0,0,1/2],
                   [0,0,0,0,1/3,0,0,1/2],
                   [0,0,0,0,1/3,1,1/3,0]])

# Compute eigenvalues and eigencevectrs
eigenvalues, eigenvectors = np.linalg.eig(adj_mat)

# Eigen vector corresponding to 1
eigen_1 = abs(eigenvectors[:,0])/np.linalg.norm(eigenvectors[:,0], 1)

# most important page
#page = np.where(eigen_1 == eigen_1.max())
page = 8
print(eigen_1, page, sep = "\n")

# Code ends here

# --------------
# Code starts here

# Initialize stationary vector I
print(page)
adj2 = np.array([[  0,      0,      0,      0,      0,      0,      0,      0],
                    [0.5,   0,      0.5,    1/3,    0,      0,      0,      0],
                    [0.5,   0,      0,      0,      0,      0,      0,      0],
                    [0,     1,      0,      0,      0,      0,      0,      0],
                    [0,     0,      0.5,    1/3,    0,      0,      0.5,    0],
                    [0,     0,      0,      1/3,    1/3,    0,      0,      0.5],
                    [0,     0,      0,      0,      1/3,    0,      0,      0.5],
                    [0,     0,      0,      0,      1/3,    1,      0.5,    0]])
# Perform iterations for power method
print(adj_mat)
#init_I = np.zeros((8,11))
init_I=np.array([1,0,0,0,0,0,0,0])
#init_I[0,0] = 1
#print(init_I)
#print(init_I[:,1])

for i in range(10):
    #init_I[:,1] = np.dot(adj_mat, init_I[:,0])
    init_temp = np.dot(adj_mat, init_I)
    init_I = init_temp/np.linalg.norm(init_I,1)
    #init_I[:,i+1] = init_temp
    #init_I[:,i+1] = np.linalg.norm(init_I,1)

print(init_I)
power_page=8

# Code ends here

# --------------
# Code starts here

# New Adjancency matrix
# New Adjancency matrix
new_adj_mat = np.array([[0,0,0,0,0,0,0,0],
                   [1/2,0,1/2,1/3,0,0,0,0],
                  [1/2,0,0,0,0,0,0,0],
                   [0,1,0,0,0,0,0,0],
                   [0,0,1/2,1/3,0,0,1/2,0],
                   [0,0,0,1/3,1/3,0,0,1/2],
                   [0,0,0,0,1/3,0,0,1/2],
                   [0,0,0,0,1/3,1,1/2,0]])

# Initialize stationary vector I
new_init_I = np.array([1,0,0,0,0,0,0,0])

# Perform iterations for power method
for i in range(10):
    new_init_I = np.dot(new_adj_mat, new_init_I)/np.linalg.norm(new_init_I, 1)

print(new_init_I)

# Code ends here

# --------------
# Alpha value
alpha = 0.85

# Code starts here
n = len(new_adj_mat)
one = np.ones_like(new_adj_mat)
# Modified adjancency matrix
G = alpha*new_adj_mat + (one - alpha)*1/n

# Initialize stationary vector I
final_init_I = np.array([1,0,0,0,0,0,0,0])

# Perform iterations for power method
for i in range(1000):
    final_init_I = np.dot(G,final_init_I)/np.linalg.norm(final_init_I,1)

print(final_init_I)
# Code ends here


import numpy as np
from distributionLib import Dist

pois = Dist.poissonDist
xBar = Dist.expt
var = Dist.var
coolPrint = Dist.coolPrint
#test-1
'''
https://en.wikipedia.org/wiki/Poisson_distribution
Example
Ugarte and colleagues report that the average number of goals in a World Cup
soccer match is approximately 2.5 and the Poisson model is appropriate.
Because the average event rate is 2.5 goals per match, λ = 2.5.
'''
print('Test-1')
Lambda=2.5
k = 10 #(number of goals in match)
P = pois(Lambda,k,plot=True)
coolPrint(P)
print('-'*80)

# test-2
'''
https://www.intmath.com/counting-probability/13-poisson-probability-distribution.php
If electricity power failures occur according to a Poisson distribution with an
average of display 3 failures every twenty weeks, calculate the probability that
there will not be more than one failure during a particular week.
'''
Lambda = 3./20
k = 10
P = pois(Lambda,k,plot=True)
coolPrint(P)
#more than one failure
print('Number of Failure = {}'.format(np.sum(P[:2])))
print('-'*80)


# Check README.md for instruction to run

import numpy as np
import argparse
import sys

#####################################################
# Function to calculate the machine precision/epsilon
# start_value - value from where we will start the iteration,
# i.e., we will keep on halving the start_value until 1+start_value == 1
# data_type - data type for which we need to find the machine precision.
#####################################################
def get_machine_precision(start_value = 1, data_type = np.float16):

	num_iterations = 0
	machine_precision = data_type(start_value)
	while True:
		num_iterations += 1
		if data_type(1+machine_precision/2) != data_type(1):
			machine_precision = data_type(machine_precision/2)
		else:
			break

	return machine_precision, num_iterations

###############################################
# I have created a parser to accept command line argument
# This code can accept two command line arguments -
# 1. precision: can be single (32 bit float) or double (64 bit float)
# 2. start_value: starting value of the algorithm used to find the machine precision

# If no command line argument is passed, default value of precision is single (32 bit float)
# and defalut value of start_value is 1.
################################################
if __name__ == '__main__':

	parser = argparse.ArgumentParser(description='Data Type and Starting Value is expected')

	parser.add_argument("-p", "--precision", type = str, default = "single", help = "Data Type")
	parser.add_argument("-s", "--start_value", type = float, default = 1, help = "Starting Value of the iteration to find machine precision")

	args = parser.parse_args()
	if args.precision != "single" and args.precision != "double":
		print("Invalid Precision")
		sys.exit()

	# Dictionary to map precision to the corresponding data type.
	data_type_dictionary = {
	"single" : np.float32,
	"double" : np.float64
	}

	machine_precision, num_iterations = get_machine_precision(args.start_value, data_type_dictionary[args.precision])

	print("Data Type : ", data_type_dictionary[args.precision])
	print("Machine Precision : ", machine_precision)
	print("Number of iterations required to get to the precision from start value ({}) = {}".format(args.start_value, num_iterations))


import numpy as np

def theta2rotx(theta: np.ndarray) -> np.ndarray:
    """
    Rx = [[1, 0, 0],
          [0, c(t), -s(t)],
          [0, s(t), c(t)]]
    :param theta: angle(s) in degrees, positive is counterclockwise
    :return: rotation_matrices
    """
    theta = np.deg2rad(np.asarray(theta).reshape(-1))
    rotation_matrices = np.zeros((theta.shape[0], 3, 3), dtype=float)
    cos_theta = np.cos(theta)
    sin_theta = np.sin(theta)
    rotation_matrices[:, 0, 0] = 1
    rotation_matrices[:, (1, 2), (1, 2)] = cos_theta[:, np.newaxis]
    rotation_matrices[:, 1, 2] = -sin_theta
    rotation_matrices[:, 2, 1] = sin_theta
    return rotation_matrices

def theta2roty(theta: np.ndarray) -> np.ndarray:
    """
    Ry = [[c(t), 0, s(t)],
          [0, 1, 0],
          [-s(t), 0, c(t)]]
    :param theta: angle(s) in degrees, positive is counterclockwise
    :return: rotation_matrices
    """
    theta = np.deg2rad(np.asarray(theta).reshape(-1))
    rotation_matrices = np.zeros((theta.shape[0], 3, 3), dtype=float)
    cos_theta = np.cos(theta)
    sin_theta = np.sin(theta)
    rotation_matrices[:, 1, 1] = 1
    rotation_matrices[:, (0, 2), (0, 2)] = cos_theta[:, np.newaxis]
    rotation_matrices[:, 0, 2] = sin_theta
    rotation_matrices[:, 2, 0] = -sin_theta
    return rotation_matrices

def theta2rotz(theta: np.ndarray) -> np.ndarray:
    """
    Rz = [[c(t), -s(t), 0],
          [s(t), c(t), 0],
          [0, 0, 1]]
    :param theta: angle(s) in degrees, positive is counterclockwise
    :return: rotation_matrices
    """
    theta = np.deg2rad(np.asarray(theta).reshape(-1))
    rotation_matrices = np.zeros((theta.shape[0], 3, 3), dtype=float)
    cos_theta = np.cos(theta)
    sin_theta = np.sin(theta)
    rotation_matrices[:, 2, 2] = 1
    rotation_matrices[:, (0, 1), (0, 1)] = cos_theta[:, np.newaxis]
    rotation_matrices[:, 0, 1] = -sin_theta
    rotation_matrices[:, 1, 0] = sin_theta
    return rotation_matrices

def theta2rotm(theta: np.ndarray, axis: str):
    """
    Convert values for theta into rotation matrices around a given axis 'x', 'y' or 'z'
    :param theta: angle(s) in degrees, positive is counterclockwise
    :param axis: 'x', 'y' or 'z'
    :return: rotation_matrices
    """
    axis = axis.strip().lower()
    if axis not in ('x', 'y', 'z'):
        raise ValueError(f"Axis must be one of 'x', 'y' or 'z''")
    elif axis == 'x':
        rotation_matrices = theta2rotx(theta)
    elif axis == 'y':
        rotation_matrices = theta2roty(theta)
    elif axis == 'z':
        rotation_matrices = theta2rotz(theta)
    if rotation_matrices.shape[0] == 1:
        rotation_matrices = rotation_matrices.reshape((3, 3))
    return rotation_matrices


from numpy import *
from math import sqrt

# Input: expects Nx3 matrix of points
# Returns R,t
# R = 3x3 rotation matrix
# t = 3x1 column vector

def rigid_transform_3D(A, B):
    assert len(A) == len(B)

    N = A.shape[0];  # total points

    centroid_A = mean(A, axis=0)
    centroid_B = mean(B, axis=0)

    # centre the points
    AA = A - tile(centroid_A, (N, 1))
    BB = B - tile(centroid_B, (N, 1))

    # dot is matrix multiplication for array
    H = transpose(AA) * BB

    U, S, Vt = linalg.svd(H)

    R = Vt.T * U.T

    # special reflection case
    if linalg.det(R) < 0:
        print "Reflection detected"
        Vt[2, :] *= -1
        R = Vt.T * U.T

    t = -R * centroid_A.T + centroid_B.T

    print t

    return R, t

# Test with random data

# Random rotation and translation
R = mat(random.rand(3, 3))
t = mat(random.rand(3, 1))

# make R a proper rotation matrix, force orthonormal
U, S, Vt = linalg.svd(R)
R = U * Vt

# remove reflection
if linalg.det(R) < 0:
    Vt[2, :] *= -1
    R = U * Vt

# number of points
n = 10

A = mat(random.rand(n, 3));
B = R * A.T + tile(t, (1, n))
B = B.T;

# recover the transformation
ret_R, ret_t = rigid_transform_3D(A, B)

A2 = (ret_R * A.T) + tile(ret_t, (1, n))
A2 = A2.T

# Find the error
err = A2 - B

err = multiply(err, err)
err = sum(err)
rmse = sqrt(err / n);

print "Points A"
print A
print ""

print "Points B"
print B
print ""

print "Rotation"
print R
print ""

print "Translation"
print t
print ""

print "RMSE:", rmse
print "If RMSE is near zero, the function is correct!"


#!/usr/bin/env python
# coding: utf-8

# # Attention Basics
# In this notebook, we look at how attention is implemented. We will focus on implementing attention in isolation from a larger model. That's because when implementing attention in a real-world model, a lot of the focus goes into piping the data and juggling the various vectors rather than the concepts of attention themselves.
#
# We will implement attention scoring as well as calculating an attention context vector.
#
# ## Attention Scoring
# ### Inputs to the scoring function
# Let's start by looking at the inputs we'll give to the scoring function. We will assume we're in the first step in the decoging phase. The first input to the scoring function is the hidden state of decoder (assuming a toy RNN with three hidden nodes -- not usable in real life, but easier to illustrate):

# In[1]:

dec_hidden_state = [5,1,20]

# Let's visualize this vector:

# In[2]:

get_ipython().run_line_magic('matplotlib', 'inline')
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# Let's visualize our decoder hidden state
plt.figure(figsize=(1.5, 4.5))
sns.heatmap(np.transpose(np.matrix(dec_hidden_state)), annot=True, cmap=sns.light_palette("purple", as_cmap=True), linewidths=1)

# Our first scoring function will score a single annotation (encoder hidden state), which looks like this:

# In[3]:

annotation = [3,12,45] #e.g. Encoder hidden state

# In[4]:

# Let's visualize the single annotation
plt.figure(figsize=(1.5, 4.5))
sns.heatmap(np.transpose(np.matrix(annotation)), annot=True, cmap=sns.light_palette("orange", as_cmap=True), linewidths=1)

# ### IMPLEMENT: Scoring a Single Annotation
# Let's calculate the dot product of a single annotation. Numpy's [dot()](https://docs.scipy.org/doc/numpy/reference/generated/numpy.dot.html) is a good candidate for this operation

# In[12]:

def single_dot_attention_score(dec_hidden_state, enc_hidden_state):

    # TODO: return the dot product of the two vectors
    return np.dot(dec_hidden_state, enc_hidden_state)

single_dot_attention_score(dec_hidden_state, annotation)

#
# ### Annotations Matrix
# Let's now look at scoring all the annotations at once. To do that, here's our annotation matrix:

# In[6]:

annotations = np.transpose([[3,12,45], [59,2,5], [1,43,5], [4,3,45.3]])

# And it can be visualized like this (each column is a hidden state of an encoder time step):

# In[7]:

# Let's visualize our annotation (each column is an annotation)
ax = sns.heatmap(annotations, annot=True, cmap=sns.light_palette("orange", as_cmap=True), linewidths=1)

# ### IMPLEMENT: Scoring All Annotations at Once
# Let's calculate the scores of all the annotations in one step using matrix multiplication. Let's continue to us the dot scoring method
#
#
#
# To do that, we'll have to transpose `dec_hidden_state` and [matrix multiply](https://docs.scipy.org/doc/numpy/reference/generated/numpy.matmul.html) it with `annotations`.

# In[23]:

def dot_attention_score(dec_hidden_state, annotations):
    # TODO: return the product of dec_hidden_state transpose and enc_hidden_states

    return np.matmul(np.transpose(dec_hidden_state), annotations)

attention_weights_raw = dot_attention_score(dec_hidden_state, annotations)
attention_weights_raw

# Looking at these scores, can you guess which of the four vectors will get the most attention from the decoder at this time step?
#
# ## Softmax
# Now that we have our scores, let's apply softmax:
#

# In[24]:

def softmax(x):
    x = np.array(x, dtype=np.float128)
    e_x = np.exp(x)
    return e_x / e_x.sum(axis=0)

attention_weights = softmax(attention_weights_raw)
attention_weights

# Even when knowing which annotation will get the most focus, it's interesting to see how drastic softmax makes the end score become. The first and last annotation had the respective scores of 927 and 929. But after softmax, the attention they'll get is 0.12 and 0.88 respectively.
#
# # Applying the scores back on the annotations
# Now that we have our scores, let's multiply each annotation by its score to proceed closer to the attention context vector. This is the multiplication part of this formula (we'll tackle the summation part in the latter cells)
#
#

# In[25]:

def apply_attention_scores(attention_weights, annotations):
    # TODO: Multiple the annotations by their weights
    return attention_weights * annotations

applied_attention = apply_attention_scores(attention_weights, annotations)
applied_attention

# Let's visualize how the context vector looks now that we've applied the attention scores back on it:

# In[26]:

# Let's visualize our annotations after applying attention to them
ax = sns.heatmap(applied_attention, annot=True, cmap=sns.light_palette("orange", as_cmap=True), linewidths=1)

# Contrast this with the raw annotations visualized earlier in the notebook, and we can see that the second and third annotations (columns) have been nearly wiped out. The first annotation maintains some of its value, and the fourth annotation is the most pronounced.
#
# # Calculating the Attention Context Vector
# All that remains to produce our attention context vector now is to sum up the four columns to produce a single attention context vector
#

# In[27]:

def calculate_attention_vector(applied_attention):
    return np.sum(applied_attention, axis=1)

attention_vector = calculate_attention_vector(applied_attention)
attention_vector

# In[28]:

# Let's visualize the attention context vector
plt.figure(figsize=(1.5, 4.5))
sns.heatmap(np.transpose(np.matrix(attention_vector)), annot=True, cmap=sns.light_palette("Blue", as_cmap=True), linewidths=1)

# Now that we have the context vector, we can concatinate it with the hidden state and pass it through a hidden layer to produce the the result of this decoding time step.


# Uses python3

def evalt(a, b, op):
    if op == '+':
        return a + b
    elif op == '-':
        return a - b
    elif op == '*':
        return a * b
    else:
        assert False

def MinMax(i, j, op, m, M):
    import numpy as np
    m_min = np.inf
    m_max = -np.inf

    for k in range(i, j):
        a = evalt(M[i][k], M[k+1][j], op[k])
        b = evalt(M[i][k], m[k+1][j], op[k])
        c = evalt(m[i][k], M[k+1][j], op[k])
        d = evalt(m[i][k], m[k+1][j], op[k])

        m_min = min(m_min, a, b, c, d)
        m_max = max(m_max, a, b, c, d)

    return(m_min, m_max)

def get_maximum_value(dataset):
    op = dataset[1 : len(dataset) : 2]
    digits = dataset[0 : len(dataset)+1 : 2]

    n = len(digits)
    m = [[0]*n for _ in range(n)]
    M = [[0]*n for _ in range(n)]

    for i in range(n):
        m[i][i] = int(digits[i])
        M[i][i] = int(digits[i])

    for s in range(1, n):
        for i in range(n-s):
            j = i + s
            m[i][j], M[i][j] = MinMax(i, j, op, m, M)

    return M[0][n-1]

if __name__ == "__main__":
    print(get_maximum_value(input()))


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

info_list = []

def PowerMethod(A, norm_indicator=True, converge_range=0.0001):

    r, c = A.shape

    if r != c:
        raise Exception("not a square matrix")

    #initialize eigenvectors
    v_list = []
    lambda_list = []
    diff_list = []

    idx = 0
    v_init = np.zeros(r)
    v_init[-1] = 1
    v_list.append(v_init)

    #initialize eigenvalues
    lambda_init = v_init.dot(A.dot(v_init))
    lambda_list.append(lambda_init)

    diff_init = float("inf")
    diff_list.append(diff_init)

    while diff_list[idx] > converge_range:
        #new vector
        v_new = A.dot(v_list[idx])

        if norm_indicator:
            v_new = v_new / np.linalg.norm(v_new)

        v_list.append(v_new)
        lambda_new = v_new.dot(A.dot(v_new))
        lambda_list.append(lambda_new)
        diff = np.abs(lambda_new - lambda_list[idx])
        diff_list.append(diff)
        idx = idx + 1

    print_log(idx, v_list, lambda_list, diff_list)

    #plot lambda_list
    x = [i for i in range(idx+1)]
    if len(x) > 20:
        x = x[:21]
        diff_list = diff_list[:21]

    plt.plot(x, diff_list)
    plt.savefig("difference_list_with" + str(norm_indicator) + "normalization")
    plt.show()
    return v_list[-1], lambda_list[-1]

def print_log(idx, v_list, lambda_list, diff_list):
    info_list = [[i,v_list[i],lambda_list[i],diff_list[i]] for i in range(idx)]
    print(tabulate(info_list, headers=["iteration","eigenvector", "eigenvalue","lambda_diff"]))


import matplotlib.pyplot as plt
import numpy as np
import scipy.optimize as spo

def error(line, data):
    '''
    Compute error between given model and observed data.

    Parameters:
    -----------
    line: tuple, list, array - C0 (slope) and C1 is Y-intercept
    data: 2D array
    '''
    x = data[:, 0]
    y = data[:, 1]
    C0 = line[0]
    C1 = line[1]
    error = np.sum((y - (C0 * x + C1)) ** 2)
    return error

def generate_line(C0, C1, sigma):
    '''
    Generate a line with noise.

    Parameters:
    -----------
    C0: slope
    C1: Y_intercept
    sigma: noise
    '''
    print ("Original Line: C0 = {}, C1 = {}".format(C0, C1))
    x_orig = np.linspace(0,10,21)
    y_orig = C0 * x_orig + C1
    plt.plot(x_orig,y_orig, 'b--', linewidth = 2.0, label = 'Original Line')

    ### Generate Noisy Points
    noise_sigma = sigma
    noise = np.random.normal(0, noise_sigma, y_orig.shape)
    data = np.asarray([x_orig,y_orig + noise]).T
    return data

def fit_line(data, error_func):
    ''' Fit a line to given data, using a supplied error function.
    Parameters
    ----------
    data: 2D array where each row is a point (X0,Y)
    error: function that computes the error between a line and observed data

    Returns a line that minimizes the error function
    '''

    # Intializing as a horizontal line with y = y.mean
    l_init = np.float32([0, np.mean(data[:, 1])])

    # Plotting the initial guess
    X_min = -1
    X_max = 5
    X_range = np.float32([X_min, X_max ])
    C0_init = l_init[0]
    C1_init = l_init[1]
    Y_init = C0_init * X_range + C1_init
    plt.plot(X_range, Y_init, 'm--', label = 'Initial guess')

    # Minimizing error function
    result = spo.minimize(error, l_init, args = (data,), method = 'SLSQP', options = {'disp': True})

    return result.x

def run():
    data = generate_line(1, 2, 2)
    fited_line = fit_line(data,error)
    C0_fited = fited_line[0]
    C1_fited = fited_line[1]
    X_data = data[:, 0]
    Y_data = data[:, 1]
    plt.plot(X_data, Y_data, 'go', label = 'Data Points')
    plt.plot(X_data,C0_fited * X_data + C1_fited, 'r--', label = 'fitted line')
    plt.legend(loc = 'upper left')
    plt.show()
    print ('Fitted Line: C0 = {}, C1 = {}'.format(C0_fited, C1_fited))

if __name__ == "__main__":
    run()


import sympy as sp
from sympy.parsing.sympy_parser import *

class num_method():
    def __init__(self, fx=0, itr_no=50, ees=0.00001, prec=0, err_relative=True, tab=11):
        self.fx = fx
        self.itr_no = itr_no

        if not prec:
            self.ees = ees
            self.generate_precision()
        else:
            self.prec = prec
            self.generate_epsilon()

        # flag to know if we want the absolute error or the relative error
        self.err_relative = err_relative
        # spaces in our table format
        self.tab = tab
        # to treat "x" as a variable
        self.x = sp.Symbol("x")

    def _components(self):
        print()
        print("fx: ", self.fx)
        print("itr_no: ", self.itr_no)
        print("tabs: ", self.tab)
        print("ees: ", self.ees)
        print("prec: ", self.prec)
        print()

    def generate_precision(self):
        import math
        self.prec = math.floor(2 - (math.log10((self.ees*100)*2)))

    def generate_epsilon(self):
        self.ees = (0.5*10**(2-self.prec))

    def convert_float(self, num):
        # used for rounding a number to n decimal places
        return float("{:.{prec}f}".format(num, prec=self.prec))

    def err_bound(self, alpha, xi):
        delta = alpha - xi
        dfa = sp.diff(self.fx)
        ddfa = sp.diff(dfa)

        ddfa = float(sp.N(ddfa.subs(self.x, alpha)))
        dfa = float(sp.N(dfa.subs(self.x, alpha)))

        delta_new = ((-ddfa)/(2*dfa)) * (delta**2)
        return abs(delta_new)

    def err_bound_bracket(self,xl,xu,n):
        return abs((xu-xl)/(2**n))

    def bisection(self, xl, xu):
        print()
        print()
        print("Bisection :")
        print("_______________________")
        print()

        # creating the table format for printing
        table_format = ["i", "xl", "xu", "xr", "f(xr)"]
        print("|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}".format(
            *table_format, tab=2*self.tab))

        fxl = float(sp.N(self.fx.subs(self.x, xl)))
        fxu = float(sp.N(self.fx.subs(self.x, xu)))

        if fxl*fxu > 0:
            print("No roots !!!")
            return -1, xl, False

        xr = float((xu + xl) / 2)
        fxr = float(sp.N(self.fx.subs(self.x, xr)))

        # creating the data format for printing
        data = [1, xl, xu, xr, fxr, self.ees]
        print("|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|Ɛes={:>{tab}}".format(
            *data, tab=2*self.tab))

        for i in range(2, self.itr_no+1):
            # determine which sub-interval our root lies in
            if fxl*fxr == 0:
                print(f"root x{i} = {xr}")
            elif fxl*fxr < 0:
                xu = xr
            else:
                xl = xr

            # store xr to be used later in calculating our approx. error
            xr_old = xr

            xr = (xu + xl) / 2
            fxr = float(sp.N(self.fx.subs(self.x, xr)))

            if self.err_relative:
                ea = abs((xr - xr_old)/xr) if xr != 0 else 0.001
            else:
                ea = abs(xr - xr_old)

            # creating the data format for printing
            data = [i, xl, xu, xr, fxr, ea]

            # check our stopping condition
            if ea <= self.ees:
                print("|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|Ɛa ={:>{tab}} <=".format(
                    *data, tab=2*self.tab))
                print()
                print(f"root x{i} = {xr}")
                return i, xr, self.err_bound_bracket(xu,xl,i)
            else:
                print("|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|Ɛa ={:>{tab}} > ".format(
                    *data, tab=2*self.tab))

        print()
        print("Max number of allowed iterations reached !!!")
        print(f"root x{i} = {xr}")
        print("_________________")
        return i, xr, self.err_bound_bracket(xu,xl,i)

    def false_position(self, xl, xu):
        print()
        print()
        print("False-position :")
        print("_______________________")
        print()

        # creating the table format for printing
        table_format = ["i", "xl", "xu", "f(xl)", "f(xu)", "xr", "f(xr)"]
        print("|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}".format(
            *table_format, tab=2*self.tab))

        fxl = float(sp.N(self.fx.subs(self.x, xl)))
        fxu = float(sp.N(self.fx.subs(self.x, xu)))

        if fxl*fxu > 0:
            print("No roots !!!")
            return -1, xl

        xr = float((xl*fxu - xu*fxl) / (fxu - fxl))
        # xr = float(((xu-xl)*fxl) / (fxu - fxl))
        fxr = float(sp.N(self.fx.subs(self.x, xr)))

        # creating the data format for printing
        data = [1, xl, xu, fxl, fxu, xr, fxr, self.ees]
        print("|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|Ɛes={:>{tab}}".format(
            *data, tab=2*self.tab))

        for i in range(0, self.itr_no+1):
            # determine which sub-interval our root lies in
            if fxl*fxr == 0:
                print(f"root x{i} = {xr}")
            elif fxl*fxr < 0:
                xu = xr
            else:
                xl = xr

            # store xr to be used later in calculating our approx. error
            xr_old = xr

            xr = float((xl*fxu - xu*fxl) / (fxu - fxl))
            # xr = float(((xu-xl)*fxl) / (fxu - fxl))
            fxr = float(sp.N(self.fx.subs(self.x, xr)))

            fxl = float(sp.N(self.fx.subs(self.x, xl)))
            fxu = float(sp.N(self.fx.subs(self.x, xu)))

            if self.err_relative:
                ea = abs((xr - xr_old)/xr) if xr != 0 else 0.001
            else:
                ea = abs(xr - xr_old)

            # creating the data format for printing
            data = [i, xl, xu, fxl, fxu, xr, fxr, ea]

            # check our stopping condition
            if ea <= self.ees:
                print("|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|Ɛa ={:>{tab}} <=".format(
                    *data, tab=2*self.tab))
                print()
                print(f"root x{i} = {xr}")
                return i, xr
            else:
                print("|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|Ɛa ={:>{tab}} > ".format(
                    *data, tab=2*self.tab))

        print()
        print("Max number of allowed iterations reached !!!")
        print(f"root x{i} = {xr}")
        print("_________________")
        return i, xr

    def newton_raphson(self, xi, alpha=False):
        print()
        print()
        print("Newton Raphson :")
        print("_______________________")
        print()

        # creating the table format for printing
        table_format = ["i", "xi", "x(i+1)"]
        print("|{:>{tab}}|{:>{tab}}|{:>{tab}}|".format(
            *table_format, tab=2*self.tab))

        for i in range(self.itr_no+1):
            # evaluate f(xi) and f'(xi)
            fxi = float(sp.N(self.fx.subs(self.x, xi)))
            dfxi = float(sp.N(sp.diff(self.fx).subs(self.x, xi)))
            # if we want to add delta
            # delta δ = alpha - xi

            # estimate new value of x(i+1)
            xi_new = float(xi - (fxi/dfxi))

            if self.err_relative:
                ea = abs((xi_new - xi)/xi_new) if xi_new != 0 else 0.001
            else:
                ea = abs(xi_new - xi)

            # creating the data format for printing
            data = [i, xi, xi_new, ea]

            # check our stopping condition
            if ea <= self.ees:
                print("|{:>{tab}}|{:>{tab}}|{:>{tab}}|Ɛa ={:>{tab}} <=".format(
                    *data, tab=2*self.tab))
                print()
                print(f"root x{i+1} = {xi_new}")
                if alpha != False:
                    return i+1, xi_new,self.err_bound(alpha,xi)
                else:
                    return i+1, xi_new,False

            else:
                print("|{:>{tab}}|{:>{tab}}|{:>{tab}}|Ɛa ={:>{tab}} > ".format(
                    *data, tab=2*self.tab))

            # replace xi with x(i+1)
            xi = xi_new

        print()
        print("Max number of allowed iterations reached !!!")
        print(f"root x{i+1} = {xi_new}")
        print("_________________")
        if alpha != False:
            return i+1, xi_new,self.err_bound(alpha,xi)
        else:
            return i+1, xi_new,False

    #SECANT
    def secant(self, xi, xi_1):
        print()
        print()
        print("Secant :")
        print("_______________________")
        print()

        # creating the table format for printing
        table_format = ["i","x(i-1)","x(i)", "x(i+1)"]
        print("|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|".format(*table_format, tab=2 * self.tab))

        # if self.fx.subs(self.x,xi) * self.fx.subs(self.x,xi_1) >= 0:
        #     print("Secant method won't work")
        #     return

        for i in range(self.itr_no+1):
            fxi = float(sp.N(self.fx.subs(self.x,xi)))
            fxi_1 = float(sp.N(self.fx.subs(self.x,xi_1)))

            #Computing the estimated root
            xi_new = float(xi - fxi * ((xi_1 - xi)/(fxi_1 - fxi)))

            if self.err_relative:
                ea = abs((xi_new - xi)/xi_new) if xi_new != 0 else 0.001
            else:
                ea = abs(xi_new - xi)

            # creating the data format for printing
            data = [i, xi_1, xi, xi_new, ea]

            # check our stopping condition
            if ea <= self.ees:
                print("|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|Ɛa ={:>{tab}} <=".format(*data, tab=2 * self.tab))
                print()
                print(f"root x{i + 1} = {xi_new}")
                return i+1, xi_new
            else:
                print("|{:>{tab}}|{:>{tab}}|{:>{tab}}|{:>{tab}}|Ɛa ={:>{tab}} > ".format(*data, tab=2 * self.tab))

            #Replacing values
            xi_1 = xi
            xi = xi_new

            # if xi_1 == xi:
            #     print("Can't Divide by Zero")
            #     return -1, xi

        print()
        print("Max number of allowed iterations reached !!!")
        print(f"root x{i + 1} = {xi_new}")
        print("_________________")
        return i+1, xi_new

    def fixed_point(self, x1, x2=None):
        print()
        print()
        print("Fixed-Point :")
        print("_______________________")
        print()

        if x2:
            xi = (x1 + x2)/2
        else:
            xi = x1

        # evaluate g'(xi)
        dgx = float(sp.N(sp.diff(self.fx).subs(self.x, xi)))

        if dgx > 1:
            print(f"g'{xi} = {dgx} > 1    Divergence !!!")
            return -1, xi

        # creating the table format for printing
        table_format = ["i", "xi", "x(i+1)"]
        print("|{:>{tab}}|{:>{tab}}|{:>{tab}}|".format(
            *table_format, tab=2*self.tab))

        for i in range(1, self.itr_no+1):
            # estimate new value of x(i+1)
            xi_new = float(sp.N(self.fx.subs(self.x, xi)))

            if self.err_relative:
                ea = abs((xi_new - xi)/xi_new) if xi_new != 0 else 0.001
            else:
                ea = abs(xi_new - xi)

            # creating the data format for printing
            data = [i, xi, xi_new, ea]

            # check our stopping condition
            if ea <= self.ees:
                print("|{:>{tab}}|{:>{tab}}|{:>{tab}}|Ɛa ={:>{tab}} <=".format(
                    *data, tab=2*self.tab))
                print()
                print(f"root x{i} = {xi_new}")
                return i, xi_new
            else:
                print("|{:>{tab}}|{:>{tab}}|{:>{tab}}|Ɛa ={:>{tab}} > ".format(
                    *data, tab=2*self.tab))

            # replace xi with x(i+1)
            xi = xi_new

        print()
        print("Max number of allowed iterations reached !!!")
        print(f"root x{i} = {xi_new}")
        print("_________________")
        return i, xi_new

def function_parser(fx):

    #Define trasnformation
    transformations = (standard_transformations + (implicit_multiplication_application,) \
                    + (function_exponentiation,) + (implicit_application,) + (implicit_multiplication,) \
                    + (convert_xor,) + (factorial_notation,) + (auto_number,))

    my_eq = parse_expr(fx, transformations=transformations)
    x = sp.Symbol("x")
    e = sp.Symbol("e")

    my_eq = my_eq.subs(e,sp.exp(1))

    return my_eq

if __name__ == "__main__":
    # the objective function
    x = sp.Symbol("x")
    fx = x**4 - 2*(x**3) - 4*(x**2) + 4*x + 4

    # the number of iterations
    itr_no = 10

    # the Epsilon
    ees = 0.01

    # initializing our numerical methods class
    num_method = num_method(fx, itr_no=itr_no, ees=ees)

    # our interval
    # [xl, xu]
    xl = -2
    xu = -1

    # Calling our Bisection Method
    num_method.bisection(xl, xu)

    # if want to add sin use sp.sin(x)
    fx = sp.exp(-x) - x

    x0 = 0
    num_method.fx = fx
    num_method.ees = 10**(-6)
    num_method.err_relative = False
    num_method.newton_raphson(x0)

    fx = (2*x - 1)/x
    x1 = 1
    x2 = 4
    num_method.fx = fx
    num_method.ees = 0.1
    num_method.err_relative = False
    num_method.fixed_point(x1, x2)

    fx = x**2 - x + 1
    x1 = 2.5
    num_method.fx = fx
    num_method.ees = 0.1
    num_method.err_relative = False
    num_method.fixed_point(x1)

    num_method.fx = 2*x**3 - 2*x - 5
    num_method.ees = 10**-2
    num_method.err_relative = True
    num_method.false_position(1, 2)

    # printing our class components for debugging
    num_method._components()


# encoding=utf8

import numpy as np

def bernoulli(p=0.5, size=1):
    r"""
    Generates random samples from the Bernoulli distribution.

    Parameters
    ----------
    p : probability of success, default 0.5
        The probability of success of a Bernoulli trial (Yes/No, Pass/Fail, etc.). Must be between
        0 and 1
    size : int, default 1
        The number of random Bernoulli samples to generate.

    Returns
    -------
    list or boolean
        List containing the generated Bernoulli random samples represented by boolean True/False values.
        If size is 1, the single generated sample is returned as a boolean.

    Notes
    -----

    See Also
    --------
    Bernoulli : class
        Class containing several methods related to estimating Bernoulli random variates.

    References
    ----------

    """
    if 0.0 > p or p > 1.0:
        raise ValueError('p must be between 0 and 1')
    if size < 1:
        raise ValueError('size parameter must be at least 1')
    if np.floor(size) != size:
        size = np.floor(size)

    b = []
    for _ in np.arange(size):
        b.append(continuous_uniform() < p)

    if len(b) == 1:
        b = b[0]

    return b

def binomial(n=5, p=0.5, size=1):
    if 0.0 > p or p > 1.0:
        raise ValueError('p must be between 0 and 1')
    if n < 0:
        raise ValueError('n must be at least 0')
    if np.floor(n) != n:
        n = np.floor(n)
    if size < 1:
        raise ValueError('size parameter must be at least 1')
    if np.floor(size) != size:
        size = np.floor(size)

    bi = []

    for _ in np.arange(size):
        s = 0

        for _ in np.arange(n):
            s += bernoulli(p)

        bi.append(s)

    if len(bi) == 1:
        bi = bi[0]

    return bi

def continuous_uniform(a=0.0, b=1.0, size=1):
    if a > b:
        raise ValueError('parameter b must be greater than a')
    if size < 1:
        raise ValueError('size parameter must be at least 1')
    if np.floor(size) != size:
        size = np.floor(size)

    u = []
    for _ in np.arange(size):
        u.append(a + (b - a) * np.random.random_sample())

    if len(u) == 1:
        u = u[0]

    return u

# def hypergeometric(n, N, K):
#     if n < 1:
#         raise ValueError('number of trials must be at least 1')
#     elif n > N:
#         raise ValueError('number of trials must be higher than population of elements')
#     elif N < 1:
#         raise ValueError('population of elements must be at least 1')
#     elif K < 0:
#         raise ValueError('number of successes must be at least 0')
#
#     h = 0
#
#     for _ in np.arange(n):
#         p = K / N
#
#         if bernoulli(p):
#             h += 1


import numpy as np
import scipy as sc
import util
import log

from scipy import special
from scipy import stats

def mleRegression(x_data, t_data, std):
    '''
    Completes a MLE regression for the given data
    Args:
        x_data (np.array): array of input data
        y_data (np.array): array of training data
    '''
    log.log("Calculating MLE regression")
    (N,K) = x_data.shape
    x = np.zeros((N,K+1)) + 1
    x[:,1:] = x_data #Expanding with a colomn of ones for bias terms
    #(X'*X)^(-1)*(X')*Y
    xtx_i = np.linalg.inv(x.T.dot(x))
    beta_hat = xtx_i.dot(x.T).dot(t_data)
    #Standard variance
    y_star = (t_data - x.dot(beta_hat))

    s2 = y_star.dot(y_star)
    sig_hat = 1.0/(N-K-1)*s2
    std_err = (np.sqrt(np.eye(K+1)*sig_hat*xtx_i)).dot(np.zeros(K+1)+1)

    t = (beta_hat/std_err)
    p_right = 1.0-sc.stats.t.cdf(np.abs(t),N-K-1)
    p_left = sc.stats.t.cdf(-np.abs(t),N-K-1)
    p = p_right+p_left

    log.sucess("MLE Regression complete")
    return beta_hat, std_err, t, p

def priorExpectations(x_data, t_data, beta_hat, beta_tilde, a, b, c):
    '''
    Computes the expecation and variance of the beta_hats for conjugate priors
    @params:
        x_data (np.array): array of input data
        t_data (np.array): array of training data
        beta_hat (np.array): MLE linear regression weights
        beta_tilde (np.array): prior hyperparameter
        a (float): hyper-parameter
        b (float): hyper-parameter
        c (float): hyper-parameter
    '''
    log.log("Computing exp and var for conjugate priors")
    log.log("Hyper-parameters a:%.2f b:%.2f c:%.2f"%(a, b, c))

    (N,K) = x_data.shape
    x = np.zeros((N,K+1)) + 1
    x[:,1:] = x_data #Expanding with a colomn of ones for bias terms

    y_star = (t_data - x.dot(beta_hat))
    s2 = y_star.dot(y_star)
    #(X'*X)^(-1)*(X')*Y
    xtx_i = np.linalg.inv(x.T.dot(x))
    M = np.eye(K+1)/c

    c1 = np.linalg.inv(np.linalg.inv(M)+np.linalg.inv(x.T.dot(x)))
    c1 = (beta_tilde - beta_hat).T.dot(c1).dot(beta_tilde - beta_hat)
    c2 = np.linalg.inv(M+x.T.dot(x))

    exp_sig2 = (2*b + s2 + c1)/(N + 2*a - 2)
    exp_beta = c2.dot(x.T.dot(x).dot(beta_hat) + M.dot(beta_tilde))
    covar_beta = (exp_sig2)*c2
    var_beta = (np.eye(K+1)*(covar_beta)).dot(np.zeros(K+1)+1) #Get the variance (diagonal) elements

    log.sucess("MLE Regression complete")
    return exp_sig2, exp_beta, var_beta

def gPriorExpectations(x_data, t_data, beta_hat, beta_tilde, c, c0):
    '''
    Computes the expecation and variance along with the bayes factor for Zellner's
    informative g-prior
    @params:
        x_data (np.array): array of input data
        t_data (np.array): array of training data
        beta_hat (np.array): MLE linear regression weights
        beta_tilde (np.array): prior hyperparameter
        c (float): hyper-parameter
        c0 (float): model comparision hyper-parameter
    '''
    log.log("Computing exp, var and bayes factor for Zellner's informative G-prior")
    log.log("Hyper-parameters c:%.2f"%(c))

    (N,K) = x_data.shape
    x = np.zeros((N,K+1)) + 1
    q = 1
    x[:,1:] = x_data #Expanding with a colomn of ones for bias terms

    y_star = (t_data - x.dot(beta_hat))
    s2 = y_star.dot(y_star)
    #(X'*X)^(-1)*(X')*Y
    xtx = x.T.dot(x)
    yty = t_data.T.dot(t_data)

    exp_beta = 1.0/(c+1)*(beta_tilde + c*beta_hat)
    c1 = (s2 + (beta_tilde - beta_hat).T.dot(xtx).dot(beta_tilde-beta_hat)/(c+1))
    covar_beta = (c/(c+1.0))*(c1)/(N-2)*np.linalg.inv(xtx)
    var_beta = (np.eye(K+1)*(covar_beta)).dot(np.zeros(K+1)+1)

    b_coeff = ((c0+1)**((K+1-q)/2.0))/((c+1)**((K+1)/2.0))

    b_dem = yty - c/(c+1.0)*t_data.T.dot(x).dot(np.linalg.inv(xtx)).dot(x.T).dot(t_data) + \
             1/(c+1.0)*beta_tilde.T.dot(xtx).dot(beta_tilde) - 2/(c+1.0)*t_data.T.dot(x).dot(beta_tilde)

    #Comput the bayes factors for models excluding a specific input
    b_num = np.zeros(K+1)
    for i in range(K+1):
        x0 = np.delete(x, i, 1)
        beta_tilde0 = np.delete(beta_tilde, i, 0)
        xtx = x0.T.dot(x0)

        b_num[i] = yty - c0/(c0+1.0)*t_data.T.dot(x0).dot(np.linalg.inv(xtx)).dot(x0.T).dot(t_data) + \
            1/(c0+1.0)*beta_tilde0.T.dot(xtx).dot(beta_tilde0)-2/(c0+1.0)*t_data.T.dot(x0).dot(beta_tilde0)

    b_10 = b_coeff*(b_num/b_dem)**(0.5*N)

    return exp_beta, var_beta, b_10

def getHPD(x_data, t_data, beta_hat, beta_tilde, alpha):
    '''
    Get the HPD region
    @params:
        x_data (np.array): array of input data
        t_data (np.array): array of training data
        beta_hat (np.array): MLE linear regression weights
        beta_tilde (np.array): prior hyperparameter
        alpha (float): HPD range = (1-alpha)
    '''
    log.log("Computing HPD region")

    (N,K) = x_data.shape
    x = np.zeros((N,K+1)) + 1
    x[:,1:] = x_data #Expanding with a colomn of ones for bias terms

    xtx = x.T.dot(x)
    w = (np.eye(K+1)*np.linalg.inv(xtx)).dot(np.zeros(K+1)+1)

    y_star = (t_data - x.dot(beta_hat))
    s2 = y_star.dot(y_star)

    f = sc.stats.t.ppf(1-(alpha/2.0), N-K-1, loc=0, scale=1)
    hpd_interval = f*np.sqrt(w*s2/(N-K-1.0))

    hpd = np.zeros((K+1,2))
    hpd[:,0] = beta_hat[:] - hpd_interval[:]
    hpd[:,1] = beta_hat[:] + hpd_interval[:]

    return hpd

def zellnerNonInfoGPrior(x_data, y, beta_hat, beta_tilde):
    '''
    Computes the expectation and variance of Zellner's non-informative Gprior
    @params:
        x_data (np.array): array of input data
        t_data (np.array): array of training data
        beta_hat (np.array): MLE linear regression weights
        beta_tilde (np.array): prior hyperparameter
    '''
    log.log("Computing exp and var for Zellner's non-informative G-prior")

    clim = 101
    (N,K) = x_data.shape
    x = np.zeros((N,K+1)) + 1
    x[:,1:] = x_data #Expanding with a colomn of ones for bias terms
    #General commonly used numbers
    xtxi = np.linalg.inv(x.T.dot(x))
    yty = y.T.dot(y)
    c1 = y.T.dot(x).dot(xtxi).dot(x.T).dot(y)
    c2 = beta_hat.T.dot(x.T.dot(x)).dot(beta_hat)

    y_star = (y - x.dot(beta_hat))
    s2 = y_star.dot(y_star)

    #For non-informative we must marginalize over c
    exp_num = 0; exp_dem = 0;
    for c in range(1,clim):
        fxy0 = (1.0/c)*(c+1)**(-(K+1)/2.0)*(yty-(c*c1)/(c+1.0))**(-N/2.0)
        exp_num += c/(c+1.0)*fxy0
        exp_dem += fxy0
    exp_beta = (exp_num/exp_dem)*beta_hat

    var_num1 = 0; var_num2 = 0;
    for c in range(1,clim):
        fxyc0 = (c+1)**(-(K+1)/2.0)*(yty-(c*c1)/(c+1.0))**(-N/2.0)
        var_num1 += fxyc0/((N-2.0)*(c+1.0))*(s2+c2/(c+1.0))
        var_num2 += (c/(c+1.0) - (exp_num/exp_dem))**2 * fxyc0*(1.0/c)

    covar_beta = (var_num1/exp_dem)*(xtxi) + np.outer(beta_hat,(var_num2/exp_dem)*beta_hat.T)
    var_beta = (np.eye(K+1)*(covar_beta)).dot(np.zeros(K+1)+1)

    return exp_beta, var_beta


import numpy as np

from sympy import symbols, factorial, simplify, integrate, diff, exp
from sympy import cos, sin, sqrt, pi, I
from sympy import legendre
from sympy.abc import theta, phi, x

# with(orthopoly):

def intSpherik(f):
	"""
	Compute the integral of:
	.. math :: \int_0^{\pi}\int_0^{2\pi} f \mathrm{d}\phi \mathrm{d}\theta

	Parameters
	----------
	f

	Returns
	-------

	Notes
	-----
	theta, phi are sympy symbols
	"""
	# g = int(f, phi=0..2*pi);
	g = integrate(f, (phi, 0, 2*pi))
	# simplify(int_(g * sin(theta), theta=0..Pi))
	return simplify(integrate(g * sin(theta), (theta, 0, pi)))

def associatedLegendre(l: int, m: int):
	"""
	Compute the associated legendre as (with replacing x by cos(theta) in the derivative:

	.. math :: (-1)^m  \frac{1}{(2^l\cdot l!)} (\sin(\theta))^m
				\frac{\mathrm{d}^m P_n(x)}{\mathrm{d} x^m}\\
				x = \cos(\theta)

	Parameters
	----------
	l : int
	m : int

	Returns
	-------
	associated_legendre :

	Notes
	-----
	Result is simplify compared to the maple script

	x, theta are sympy symbols

	Raises
	------
	ArithemticError if m < 0

	"""
	# TODO: look into sympy.assoc_legendre it seems behave the same, and it is way faster
	if l == 0:
		return 1
	if m < 0:
		raise ArithmeticError(f"m should be non negative, m is {m}")

	associated_legendre = (-1) ** m * 1 / (2 ** l * factorial(l)) * (sin(theta)) ** m * \
	diff(((x**2 - 1)**l), x, l+m) .subs(x, cos(theta))
	# subs(x=cos(theta), diff((x ^ 2 - 1) ^ l, seq(x, i=1..(l + m))))
	# f.subs(x, cos(theta))

	return associated_legendre

def YY(l: int, m: int):
	"""

	Parameters
	----------
	l
	m

	Returns
	-------

	Notes
	-----
	x, theta are sympy symbols
	"""
	# TODO: check
	if l == 0 and m == 0:
		return 1 / (4 * np.pi)

	mm = abs(m)

	res = I ** (m + mm) * \
		simplify(sqrt((2*l + 1) * factorial(l - mm) / factorial(l + mm) / 4 / pi) * 1/2 ** l / factorial(l)) * \
		sin(theta) ** mm * exp(I * m * phi) * \
		diff(((x**2 - 1) ** l), x, l + mm).subs(x, cos(theta))
	# subs(x=cos(theta), diff( (x * 2 - 1) ** l, x$(l + mm)))
	# f.subs(x, cos(theta))

	return res

def SS(l: int, m: int):
	"""

	Parameters
	----------
	l
	m

	Returns
	-------

	Notes
	-----
	phi is a sympy symbol
	"""

	if m == 0:
		return sqrt((2 * l + 1) / (4 * pi)) * legendre(l, cos(theta))

	cte = sqrt((2 * + l + 1) * factorial(l - abs(m)) / factorial(l + abs(m)) / 2 / pi)

	if m > 0:
		# note : it was associatedLegendre(l, m, theta) in the maple code
		return cte * associatedLegendre(l, m) * cos(m * phi)
	return cte * associatedLegendre(l, -m) * sin(-m * phi)


'''
Linear Optimization assignment 1. Finding solution to a system of linear equation by Gaussian elimination method
'''
import numpy as np
import math

#To find column ith of matrix
#Used in printing the solution when the system is consistent
def column(matrix, i):
    return [row[i] for row in matrix]

# Helper function
def myfunction(x):
    if x < math.pow(10, -8):
        return 0

'''
Function which implements main algorithm of Gaussian Elimination to find the reduced row echelon form.
'''
def reducedRowEchelonForm(M):
    # Base case
    if not M:
        return

    # Find rows, columns
    no_rows = len(M)
    no_columns = len(M[0])
    front = 0

    #iterate through the rows
    for r in range(no_rows):
        if no_columns <= front:
            return
        i = r
        while M[i][front] == 0:
            i = i + 1
            if no_rows == i:
                i = r
                front = front + 1
                if no_columns == front:
                    return

        M[i],M[r] = M[r],M[i] #Swap rows i and r
        temp = M[r][front]

        if temp != 0:
            M[r] = [ mrtemp / float(temp) for mrtemp in M[r]]

        for i in range(no_rows):
            if i != r:
                temp = M[i][front] #subtract temp multiplied by row r from row i
                M[i] = [ itemp - temp*rtemp for itemp,rtemp in zip(M[i],M[r])]

        front = front + 1

#read input from file
f = open('input.txt', 'r')
mtx = [ map(int,line.split(',')) for line in f ]

#function call
reducedRowEchelonForm(mtx)

#print np.apply_along_axis(myfunction, axis=1, arr=mtx)
a = np.array(mtx);
b = a[:, :-1]
bound = 0.000000001

#print the reduced row echelon matrix
print "The RREF is:"
for rw in mtx:
  print ', '.join( (str(rv) for rv in rw) )

ans = 0
ans1 = 0
rows = (a != 0).sum(1)
rows1 = (b != 0).sum(1)

#print rows
#print rows1

#Loop to find rank of augmented matrix
for r in rows:
    if r == 0:
        ans = ans + 1

#Loop to find rank of original matrix
for r in rows1:
    if r == 0:
        ans1 = ans1 + 1

#print ans
#print ans1

ans = len(mtx) - ans
ans1 = len(mtx) - ans1

print "The rank of augmented matrix is", ans
print "The rank of original matrix is", ans1

if ans != ans1:
    print "The system has no solution"

check = len(mtx[0]) - 1
diff = check - ans

solution = column(a, check)

if ans < check:
    print "The system has infinite solutions, described by an affine subspace of dimension ",diff
'''Print solution
If the system is consistent then the last column of the RREF is the solution
'''
if ans == check:
    print "The system has a unique solution: "
    for s in solution:
        if abs(s) < bound:
            s = 0
        print s


import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d
from matplotlib import cm
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
import torch
import os

# Implementing relu()
def relu(z):
    if z > 0:
        return z
    else:
        return 0

# Define linewidth and fontsize to control the aesthetics of the plots easily
linewidth = 4
fontsize = 20

# Define a range of values for the inputs of relu(z)
z_range = np.arange(-5,5, 0.01)

plt.figure(figsize=(16,9))
# For each z in x_range compute relu(z)
y_relu = [relu(z) for z in z_range]
plt.plot(z_range, y_relu, c='b', linewidth= linewidth, label='Relu(z)')
plt.ylim(-5, 5)
plt.xlim(-5, 5)
plt.grid()
plt.legend(fontsize=fontsize, loc=2)
plt.show()

def grad_relu(z):
    if z > 0:
        return 1
    else:
        return 0

### The gradients of relu
y_relu = [relu(z) for z in z_range]
grad_y_relu = [grad_relu(z) for z in z_range]

plt.figure(figsize=(16, 9))
# The relu
plt.subplot(1,2,1)
plt.plot(z_range, y_relu, c='b',linewidth= linewidth, label='Relu(z)')
plt.legend(fontsize=fontsize,loc=2)
plt.grid()

### The gradients of relu
plt.subplot(1,2,2)
plt.plot(z_range, grad_y_relu, c='r',linewidth= linewidth, label='d Relu(z)/dz')
plt.legend(fontsize=fontsize,loc=2)
plt.grid()
plt.show()

# Demonstrating the flexibility of relu: relu(z),relu(-z),-relu(z),-relu(-z)
z_range = np.arange(-5,5, 0.01)

plt.figure(figsize=(16,9))
plt.suptitle('The Flexibility of Relu(z)', fontsize=fontsize)

plt.subplot(2,2,1)
y_relu = [relu(z) for z in z_range]
plt.plot(z_range, y_relu, c='b', linewidth= linewidth, label='Relu(z)')
plt.ylim(-5,5)
plt.xlim(-5,5)
plt.grid()
plt.legend(fontsize=fontsize, loc=2)

plt.subplot(2,2,2)
y_relu = [relu(-z) for z in z_range]
plt.plot(z_range, y_relu, c='k', linewidth= linewidth,label='Relu(-z)')
plt.ylim(-5,5)
plt.xlim(-5,5)
plt.legend(fontsize=fontsize,loc=1)
plt.grid()

plt.subplot(2,2,3)
y_relu = [-relu(z) for z in z_range]
plt.plot(z_range, y_relu, c='r', linewidth= linewidth,label='-Relu(z)')
plt.ylim(-5,5)
plt.xlim(-5,5)
plt.legend(fontsize=fontsize,loc=2)
plt.grid()

plt.subplot(2,2,4)
y_relu = [-relu(-z) for z in z_range]
plt.plot(z_range, y_relu, c='g', linewidth= linewidth,label='-Relu(-z)')
plt.ylim(-5,5)
plt.xlim(-5,5)
plt.legend(fontsize=fontsize,loc=1)
plt.grid()

plt.show()

# The rotation of the slope in relu
w_range = np.arange(0.5, 3.5, 0.5)
plt.figure(figsize=(16, 9))
plt.suptitle('Changing the slope of Relu(w*z) using a coefficient w', fontsize=fontsize)
for idx, w in enumerate(w_range):
    plt.subplot(2,3,idx+1)
    y_relu = [relu(w*z) for z in z_range]
    plt.plot(z_range, y_relu, c='b', linewidth=linewidth, label='w = %.2f' % w)
    plt.ylim(-1, 5)
    plt.xlim(-5, 5)
    plt.grid()
    plt.legend(fontsize=fontsize, loc=2)
plt.show()

# Shifting the relu horizontally
bias = np.arange(0.5, 3.5, 0.5)
plt.figure(figsize=(16, 9))
plt.suptitle('Shifting Relu(z+b) horizontally using a bias term b inside Relu()', fontsize=fontsize)
for idx, b in enumerate(bias):
    plt.subplot(2,3, idx+1)
    y_relu = [relu(z+b) for z in z_range]
    plt.plot(z_range, y_relu, c='b', linewidth=linewidth, label='b = %.2f' % b)
    plt.ylim(-1, 5)
    plt.xlim(-4, 4)
    plt.grid()
    plt.legend(fontsize=fontsize, loc=2)
plt.show()

# Shifting the relu vertically
bias = np.arange(0.5, 3.5, 0.5)
plt.figure(figsize=(16, 9))
plt.suptitle('Shifting Relu(z) + b vertically using a bias term b outside Relu()', fontsize=fontsize)
for idx, b in enumerate(bias):
    plt.subplot(2,3, idx+1)
    y_relu = [relu(z)+b for z in z_range]
    plt.plot(z_range, y_relu, c='b', linewidth=linewidth, label='b = %.2f' % b)
    plt.ylim(-1, 5)
    plt.xlim(-4, 4)
    plt.grid()
    plt.legend(fontsize=fontsize, loc=2)
plt.show()

# Defining the data and the ground-truth
x = torch.unsqueeze(torch.linspace(-10, 10, 300), dim=1)
y = x.pow(3)

# Setting the available device
use_gpu = torch.cuda.is_available()
device = torch.device("cuda" if use_gpu else "cpu")
print("Device", device)

# Build a regression model clas
class Regressor(nn.Module):
    def __init__(self, n_hidden=2):
        super(Regressor, self).__init__()
        self.hidden = torch.nn.Linear(1, n_hidden)  # hidden layer
        self.predict = torch.nn.Linear(n_hidden, 1)  # output layer

    def forward(self, x):
        x = F.relu(self.hidden(x))
        x = self.predict(x)
        return x

# number of relu() units
n_hidden = 7
# total number of epochs
n_epochs = 4000
# Building an object from the regressor class while  passing
# n_hidden and setting the model to train() mode
regressor = Regressor(n_hidden=n_hidden).train()
# Defining the optimizer
optimizer = torch.optim.SGD(regressor.parameters(), lr=0.0001)
# Defining MSE as the appropriate los function
# For regression.
loss_func = torch.nn.MSELoss()

plt.figure(figsize=(16, 9))
for epoch in range(n_epochs):
    # Put the model in training mode
    regressor.train()
    # This is there to clear the previous plot in the animation
    # After each epoch
    plt.clf()
    # input x to the regressor and receive the predicion
    y_hat = regressor(x)
    # Compute the loss between y_hat and the actual
    # Value of the ground-truth curve, y
    loss = loss_func(y_hat, y)
    # Compute the gradients w.r.t all the parameters
    loss.backward()
    # Update the parameters
    optimizer.step()
    # Zero out all the gradients before inputing the next data point
    # Into the regressor model
    optimizer.zero_grad()

    # Every 100 epoch evaluate do some plotting
    if epoch % 100 == 0:
        print('Epoch %d --- Loss %.5f' % (epoch+1, loss.data.numpy()))
        # Bbefore evaluation, put the model back to evaluation mode
        regressor.eval()
        # At this very moment of training, grab the current biases and weights
        # From the model object, namely, b_0, b_1, w_0, and w_1
        biases_0 = regressor.hidden.bias.cpu().detach().numpy()
        weights_0 = regressor.hidden.weight.squeeze(0).cpu().detach().numpy()
        biases_1 = regressor.predict.bias.cpu().detach().numpy() # This has ONLY 1 value
        weights_1 = regressor.predict.weight.squeeze(0).cpu().detach().numpy()

        # For the purpose of plotting consider the current range of
        # x as the inputs to EACH relu() individualy
        data = x.detach().numpy()
        # This will hold the UNLIMATE
        # prediction, that is, relu(input*w_0+b_0)*w_1 + b_1
        # We reset it before plotting the current status of the model
        # And the learned relu() functions
        sum_y_relu = []
        # For each relu() unit do the following
        for idx in range(n_hidden):

            plt.suptitle('Epoch=%d --- MSE loss= %.2f' % (epoch+1, loss.data.numpy()), fontsize=fontsize)
            # Plot output of the current relu() unit
            plt.subplot(1,3,1)
            plt.title('Relu(w_0*x + b_0)', fontsize=fontsize)
            y_relu = [relu(d*weights_0[idx]+biases_0[idx]) for d in data]
            plt.plot(data, y_relu)
            plt.ylim(-1,40)
            plt.grid()

            plt.subplot(1, 3, 2)
            # Plot output of the current relu(), multiplied by its
            # corresponding weight, w_1, and summed with the bias b_1
            plt.title('Relu(w_0*x + b_0)*w_1 + b_1',fontsize=fontsize)
            y_relu = [relu(d*weights_0[idx]+biases_0[idx])*weights_1[idx] + biases_1[0] for d in data]
            plt.plot(data,y_relu)
            plt.ylim(-500,900)
            plt.grid()

            # Kee adding the Relu(w_0*x + b_0)*w_1 + b_1 for each relu to the
            # sum_y_relu list. We will sum them up later to plot
            # The ULTIMATE predction of the model y_hat
            sum_y_relu.append([relu(d*weights_0[idx]+biases_0[idx])*weights_1[idx] + biases_1[0] for d in data])

        # Sum it all up
        sum_y_relu = np.sum(np.array(sum_y_relu),axis=0)
        plt.subplot(1, 3, 3)
        plt.title('y_hat)', fontsize=fontsize)
        plt.plot(x.data.numpy(), y.data.numpy(), color="k", label='Ground-truth')
        plt.plot(data,sum_y_relu, c='r', label='Prediction')
        plt.legend()
        plt.grid()

        # A slight delay in the animation
        plt.pause(0.1)


# PROJECT: Softmax Function
# PROGRAMMER: Carlos Mertens - Udacity Student
#				Udacity Instructors (Machine Learning Engineer Nanodegree)
# DATE CREATED: (DD/MM/YY) 10/01/2019
# REVISED DATE: (DD/MM/YY)
# PURPOSE: Udacity instructors show the algorithm to implement Softmax Function
#			From the scratch.
# USAGE: ...

import numpy as np

# Write a function that takes as input a list of numbers, and returns
# the list of values given by the softmax function.
def my_softmax(L):
    exp_list = np.exp(L)
    sum_exp_list = sum(exp_list)
    result = []
    for i in exp_list:
        result.append(i * 1.0 / sum_exp_list)
    return result

my_list = [5,6,7]

# Call function to get the softmax from my_list
my_list_softmax = my_softmax(my_list)

print(my_list_softmax)


import math as m
import numpy as np

from src.discrete.bernoulli import BernDist
from src.prob_distribution import ProbDist
from src.spaces.spaces1d_leafs import DiscreteSpace

class BinDist(ProbDist):
    """Simple binomial distribution."""

    def __init__(self, n = 1, p = 0.5):
        """Create Bin(n, p) distribution.

        :param n Which should be summed
        :param p Probability that random var is true
        """

        # save params
        assert 0 <= p <= 1
        assert 1 <= n
        self.p = p
        self.n = n

        # create distribution for sampling
        self.BG = BernDist(p)
        super().__init__(DiscreteSpace(np.arange(n + 1)))

    def expectation(self):
        """Calculates the expectations for that distribution.

        :returns The expectation of the distribution"""

        return self.n * self.p

    def var(self):
        """Calculates the variance for that distribution.

        :returns The variance of the distribution"""

        return self.n * self.p * (1 - self.p)

    def sample(self, num_samples = 1):
        """Generate random numbers from Bin(n, p).

        :param num_samples How many random numbers should be generated.
        :returns Random numbers x ~ Bin(n, p).
        """

        X = np.zeros(num_samples)
        for _ in range(self.n):
            X += self.BG.sample(num_samples)

        return X

    def __density(self, x):
        """This method calculates the mass Bin(n, p).

        :param x Which value should be evaluated.
        :returns The probability this element occurs.
        """

        assert all([e >= 0 for e in x])
        prev = m.factorial(self.n) / (m.factorial(x) * m.factorial(self.n - x))
        return prev * np.power(self.p, x) * np.power(1 - self.p, np.subtract(self.n, x))


# This contains code that enables decibel math

import numpy as np

"""
summation = add_decibels(dB, coherent = False)

Take multiple decibel values and add them together. Incoherence is assumed but can be switched to coherence.

Input:
    dB = list or numpy array of decibel values
    coherent = boolean, whether the decibel values are coherent with each other

Output:
    summation = the sum total of the decibel values in the input
"""

def add_decibels(dB,coherent = False):

    dB = np.asarray(dB)

    if coherent:
        summation = 20 * np.log10(sum(10**(dB/20)))
    else:
        summation = 10 * np.log10(sum(10**(dB/10)))

    return summation

def dB_to_pressure(dB,reference,squared = True):

    dB = np.asarray(dB)

    if squared:
        value = reference * 10**(dB/20)
    else:
        value = reference * 10**(dB/10)

    return value


import numpy as np

def numerical_diff(fn, x):
    """ Numerical Differential
    함수 fn과 점 x가 주어졌을 때, x에서의 함수 fn의 미분(도함수) 값"""
    h = 1e-4  # 0.0001
    return (fn(x + h) - fn(x - h)) / (2 * h)

def f1(x):
    return 0.001 * x **2 + 0.01 * x

def f1_prime(x):
    """근사값을 사용하지 않은 함수 f1의 도함수"""
    return 0.002 * x + 0.01

def f2(x):
    """x = [x0, x1, ... ]"""
    return np.sum(x**2)  # x0**2 + x1**2 + ...

def partial_gradient(fn, x):
    """ndarray x = [x0, x1, ..., xn]에서의 함수 fn = fn(x0, x1, ..., xn)의
    각 편미분 값들의 배열을 리턴"""
    x = x.astype(np.float)  # 실수 타입으로 변경
    # gradient: np.zeros(shape=x.shape) >> 원소 n개의 0으로 된 array 생성
    gradient = np.zeros_like(x)
    h = 1e-4  # 0.0001
    for i in range(x.size):
        ith_val=x[i]
        x[i] = ith_val+h
        fh1 = fn(x)
        x[i] = ith_val-h
        fh2 = fn(x)
        gradient[i] = (fh1 - fh2) / (2*h)
    return gradient

def f3(x):
    return x[0] + (x[1] ** 2) + (x[2] ** 3)

def f4(x):
    return x[0]**2 + 2*x[0]*x[1] + x[1]*2

if __name__ == '__main__':
    estimate = numerical_diff(f1, 3)
    print('근사값:', estimate)
    real = f1_prime(3)
    print('실제값:', real)

    # f2 함수의 점(3, 4)에서의 편미분 값
    estimate_1 = numerical_diff(lambda x: x**2 + 4**2, 3)
    print(estimate_1)
    estimate_2 = numerical_diff(lambda x: 3**2 + x**2, 4)
    print(estimate_2)

    # f2 함수의 점(3, 4)에서의 편미분 값
    gradient = partial_gradient(f2, np.array([3, 4]))
    print(gradient)

    # f3 = x0 + x1 ** 2 + x2 ** 3
    # 점 (1, 1, 1) 에서의 각 편미분들의 값
    # df/dx0 = 1, df/dx1 = 2, df/dx3 = 3
    gradient2 = partial_gradient(f3, np.array([1, 1, 1]))
    print(gradient2)

    # f4 = x0**2 + 2 * x0 * x1 + x1**2
    # 점 (1, 2)에서의 df/dx0 = ?, df/dx1 =?
    gradient3 = partial_gradient(f4, np.array([1, 2]))
    print(gradient3)


#!/usr/bin/env python
'''This module contains routines to perform Gram-Schmidt orthonormalization on
a sequence of vectors.

'''
import numpy as np
import numpy.linalg as la

def gso(A, overwrite=False, out=None):
    '''Performs Gram-Schmidt orthonormalization on a sequence of vectors.

    Parameters
    ----------
    A : ndarray
        (M x N) ndarray with M <= N. The rows of A contain the sequence of
        vectors.
    overwrite : bool, optional
        If `True`, the matrix A is overwritten.
    out : ndarray, optional
        (M x N) ndarray with M <= N. The rows of `out` contain the sequence of
        orthonormal vectors. If `overwrite = True`, `out` is neglected.

    Returns
    -------
    output : ndarray
        (M x N) ndarray with M <= N. The rows of `out` contain the sequence of
        orthonormal vectors.

    Notes
    -----
    See Golub and Van Loan, Matrix Computations, 3rd edition, Section 5.2.8,
    Algorithm 5.2.5, p. 231.

    '''
    assert A.shape[0] <= A.shape[1]
    M = A.shape[0]
    if overwrite:
        output = A
    else:
        if out is not None:
            output = out
        else:
            output = np.zeros_like(A)
            output[:,:] = A
    for i in range(M):
        output[i,:] = output[i,:]/la.norm(output[i,:])
        for j in range(i+1, M):
            output[j,:] = output[j,:] - np.dot(output[j,:], output[i,:])*output[i,:]
    return output

if __name__ == '__main__':
    A = np.random.random((6,6))
    print('A')
    print(A)
    out = gso(A)
    print('\n')
    print(out)
    print('\n')
    print(np.dot(out.T, out))
    for i in range(A.shape[0]):
        for j in range(A.shape[0]):
            print(i, j, np.dot(out[i,:], out[j,:]))
        print('\n')


#Find the norm of the vector [3, 9, 5, 4] using the actual formula above. You should write a function find_norm(v1) that returns this value as a float and then call it on the provided variable n1. You should not use scipy, but you may use the math module.

import math
import numpy as np
A = [3, 9, 5, 4]
def find_norm(v1):
    list_ = [i**2 for i in v1]
    return math.sqrt(sum(list_))

find_norm(A)


import numpy as np
import math as m
import matplotlib.pyplot as plt

# P(Z = 1) = sum(1,n) of P(Z = 1|Y = i)P(Y = i) = k/n * (sum(k+1,n) of (1/(i-1))) ~ (k/n) * ln(n/k)

# Fix n = 30 and simulate P(Z = 1)
N1 = 30
K1 = [k for k in range(1, N1+1)]
Y1 = [k / N1 * m.log(N1 / k) for k in K1]
plt.plot(K1, Y1, color="blue", label="P(Z = 1| N = 30)")

# Fix n = 40 and simulate P(Z = 1)
N2 = 40
K2 = [k for k in range(1, N2+1)]
Y2 = [k / N2 * m.log(N2 / k) for k in K2]
plt.plot(K2, Y2, color="red", label="P(Z = 1| N = 40)")

# Interpretation: The probability of selecting the best secretary, when n is fixed to 30, rises as one approaches
# k ~ 11 and decreases as one approaches k = 30. The tendency is analogous when n is fixed to 40, but this time the
# maximum probability is reached when k ~ 15.

#Bruk print-f-funksjon for å skrive ut max y-verdi i begge tilfeller.
plt.grid(True)
plt.ylabel("P(Z = 1)");plt.xlabel("k"); plt.title("Probability of Choosing the Best Candidate, Given k")
#plt.savefig('p1p1f1.png', transparent=True, bbox_inches='tight',)

print(Y1.index(max(Y1)))
print(max(Y1))
print(Y2.index(max(Y2)))
print(max(Y2))
plt.show()


import math
from typing import List, Optional, Tuple

import numpy as np

def leggauss_2d(order: int) -> Optional[Tuple[np.ndarray, np.ndarray]]:
    """Generate Gauss-Legendre points and weights of the given order on the reference square [-1,1].
    """
    if math.floor(math.sqrt(order))**2 != order:
        print('order has to be a square')
        return None
    points_1d, weights_1d = np.polynomial.legendre.leggauss(int(math.sqrt(order)))
    xv, yv = np.meshgrid(points_1d, points_1d)
    points = np.stack((xv, yv), axis=2).reshape(order, 2)
    weights = np.outer(weights_1d, weights_1d).flatten()
    return points, weights

def ref_to_element_1d(x1: float, x2: float, num_points: int) -> Tuple[np.ndarray, np.ndarray]:
    ref_points, ref_weights = np.polynomial.legendre.leggauss(num_points)
    points: np.ndarray = ref_points*0.5*abs(x2 - x1) + 0.5*(x2 + x1)
    weights: np.ndarray = ref_weights*0.5*(x2 - x1)
    return points, weights

def ref_to_element_2d(x1: float, x2: float, y1: float, y2: float, num_points: int) -> Tuple[np.ndarray, np.ndarray]:
    ref_element: Optional[Tuple[np.ndarray, np.ndarray]] = leggauss_2d(num_points)
    assert ref_element is not None
    ref_points, ref_weights = ref_element
    points: np.ndarray = np.array([[x*(x2 - x1)*0.5 + (x1 + x2)*0.5, y*(y2 - y1)*0.5 + (y1 + y2)*0.5] for x, y in ref_points])
    weights: np.ndarray = ref_weights*(x2 - x1)*(y2 - y1)*0.25
    return points, weights

def generate_points_weights(elements: List[List[float]], num_points: int, dim: int) -> Tuple[np.ndarray, np.ndarray]:
    points_list: List[np.ndarray] = []
    weights_list: List[np.ndarray] = []
    for element in elements:
        if dim == 1:
            points, weights = ref_to_element_1d(*element, num_points)
        elif dim == 2:
            points, weights = ref_to_element_2d(*element, num_points)
        points_list.append(points)
        weights_list.append(weights)
    return np.concatenate(points_list), np.concatenate(weights_list)

def quadtree(x_start, x_end, y_start, y_end, elements, nodes) -> None:
    pass