import matplotlib.pyplot as plt
import numpy as np
import imageio
from _utils import *

def DCT2D(x):
    '''
    Discrete space cosine transform
    x: Input matrix
    '''
    N1, N2 = x.shape
    X = np.zeros((N1, N2))
    n1, n2 = np.mgrid[0:N1, 0:N2]
    for w1 in range(N1):
        for w2 in range(N2):
            l1 = (2/N1)**0.5 if w1 else (1/N1)**0.5
            l2 = (2/N2)**0.5 if w2 else (1/N2)**0.5
            cos1 = np.cos(np.pi*w1*(2*n1 + 1)/(2*N1))
            cos2 = np.cos(np.pi*w2*(2*n2 + 1)/(2*N2))
            X[w1, w2] = l1*l2*np.sum(x*cos1*cos2)
    return X

def iDCT2D(X, shift=True):
    '''
    Inverse discrete space cosine transform
    X: Input spectrum matrix
    '''
    N1, N2 = X.shape
    x = np.zeros((N1, N2))
    k1, k2 = np.mgrid[0:N1, 0:N2]
    l1 = np.ones((N1, N2))*(2/N1)**0.5
    l2 = np.ones((N1, N2))*(2/N2)**0.5
    l1[0] = (1/N1)**0.5; l2[:,0] = (1/N2)**0.5
    for n1 in range(N1):
        for n2 in range(N2):
            cos1 = np.cos(np.pi*k1*(2*n1 + 1)/(2*N1))
            cos2 = np.cos(np.pi*k2*(2*n2 + 1)/(2*N2))
            x[n1, n2] = np.sum(l1*l2*X*cos1*cos2)
    return x

if __name__ == "__main__":
    image = imageio.imread('./sample/cameraman.png')
    s = 4
    image = image[::s, ::s] / 255
    N1, N2 = image.shape
    histogram(image, interval=[0, 1])

    IMAGE = DCT2D(image)
    xX = np.array([image, np.log10(1 + abs(IMAGE))])
    panel(xX, [2, 1], text_color='green',
          texts=['Input image', 'DCT Spectrum'])

    image_ = iDCT2D(IMAGE)
    Xx_ = np.array([np.log10(1 + abs(IMAGE)), image_])
    panel(Xx_, [2, 1], text_color='green',
          texts=['DCT Spectrum', 'Reconstructed image'])

    u, v = np.mgrid[0:N1, 0:N2] / max(N1, N2)
    r = (u ** 2 + v ** 2) ** 0.5
    theta = np.arctan2(v, u)
    H = np.exp(-3 * r ** 2) * (np.cos(4 * 2 * theta) / 2 + 1 / 2)
    image__ = iDCT2D(H * IMAGE)
    Hx__ = np.array([H, abs(image__ * 0.5 + 0.5)])
    panel(Hx__, (2, 1), text_color='green',
          texts=['Filter', 'Filtered image'])


import numpy as np
import matplotlib.pyplot as plt

# Return fitted model parameters to the dataset at datapath for each choice in degrees.
# Input: datapath as a string specifying a .txt file, degrees as a list of positive integers.
# Output: paramFits, a list with the same length as degrees, where paramFits[i] is the list of
# coefficients when fitting a polynomial of d = degrees[i].
def main(datapath, degrees):
    paramFits = []
    # fill in
    # read the input file, assuming it has two columns, where each row is of the form [x y] as
    # in poly.txt.
    # iterate through each n in degrees, calling the feature_matrix and least_squares functions to solve
    # for the model parameters in each case. Append the result to paramFits each time.

    file_path = open( datapath,  "r")
    X = []
    Y = []
    #X = []
    #Y = []
    y_1 = []
    y_2 = []
    y_3 = []
    y_4 = []
    #count = 0
    y_5 = []
    file_data_path = file_path.readlines()

    for i in file_data_path:
        i = i.split(" ")
        l1 = float(i[0])
        l2 = float(i[1])
        X.append(l1)
        Y.append(l2)

    for j in degrees:
        mitrx = feature_matrix(X, j)
        ##matrix
        paramFits.append(least_squares(mitrx, Y))

    xsort = sorted(X)

    for l in sorted(X):
        res1 = paramFits[0][0] * l + paramFits[0][1]
        res2 = paramFits[1][0] * (l ** 2) + paramFits[1][1] * l + paramFits[1][2]
        res3 = paramFits[2][0] * (l ** 3) + paramFits[2][1] * (l ** 2) + paramFits[2][2] * l + paramFits[2][3]
        res4 = paramFits[3][0] * (l ** 4) + paramFits[3][1] * (l ** 3) + paramFits[3][2] * (l ** 2) + paramFits[3][3] * l + paramFits[3][4]
        res5 = paramFits[4][0] * (l ** 5) + paramFits[4][1] * (l ** 4) + paramFits[4][2] * (l ** 3) + paramFits[4][3] * (l ** 2) + paramFits[4][4] * l + paramFits[4][5]
        y_1.append(res1)
        y_2.append(res2)
        y_3.append(res3)
        y_4.append(res4)
        y_5.append(res5)

    #close
    file_path.close()

    plt.scatter(X, Y, color='b', marker='*')

    plt.plot(sorted(X), y_1, color='g', linestyle='-.')
    plt.plot(sorted(X), y_2, color='b', linestyle='-.')
    plt.plot(sorted(X), y_3, color='m', linestyle='-.')
    plt.plot(sorted(X), y_4, color='y', linestyle='-.')
    plt.plot(sorted(X), y_5, color='r', linestyle='-.')

    plt.legend(["data = 1", "data = 2", "data = 3", "data = 4", "data = 5", "Path Data"], loc='upper right')
    plt.ylabel("Y Data")
    plt.xlabel("X Data")

    plt.show()
    ##
    return paramFits

# Return the feature matrix for fitting a polynomial of degree d based on the explanatory variable
# samples in x.
# Input: x as a list of the independent variable samples, and d as an integer.
# Output: X, a list of features for each sample, where X[i][j] corresponds to the jth coefficient
# for the ith sample. Viewed as a matrix, X should have dimension #samples by d+1.
def feature_matrix(x, d):
    # fill in
    # There are several ways to write this function. The most efficient would be a nested list comprehension
    # which for each sample in x calculates x^d, x^(d-1), ..., x^0.
    X_list = []
    ind = 0
    for i in x:
        d1 = d

        X_list.append([])

        while d1 >= 0:
            X_list[ind].append(i**d1)
            d1 -= 1
        # d = 1
        ind += 1
    return X_list

# Return the least squares solution based on the feature matrix X and corresponding target variable samples in y.
# Input: X as a list of features for each sample, and y as a list of target variable samples.
# Output: B, a list of the fitted model parameters based on the least squares solution.
def least_squares(X, y):
    X_array = np.array(X)
    # X_array = list(X)
    Y_array = np.array(y)

    # fill in
    # Use the matrix algebra functions in numpy to solve the least squares equations. This can be done in just one line.

    return (np.linalg.inv(X_array.T @ X_array)) @ (X_array.T @ Y_array)

if __name__ == '__main__':
    datapath = 'poly.txt'
    degrees = [1, 2, 3, 4, 5]
    paramFits = main(datapath, degrees)
    print(paramFits)


import numpy     as np

from typing     import Type, List, NoReturn, Union, Dict

RealNumber    = Union[np.float16, np.float32, np.float64, float];
ComplexNumber = Union[np.complex64, np.complex128];

def cayley_to_upper(z:ComplexNumber) -> ComplexNumber:
    #
    # Compute the image of z via the Cayley map into the uper half plane.
    #
    return 1j*(1.0+z)/(1.0-z);

def cayley_from_upper(z:ComplexNumber) -> ComplexNumber:
    #
    # Compute the image of z via the Cayley map from the uper half plane into the unit disk.
    #
    return (z-1j)/(z+1j);

def moebius_transformation(z:ComplexNumber,a:RealNumber,b:RealNumber,c:RealNumber,d:RealNumber) -> Union[ComplexNumber, NoReturn]:
    #
    # Compute the image of z via the Moebius transformation defined by the real paramters a,b,c,d.
    #
    if a*b-c*d==0 :
        raise ValueError("In order to be a proper Moebius Transformaion, it must be a*b-c*d!=0");
    else:
        return (a*z-b)/(c*z+d);

def moebius_unit_disk(z:ComplexNumber, theta:RealNumber=np.pi, a:ComplexNumber=np.complex(0,0)) -> ComplexNumber:
    #
    # Compute the image of z via the Moebius transformation which is an automorphism of the unit disk.
    #
    # Input:
    #   z       : complex number (most likely, in the unit disk)
    #   theta   : angle of rotation
    #   a       : point of the unit disk such that 0-->a
    #
    # Output:
    #   w       : complex number (in the unit disk)
    #
    return np.exp(1j*theta*np.pi)*(z-a)/(1-np.conjugate(a)*z);

def moebius_upper_half_plane(z:ComplexNumber,a:RealNumber,b:RealNumber,c:RealNumber,d:RealNumber) -> Union[ComplexNumber, NoReturn]:
    #
    # Compute the image of z via the Moebius transformation which is an automorphism of the upper-half plane.
    #
    # Input:
    #   z       : complex number (most likely, in the upper half plane)
    #   a,b,c,d : real parameters such that a*b-c*d>0
    #
    # Output:
    #   w       : complex number (in the upper half plane)
    #
    if not a*b-c*d>0 :
        raise ValueError("In order to be a proper Moebius Transformaion of the upper-half plane, it must be a*b-c*d>0");
    else:
        return (a*z-b)/(c*z+d);

def principal_sqrt(x:RealNumber) -> RealNumber:
    #
    # Compute the principal squareroot of the real number x.
    #
    if np.any(np.angle(x) > 0):
        return np.exp(1j * np.angle(x) / 2) * np.sqrt(np.sqrt(np.real(x)**2 + np.imag(x)**2));
    else:
        return np.exp(1j * (np.angle(x)+2*np.pi) / 2) * np.sqrt(np.sqrt(np.real(x)**2 + np.imag(x)**2));


from __future__ import division
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import time

print("BSUIR: Machine Learning, L1")

def h0x(x_value, theta):
    return theta[0] + theta[1] * x_value

def compute_cost(X, Y, theta):
    m = len(X)
    diff = []
    for i in range(0, m):
        val = pow(h0x(X[i], theta) - Y[i], 2)
        diff.append(val)
    cost = (1 / (2 * m)) * sum(diff)
    return cost

def gradient_descent(X, Y, theta, iterations, alpha):
    """
    From Andrew Ng implementation: without ones vector in X
    """
    m = len(X)
    J = []
    for i in range(iterations):
        val = np.zeros(len(theta))
        for j in range(0, m):
            val[0] += h0x(X[j], theta) - Y[j]
            for k in range(1, len(theta)):
                val[k] += (h0x(X[j], theta) - Y[j]) * X[j]
        for z in range(0, len(theta)):
            theta[z] = theta[z] - (alpha / m) * val[z]
        J.append(compute_cost(X, Y, theta))
    return [theta, J]

def gradient_descent_not_vectorized(X, Y, theta, iterations, alpha):
    """
    For reducing impact of compute_cost_vectorized vs compute_cost
    """
    m = len(X)
    J = []
    for i in range(iterations):
        val = np.zeros(len(theta))
        for j in range(0, m):
            for k in range(0, len(theta)):
                val[k] += (h0x_vectorized(X[j], theta) - Y[j]) * X[j][k]
        for z in range(0, len(theta)):
            theta[z] = theta[z] - (alpha / m) * val[z]
        J.append(compute_cost_vectorized(X, Y, theta))
    return [theta, J]

def h0x_vectorized(X, theta):
    return X.dot(theta)

def compute_cost_vectorized(X, Y, theta):
    # J = (1 / (2 * m)) * (X * theta - y)' * (X * theta - y); % equally (sum(power(X, 2)))
    m = len(X)
    temp = (h0x_vectorized(X, theta) - Y)
    return (1 / (2 * m)) * np.dot(temp.T, temp)[0][0]

def gradient_descent_vectorized(X, Y, theta, iterations, alpha):
    m = len(Y)
    J_history = []
    for i in range(iterations):
        # theta = theta - alpha * (1/m) * (((X*theta) - y)' * X)'; % Vectorized
        h0x = (h0x_vectorized(X, theta) - Y).T
        dt = np.dot(h0x, X).T
        a = alpha * (1 / m) * dt
        theta = theta - a
        J_history.append(compute_cost_vectorized(X, Y, theta))
    return [theta, J_history]

def feature_normalization(X):
    X = X.T
    for i in range(1, len(X)):
        mu = np.mean(X[i])
        s = np.std(X[i], ddof=1)
        X[i] = (X[i] - mu) / s
    return X.T

def normal_eqn(X, Y):
    # theta = pinv(X' * X) * (X' * y); % Vectorized
    return np.dot(np.linalg.inv(np.dot(X.T, X)), np.dot(X.T, Y))

if __name__ == '__main__':
    # 1
    file_path = 'ex1data1.csv'
    data_frames = pd.read_csv(file_path)

    x = data_frames['population']
    y = data_frames['profit']

    x = list(x)  # np.array(x)
    y = list(y)

    # 2
    fig, ax = plt.subplots()
    ax.scatter(x, y)

    plt.show()

    # 3
    theta = [0, 0]
    print('With theta = [0 ; 0]\nCost computed: ', compute_cost(x, y, theta))
    print('Expected cost value (approx) 32.07\n')

    theta = [-1, 2]
    print('\nWith theta = [-1 ; 2]\nCost computed: ', compute_cost(x, y, theta))
    print('Expected cost value (approx) 54.24\n')

    # 4
    iterations = 1500
    alpha = 0.01

    print('Running Gradient Descent ...\n')
    # run gradient descent
    theta = [0, 0]
    [theta, J1] = gradient_descent(x, y, theta, iterations, alpha)

    print('Theta found by gradient descent:', theta)
    print("Cost: ", compute_cost(x, y, theta))
    print('Expected theta values (approx):  -3.6303  1.1664\n\n')

    ax.plot([4, 23], [h0x(0, theta), h0x(23, theta)], 'red')
    plt.show()

    # 5
    u = np.arange(-5, 5, 0.1)
    v = np.arange(-5, 5, 0.1)
    z = np.zeros((len(u), len(v)))
    for i in range(len(u)):
        for j in range(len(u)):
            z[i][j] = compute_cost(x, y, [u[i], v[j]])

    u, v = np.meshgrid(u, v)

    fig = plt.figure()
    ax = fig.gca(projection='3d')
    surf = ax.plot_surface(u, v, z, linewidth=0, antialiased=False)
    plt.show()
    fig, ax = plt.subplots()
    plt.contour(u, v, z, np.logspace(-2, 3, 20))
    plt.show()

    # 6
    file_path = 'ex1data2.csv'
    data = pd.read_csv(file_path)

    # 7-8
    X = data.iloc[:, 0:2]  # read first two columns into X
    Y = data.iloc[:, 2]  # read the third column into y
    m = len(Y)
    ones = np.ones((m, 1))
    X = np.hstack((ones, X))  # [x1, x2] => [1, x1, x2]
    theta = np.zeros((3, 1))
    Y = Y[:, np.newaxis]  # convert to a matrix

    print('With theta = [0; 0; 0]\nCost computed: ', compute_cost_vectorized(X, Y, theta))
    print('Expected cost value (approx) 65591548106.45744\n')

    print('Without normalization: \n')
    # Can not be calculated since numbers are too large
    # print(gradient_descent_vectorized(X, Y, theta, iterations, alpha)[0])
    print('With normalization: \n')
    X = feature_normalization(X)

    start1 = time.time()
    [theta, J1] = gradient_descent_not_vectorized(X, Y, theta, iterations, alpha)
    end1 = time.time()
    print('Time gradient not vectorized: ', end1 - start1, theta)

    start2 = time.time()
    [gdv, J] = gradient_descent_vectorized(X, Y, np.zeros((3, 1)), iterations, alpha)
    end2 = time.time()

    print('Vectorized time: ', end2 - start2)
    print('Solution: ')
    print(gdv)

    # 9

    objects = ('Vectorized', 'Not-Vectorized')
    y_pos = np.arange(len(objects))
    performance = [end2 - start2, end1 - start1]

    plt.bar(y_pos, performance, align='center', alpha=0.5)
    plt.xticks(y_pos, objects)
    plt.ylabel('Time (s)')
    plt.title('Performance')

    plt.show()

    # 10

    year = range(0, iterations)

    plt.plot(year, J, color='orange')
    plt.xlabel('Iterations')
    plt.ylabel('Cost J')
    plt.title('Cost by iterations')
    plt.show()

    # 11

    theta = normal_eqn(X, Y)
    print('Normal equation: \n')
    print(theta)


import numpy as np

def dist(X1,X2):
    """
    Compute the Euclidean distance between each row of X1 and X2
    """
    X1sq = np.sum(np.square(X1),1)
    X2sq = np.sum(np.square(X2),1)
    sqdist = -2.*np.dot(X1, X2.T) + (X1sq[:,None] + X2sq[None,:])
    sqdist = np.clip(sqdist, 0, np.inf)
    return sqdist

def d(x1,x2):
    ''' Compute absolute distance between all elments of two vectors and output outersubtract matrix '''
    return np.abs(np.subtract.outer(x1,x2))

def SE_kernel(X1,X2,theta):
    l = theta[1]
    sigma_f = theta[2]
    if l <=0 or sigma_f <= 0:
        print("Check hyperparameter values!")
    sqdist = dist(X1,X2)
    return sigma_f**2 * np.exp(-0.5*sqdist/(l**2))

def RQ_kernel(X1,X2,theta):
    alpha = 2
    l = theta[1]
    sigma_f = theta[2]
    if l <=0 or sigma_f <= 0:
        print("Check hyperparameter values!")
    sqdist = dist(X1,X2)
    return sigma_f**2 * (1+sqdist/(2*alpha*l**2))**(-alpha)

def camphor_copper_kernel(X1,X2,theta):
    #This hyperparameter vector will work well:  theta=[0.001,0.26,0.1] (given that 0.05 added to lengthscale of RBF for z-variable)
    #OLD hyperparameters that stood out in testing: theta=[0.09,0.2,0.35]
    l = theta[1]
    sigma_f = theta[2]
    if l <=0 or sigma_f <= 0:
        print("Check hyperparameter values!")
    ''' Same lenghtscale l across dimensions '''
    ''' Periodic kernels has same period p=1 since data normalized to [0,1] '''
    p = 1
    kernelX = np.exp((-2*np.square(np.sin(np.pi*d(X1[:,0],X2[:,0])/p)))/l**2)
    kernelY = np.exp((-2*np.square(np.sin(np.pi*d(X1[:,1],X2[:,1])/p)))/l**2)
    kernelZ = np.exp(-0.5*np.square(d(X1[:,2],X2[:,2]))/((l+0.05)**2))
    ''' NOTE: 0.05 added to lengthscale of RBF for z-variable'''
    kernelalpha = np.exp((-2*np.square(np.sin(np.pi*d(X1[:,3],X2[:,3])/p)))/l**2)
    kernelbeta = np.exp((-2*np.square(np.sin(np.pi*d(X1[:,4],X2[:,4])/p)))/l**2)
    kernelgamma = np.exp((-2*np.square(np.sin(np.pi*d(X1[:,5],X2[:,5])/p)))/l**2)
    return sigma_f**2 * kernelX * kernelY * kernelZ * kernelalpha * kernelbeta * kernelgamma


#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import numpy as np
import matplotlib.pyplot as plt

def derivative(a,b,n):
    """derivative(a,b,n)
    Function to create the derivative matrix.  Takes 3 inputs:
        - a = float, starting point of the domain
        - b = float, end point of domain
        - n = number of points created in the domain
    Output: an 'n x n' 2-dimensional numpy array that, when matrix multiplied by a 1-D array,
    returns the approximate derivative at each point."""
    t = np.linspace(a,b,n)
    dx = t[1]-t[0]
    empty = np.zeros(n, dtype = 'float64')
    a,b = np.meshgrid(empty, empty)
    A = a + b
    #Forward Difference
    A[0,0] = -1/dx
    A[0,1] = 1/dx
    #Symmetric Difference
    index = np.arange(1,n-1)
    A[index, index-1] = -1/(2*dx)
    A[index, index+1] = 1/(2*dx)
    #Backward Difference
    A[n-1,n-2] = -1/dx
    A[n-1,n-1] = 1/dx
    return A

def second_derivative(a,b,n):
    """second_derivative(a,b,n)
    Function to create the second derivative matrix.  Takes 3 inputs:
        - a = float, starting point of the domain
        - b = float, end point of domain
        - n = number of points created in the domain
    Output: an 'n x n' 2-dimensional numpy array that, when matrix multiplied by a 1-D array,
    returns the approximate second derivative at each point."""
    t = np.linspace(a,b,n)
    dx = t[1]-t[0]
    empty = np.zeros(n, dtype = 'float64')
    a,b = np.meshgrid(empty, empty)
    A = a + b
    #Forward Difference
    A[0,0] = 1
    A[0,1] = -2
    A[0,2] = 1
    #Symmetric/Forward Difference
    A[1,0] = 2
    A[1,1] = -3
    A[1,3] = 1
    #Symmetric Difference
    index = np.arange(2,n-2)
    A[index, index-2] = 1/2
    A[index, index] = -1
    A[index, index+2] = 1/2
    #Symmetric/Backward Difference
    A[n-2,n-4] = 1
    A[n-2,n-2] = -3
    A[n-2,n-1] = 2
    #Backward Difference
    A[n-1,n-3] = 1
    A[n-1,n-2] = -2
    A[n-1,n-1] = 1
    return A*(1/(2*dx**2))

def f(a,b,n):
    """Returns an array of the square of the input (a float)"""
    t = np.linspace(a,b,n)
    return t**2

def s(a,b,n):
    """Returns an array of the sine of the input (float)"""
    t = np.linspace(a,b,n)
    sin = np.vectorize(np.sin)
    sin = sin(t)
    return sin

def g(a,b,n):
    """Returns an array of the gaussian function"""
    def gauss(x):
        """Returns the Gaussian function dependent on the input float"""
        c = 1/(np.sqrt(2*np.pi))
        gauss = c*np.exp(-x**2/2)
        return gauss
    t = np.linspace(a,b,n)
    gs = np.vectorize(gauss)
    gs = gs(t)
    return gs

def plot_function(a,b,n,f,string):
    """plot_function(a,b,n,f,string)"""
    t = np.linspace(a,b,n)
    deriv = np.dot(derivative(a,b,n),f)
    sec_deriv = np.dot(second_derivative(a,b,n),f)
    plt.plot(t,f,'b', label=string)
    plt.plot(t,deriv,'r', label='Derivative')
    plt.plot(t,sec_deriv,'g', label='Second Derivative')
    plt.title(string)
    plt.legend()
    plt.show()


'''
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace (0, 10, 1000)
y = np. power(x, 2)
plt.plot (x, y)
plt.show()
plt.savefig('plot.png')
'''
'''
import matplotlib.pyplot as plt
plt.plot([-1, -4.5, 16, 23], "ob")

plt.show()
plt.savefig('plot.png')
'''
'''
import matplotlib.pyplot as plt
days = [5,10,15,20]
celcius_values = [24, 16, 16, 23]
plt.plot(days, celcius_values, "ob")
plt.xlabel('Day')
plt.ylabel('Degrees Celsius')
plt.show()
plt.savefig('plot3.png')
'''
'''
import matplotlib.pyplot as plt
days = list(range(1,9))
celsius_min = [19.6, 24.1, 26.7, 28.3, 27.5, 30.5, 32.8, 33.1]
celsius_max = [24.8, 28.9, 31.3, 33.0, 34.9, 35.6, 38.4, 39.2]
plt.xlabel('Day')
plt.ylabel('Degrees Celsius')
plt.plot(days, celsius_min,
         days, celsius_min, "oy",
         days, celsius_max,
         days, celsius_max, "or")
print("The current limits for the axes are:")
print(plt.axis())
print("We set the axes to the following values:")
xmin, xmax, ymin, ymax = 0, 10, 14, 45
print(xmin, xmax, ymin, ymax)
plt.axis([xmin, xmax, ymin, ymax])
plt.show()
plt.savefig('maxAndMin.png')
'''
'''
import numpy as np
import matplotlib.pyplot as plt
X = np.linspace(-2 * np.pi, 2 * np.pi, 50, endpoint=True)
F1 = 3 * np.sin(X)
F2 = np.sin(2*X)
F3 = 0.3 * np.sin(X)
startx, endx = -2 * np.pi - 0.1, 2*np.pi + 0.1
starty, endy = -3.1, 3.1
plt.axis([startx, endx, starty, endy])
plt.plot(X,F1)
plt.plot(X,F2)
plt.plot(X,F3)
plt.plot(X, F1, 'ro')
plt.plot(X, F2, 'bx')
plt.show()
plt.savefig("plot5.png")
'''
'''
import numpy as np
import matplotlib.pyplot as plt

X = np.linspace(0, 2 * np.pi, 50, endpoint=True)
F1 = 3 * np.sin(X)
F2 = np.sin(2*X)
F3 = 0.3 * np.sin(X)
F4 = np.cos(X)
plt.plot(X, F1, color="blue", linewidth=2.5, linestyle="-")
plt.fill_between(X,0,F1, color='blue', alpha=.1)
plt.plot(X, F2, color="red", linewidth=1.5, linestyle="--")
plt.plot(X, F3, color="green", linewidth=2, linestyle=":")
plt.plot(X, F4, color="grey", linewidth=2, linestyle="-.")
plt.show()
plt.savefig('plot6.png')
'''
# subplots

'''
import numpy as np
import matplotlib.pyplot as plt

for i in range(1,7):
    plt.subplot(2,3,i)
    plt.text(0.5, 0.5, str((2, 3, i)),
    fontsize =18, ha= 'center')
plt.show()
plt.savefig('plot7.png')
'''
'''
import numpy as np
import matplotlib.pyplot as plt
numerical_data = np.array([1,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,5,4,6,7,8])
plt.hist(numerical_data)
plt.title("Numbergram")
plt.xlabel("Value")
plt.ylabel("Frequency")
plt.show()
plt.savefig('plot8.png')
'''
'''
import numpy as np
import matplotlib.pyplot as plt

num_data_2 = np.array([-1,-5,-2,-10,-11 ,0,0,0,0,0, 1,2,3,4,5,6,7,8,9])
plt.hist(num_data_2, bins=100,color="#875F9A")
plt.title("Number Bins")
plt.xlabel("Value")
plt.ylabel("Frequency")
plt.show()
plt.savefig('plot9.png')
'''
'''
import matplotlib.pyplot as plt
import numpy as np
mu, sigma = 0, 0.1 # mean and standard deviation
gaussian_numbers = np.random.normal(mu, sigma, 10000)
plt.hist(gaussian_numbers, bins=100)
plt.title("Gaussian Histogram")
plt.xlabel("Value")
plt.ylabel("Frequency")
plt.show()
plt.savefig("plot10.png")
'''
'''
import matplotlib.pyplot as plt
fig =plt.figure(figsize=(6,4))
fig.subplots_adjust(bottom=0.025, left=0.025, top = 0.975, right=0.975)
X = [ (3,3,(1,3)), (3,3,(4,5)), (3,3,(6,9)), (3,3,7),(3,3,8) ]
for nrows, ncols, plot_number in X:
    sub = plt.subplot(nrows, ncols, plot_number)
    sub.set_xticks([])
    sub.set_yticks([])
plt.show()
plt.savefig('plot11.png')
'''
'''
import matplotlib.pyplot as plt
import numpy as np
X = np.linspace(-2 * np.pi, 2 * np.pi, 50, endpoint=True)
mylist1 = 3 * np.sin(X)
mylist2 = np.sin(2*X)
mylist3 = 0.3 * np.sin(X)
mylist4 = 3 * np.cos(X)
mylist5 = np.tan(2*X)
mylist6 = 0.3 * np.cos(X)

mylist=[mylist1, mylist2,mylist3,mylist4, mylist5,mylist6]
mycol=['r','b','y','k','c','m']
fig = plt.figure(figsize=(6, 4))
fig.subplots_adjust(bottom=0.025, left=0.025, top=0.975, right=0.975)

Y = [(3, 3, (1, 3)), (3, 3, (4, 5)), (3, 3, (6, 9)), (3, 3, 7), (3, 3, 8)]
i=0
for nrows, ncols, plot_number in Y:
    sub = plt.subplot(nrows, ncols, plot_number)
    sub.set_xticks([])
    sub.set_yticks([])
    plt.plot(mylist[i],mycol[i])
    i+=1
plt.show()
plt.savefig('plot12.png')
'''
'''
names = ['group_a', 'group_b', 'group_c']
values = [1, 10, 100]

import matplotlib.pyplot as plt
import numpy as np

plt.figure(figsize=(9, 3))
plt.subplot(131)
plt.bar(names, values)
plt.subplot(132)
plt.scatter(names, values)
plt.subplot(133)
plt.plot(names, values)
plt.suptitle('Categorical Plotting')
plt.show()
plt.savefig('plot13.png')
'''
'''
import matplotlib.pyplot as plt
import numpy as np

def f(t):
    return np.exp(-t) * np.cos(2*np.pi*t)

t1 = np.arange(0.0, 5.0, 0.1)
t2 = np.arange(0.0, 5.0, 0.02)
plt.figure()    #optional
plt.subplot(211)
plt.plot(t1, f(t1), 'bo', t2, f(t2), 'k')
plt.subplot(212)
plt.plot(t2, np.cos(2*np.pi*t2), 'r--')
plt.show()
plt.savefig('plot14.png')
'''
'''
import matplotlib.pyplot as plt
import numpy as np
labels = ['G1', 'G2', 'G3', 'G4', 'G5']
USA_means = [20, 34, 30, 35, 27]
Russia_means = [25, 32, 34, 20, 25]
China_means = [25, 32, 34, 20, 25]
x = np.arange(len(labels)) # the label locations
width = 0.25 # the width of the bars
fig , ax = plt.subplots()
rects1 = ax.bar(x - width, USA_means, width, label='USA', color="blue",edgecolor="black")
rects2 = ax.bar(x , Russia_means, width, label='Russia',color="green",edgecolor="black")
rects2 = ax.bar(x + width, China_means, width, label='China',color="red",edgecolor="black")
# Add some text for labels, title and custom x-axis tick labels, etc.
ax.set_ylabel('Scores')
ax.set_title('Scores by Countries')
ax.set_xticks(x)
ax.set_xticklabels(labels)
ax.legend()
plt.show()
plt.savefig('plot15.png')
# '''
import matplotlib.pyplot as plt
import numpy as np
last_week_cups = (20, 35, 30, 35, 27)
this_week_cups = (25, 32, 34, 20, 25)
names = ['Mary', 'Paul', 'Billy', 'Franka', 'Stephan']
fig = plt.figure()
left, bottom, width, height = 0.1, 0.3, 0.8, 0.6
ax = fig.add_axes([left, bottom, width, height])
width = 0.35
ticks = np.arange(len(names))
ax.bar(ticks, last_week_cups, width, label='Last week')
ax.bar(ticks + width, this_week_cups, width, label='This week')
ax.set_ylabel('Cups of Coffee')
ax.set_title('Coffee Consummation')
ax.set_xticks(ticks + width/2)
ax.set_xticklabels(names)
ax.legend(loc='best')
plt.show()
plt.savefig('plot17.png')


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Sat Nov 25 16:13:40 2017

@author: ratnadeepb
@License: MIT
"""

'''
Square matrices (n x n) are also vector space where an inner product can
be defined.

In M2,2 space let:
    A = [[a11, a21], [a12, a22]]
    B = [[b11, b21], [b12, b22]]

Inner Product in M2,2:
    <A, B> = a11*b11 + a21*b21 + a12*b12 + a22*b22
'''

import numpy as np
import sys

# Dot product of A and B
def mat_dot(A, B):
    try:
        A = np.array(A)
        B = np.array(B)
    except:
        sys.exit("Not compatible")

    bl = A.ndim == B.ndim
    if not bl:
        sys.exit("Not compatible")
    s = 0
    for i in range(A.ndim):
        for a, b in zip(A[i], B[i]):
            s += a * b
    return s

# Norm of a Matrix
def mat_norm(A):
    return np.sqrt(mat_dot(A, A))

# Angle between two matrices
def mat_angle(A, B, op="radians"):
    if op not in ["radians", "degrees"]:
        sys.exit("At this time we only handle radians and degrees")
    if op == "degrees":
        return (mat_dot(A, B) / (mat_norm(A) * mat_norm(B))) * (180 / np.pi)
    else:
        return (mat_dot(A, B) / (mat_norm(A) * mat_norm(B)))

if __name__ == "__main__":
    A = [[11, -10], [4, 3]]
    B = [[8, 7], [9, 16]]

    print("Dot product of these matrices is:", mat_dot(A, B))
    print("Angle betwee the matrices is:", mat_angle(A, B, "degrees"))


import numpy as np

# First method.... #np.array(p_object)
mylist = [1,2,3]
x = np.array(mylist)
print x

# Direct Method
y = np.array([2,4,5])
print y

#2 dimensional array
q = np.array([[136,53,64],[133,564,23]])
print q
print q.shape  #printing the dimension of the ARRAY

#arange(start,stop,step,dtype) funtion
z = np.arange(0,30,2) # arange(START, END, STEP size)
print z

#reshape(a,newshape,order) function
z = z.reshape(3,5)     #reshape(ROW,COLUMN)
print z

#linspace(start,stop, HOW MANY Number we want in return) function
w = np.linspace(0,4,8)
print w

#resize(a, newshape) fuction OR give it matrix dimention you desire like below...
w.resize((3,4))
print w

#resize() another example        #Changing the dimensions of the array.
a = np.array([[0,1],[2,3]])
a = np.resize(a,(2,6))
print a

b = np.ones((3,2))   #creates 1's matrix of 3by2
print b

c = np.zeros((5,2))  #creates 0's matrix of 5by2
print c

d = np.eye((5))  #creates identity matrix of 5by5
print d

e = np.diag(x)  #creates a diagonal matrix of the given NUMPY ARRAY
print e

f = np.array([1,3,2]*4)  #passing a repeated list..
print f

#repeat(a, repeats, axis) function
g = np.repeat([1,2,3],3)  #repeating each element of the list to the given times..
print g

#We can also combine arrays to create new ones. Let's create a two by three array of ones and stack it vertically
# with itself, multiplied by 2. And here's the same thing, but stacking horizontally.

h = np.ones([2,3], int)
#print h                      #adding a matrix of 2 vertically below to the exsisting array
h = np.vstack([h, 2*h])
print h

i = np.ones([2,3], int)
i = np.hstack([i, 2*i])      #adding a matrix of 2 horizontally side to the exsisting array
print i

#Mathematical OPERATIONS, element wise...

j = np.array([1,2,3],float)
k = np.array([4,5,6], float)

print j+k   #Addition

print j*k   #Multiplication

print j**2  #j to the power of 2..

print k/j  #Divison

print k.dot(j) # here  j = [1,2,3] and k = [4,5,6] => [(4.1)+(5.2)+(6.3)] = 14  DOT product

#Let's create a new array using a previous array y and its squared values. The shape of this array is two by three.
l = np.array([j, j**2])
print l
print l.shape

#We can also take the transpose of an array using the T method, which swaps the rows and columns.
# The shape of the transposed array is three by two.
m = l.T
print m
print m.shape

print m.dtype #Type of the Array

m = m.astype('i')   #typecasting of the Array...
print m.dtype

#Some more inbuilt fuctions of NumPy
o = np.array([46,45,21,53,75])   #New Array...

print o.sum()       #SUM of the Array
print o.max()       #Maximum number/value in the Array
print o.min()       #Minimum number/value in the Array
print o.mean()      #Mean value of the Array
print o.std()       #Standard Diviation of the Array
print o.argmax()    #To find the index value of the Maximum Value in the Array... Indexing starts with 0th address..
print o.argmin()    #To find the index value of the Minimum Value in the Array...  Indexing starts with 0th address..

#Indexing/Slicing

p = np.arange(0,13) #an array of squared value of 0 to 12..
print p**2

q = np.arange(13)**2 #Another method
print q

#We can use bracket notation to get the value at a particular index, and the colon notation to get a range.
print q[0], q[2], q[0:7]

print q[1:5]
print q[-4:]

#And here, we're starting fifth from the end, to the beginning of the array,
# and counting backwards by the difference of two.
print q[-5::-2]

#Let's see how this extends to a two-dimensional array. First, let's make a two dimensional array, 0 to 35.
# We can get a specific value by using the comma notation.
# Here's the value at the second row and second column
r = np.arange(36)
r.resize(6,6)
print r
print r[2,2]  #element at 2nd Row- 2nd Column

print r[3, 3:6] #Slice of the 3rd Row.. from column 3 to 6...

print r[:2 , :-2] #Slicing(getting) first 2 rows except the last 2 columns..

print r[-1 , ::2] #Getting every alternate 2nd element from the last Row

# [] operator for Conditional indexing and assignment
r[r>20] #finding all the elements greater than 20 and repalcing them with 30
r[r>20] = 30  #
print r

#Copying element in NumPy
r2 = r[:3, :3] #printing first 3 rows and columns from the orignal array 'r'....
print r2

r2[:] = 0 #Assigning 0's to the sliced array...
print r2

print r   #In the original array that sliced part also becomes 0...

# USE ...r.copy() function is used if we want to create copy of the original array
# and doesn't change the original array..

r_copy = r.copy() #creating the copy of the original array...
print r_copy

r_copy[:] =10
print r_copy
print r         #Notice that the element of the original array has not changed...

#Iterating over Arrays...
#Creating a 4by3 matrix of random numbers from 0to9...
test = np.random.randint(0, 10, (4,3))
print test

for row in test: #iterating by ROW through array...
    print (row)

for i in range(len(test)): #iterating by ROW INDEX...
    print (test [i])

for i, row in enumerate(test): #Enumerate Gives the ROW and the Index of the ROW...
    print('row', i, 'is', row)

test2 = test**2
print test2

#ZIP() fucntion to iterate between 2 Matrix simultaneously...
for i, j in zip(test, test2):
    print(i, '+', j, '=', i+j)

#Using resize and reshape functions to find different patterns of matrix...
s = np.arange(0,36)
#print s
s.resize(6,6)
print s

print s.reshape(36)[::7]
print s[2:4,2:4]


import numpy as np
import random

#import the data. Note that the txt file has to be in the same folder
data = np.loadtxt('cdata.txt')

#function for calculating standard deviation by using eq (5) from lab 2
def std5(val):
    mean = (1./len(val))*np.sum(val) # 1st pass to find mean
    sumOfDifference = 0
    for i in val: # 2nd pass to find difference
        sumOfDifference += (i - mean) ** 2
    ans5 = np.sqrt((1./(len(val)-1)) * sumOfDifference)
    return ans5

#function for calculating standard deviation by using eq (6) from lab 2
def std6(val):
    sumForMean = 0 #this is for calculating the mean
    sumOfSquare = 0 #this is for calculating the sum of sqaured element
    for i in val: #only pass through data set
        sumForMean += i
        sumOfSquare += (i ** 2)
        if sumOfSquare < sumForMean: #check to prevent sqrt negative number
            print('you are wrong, but the code will still run')
            sumOfSquare += sumForMean #will make it larger than sum for mean but std will be wrong
    ans6 = np.sqrt((1./(len(val)-1)) * (sumOfSquare - len(val)*((sumForMean/len(val)) ** 2)))
    return ans6

#using numpy.std method as our reference answer
correctAnswer = np.std(data, ddof=1)

#using (x-y)/y to calculate relative error from lab 2
print('The relative error of method 5: ', np.abs(std5(data) - correctAnswer)/correctAnswer)
print('The relative error of method 6: ', np.abs(std6(data) - correctAnswer)/correctAnswer)

#constants for generating set 1
mean1, sigma1, n1 = (0., 1., 2000)
#constants for generating set 2
mean2, sigma2, n2 = (1.e7, 1., 2000)

#set 1 data
data1= np.random.normal(mean1, sigma1, n1)
#set 2 data
data2= np.random.normal(mean2, sigma2, n2)

#using numpy.std method to calculate respective answers
correctAnswer1 = np.std(data1, ddof=1)
correctAnswer2 = np.std(data2, ddof=1)

#using same method as above to calculate relative error for set 1
print('The relative error of method 5 of set 1: ', np.abs(std5(data1) - correctAnswer1)/correctAnswer1)
print('The relative error of method 6 of set 1: ', np.abs(std6(data1) - correctAnswer1)/correctAnswer1)

#using same method as above to calculate relative error for set 2
print('The relative error of method 5 of set 2: ', np.abs(std5(data2) - correctAnswer2)/correctAnswer2)
print('The relative error of method 6 of set 2: ', np.abs(std6(data2) - correctAnswer2)/correctAnswer2)

def std6Improved(val):
    sumForMean = 0 #same setup as above
    sumOfSquare = 0
    adjust = random.choice(val) #randomly pick an element to shift data set close to 0 and 1
    for i in val:
        sumForMean += i - adjust #shifting each data point
        sumOfSquare += ((i - adjust) ** 2) #shifting each data point
        if sumOfSquare < 0:
            print('you are wrong, but the code will still run')
            np.abs(sumOfSquare)
    #no need to account the adjustment into std calculation because shifting data set does not effect value of std
    ans6 = np.sqrt((1./(len(val)-1))*(sumOfSquare - (sumForMean ** 2)/len(val)))
    return ans6

#using same method as above to calculate relative error for improved eq 6
print('The relative error of method 6 with improved method: ', np.abs(std6Improved(data) - correctAnswer)/correctAnswer)


import numpy as np
import matplotlib.pyplot as plt
from Generaldistribution import Distribution

class Gaussian (Distribution):

    """
    Gaussian distribution class for calculating and
    visualizing a Gaussian distribution.

    Attributes:
        mean (float) represent the mean values of the distribution
        stdev (float) represent standard deviation of the distribution
        dat_list (list of float) extracted from the text file
    """
    def __init__(self, mu = 0 , sigma = 1):

        Distribution.__init__(self, mu , sigma)

    def calcualte_mean(self):
        """
        Function calculated the mean of the dataset

        Args:
            None
        Returns:
            float: mean of the dataset
        """

        avg = float(sum(self.data / len(self.data)))
        self.mean = avg

        return self.mean

    def calculate_stdev(self, sample=True):
        """
        Function calculates the standard deviation of the dataset

        Args:
            sample (bool) whether the data is population or a sample
        Returns:
            float: standard deviation of the data set
        """

        if sample:
            n = len(self.data) - 1
        else:
            n = len(self.data)
        mean = self.calcualte_mean()
        sigma = 0

        for data in self.data:
            sigma += (data - mean)**2
        sigma = np.sqrt(sigma/n)

        self.stdev = sigma

        return self.stdev

     def plot_histogram(self):
        """
        Function to output a histogram of the instance variable data
        using matplotlib.pyplot library

        Args:
            None
        Returns:
            None
        """

        plt.hist(self.data)
        plt.title("Histogram of data")
        plt.xlabel('data')
        pltylable('count')

    def pdf(self , x):
        """
        Probability Density Function calculates the Gaussian distribution

        Args:
            x (float): point for calculating the probability density function

        Returns:
            float: probability density function as output
        """

        prob = (1. /(self.stdev * np.sqrt(2*math.pi))) * np.exp(-.5*((x - self.mean) / self.stdev)**2)
        return prob

    def plot_histogram_pdf(self, n_spaces = 50):

        """Function to plot the normalized histogram of the data and a plot of the
        probability density function along the same range

        Args:
            n_spaces (int): number of data points

        Returns:
            list: x values for the pdf plot
            list: y values for the pdf plot

        """

        mu = self.mean
        sigma = self.stdev

        min_range = min(self.data)
        max_range = max(self.data)

         # calculates the interval between x values
        interval = 1.0 * (max_range - min_range) / n_spaces

        x = []
        y = []

        # calculate the x values to visualize
        for i in range(n_spaces):
            tmp = min_range + interval*i
            x.append(tmp)
            y.append(self.pdf(tmp))

        # make the plots
        fig, axes = plt.subplots(2,sharex=True)
        fig.subplots_adjust(hspace=.5)
        axes[0].hist(self.data, density=True)
        axes[0].set_title('Normed Histogram of Data')
        axes[0].set_ylabel('Density')

        axes[1].plot(x, y)
        axes[1].set_title('Normal Distribution for \n Sample Mean and Sample Standard Deviation')
        axes[0].set_ylabel('Density')
        plt.show()

        return x, y

    def __add__(self, other):

        """Function to add together two Gaussian distributions

        Args:
            other (Gaussian): Gaussian instance

        Returns:
            Gaussian: Gaussian distribution

        """

        result = Gaussian()
        result.mean = self.mean + other.mean
        result.stdev = math.sqrt(self.stdev ** 2 + other.stdev ** 2)

        return result

    def __repr__(self):

        """Function to output the characteristics of the Gaussian instance

        Args:
            None

        Returns:
            string: characteristics of the Gaussian

        """

        return "mean {}, standard deviation {}".format(self.mean, self.stdev)

if __name__ == "__main__":
    # initialize two gaussian distributions
    gaussian_one = Gaussian(25, 3)
    gaussian_two = Gaussian(30, 2)

    # initialize a third gaussian distribution reading in a data efile
    gaussian_three = Gaussian()
    gaussian_three.read_data_file('numbers.txt')
    gaussian_three.calculate_mean()
    gaussian_three.calculate_stdev()

    # print out the mean and standard deviations
    print(gaussian_one.mean)
    print(gaussian_two.mean)

    print(gaussian_one.stdev)
    print(gaussian_two.stdev)

    print(gaussian_three.mean)
    print(gaussian_three.stdev)

    # plot histogram of gaussian three
    gaussian_three.plot_histogram_pdf()

    # add gaussian_one and gaussian_two together
    gaussian_one + gaussian_two


import sympy as sp
x, y, z = sp.symbols('x y z')
sp.init_printing()

# Here we simply give the input needed as The Following:
# ( Xi ) is the initial point
# ( Xminus1 ) is the point before the Initial toint
# ( Fx ) is the equation of the function
# ( n ) is the number of Iterations needed

Xi = 0

Xminus1 = -0.5

Fx = sp.cos(x)+2*sp.sin(x)+x**2

n = 5

print("\n____________________________________")
print(" Iteration |    Xi     ||   Error "
      "\n------------------------------------")
Error = 0 ; XiPlus1 = 0
for i in range (n):
    FxVal = float(Fx.subs(x, Xi).evalf())
    FminusVal = float(Fx.subs(x, Xminus1).evalf())
    XiPlus1 = Xi - FxVal*(Xi - Xminus1)/(FxVal - FminusVal)
    Error = abs(((XiPlus1 - Xi)/XiPlus1) *100)
    print("      {0:1d}    | {1:.6f} || {2:.6f}%\n"
          .format(i+1, Xi, Error))
    Xminus1 = Xi ; Xi = XiPlus1

# In The End The Program Prints The Output as well as The Percentage of error between Iterations


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Sun Jan 27 13:31:03 2019

@author: alankar
"""
import numpy as np
import matplotlib.pyplot as plt

def f(x,c):
    return 1-np.exp(-c*x)

def fpi(func,x0, tol, verbose, *args): #x0 is initial guess
    x_old = x0
    x_new = x0+10  #some arbitrary initialization
    counter = 0
    while(np.abs(x_new-x_old)>=tol):
        counter += 1
        x_old = x_new
        x_new = func(x_old,*args)
    if (verbose==True): print ("Converged in %d steps"%counter)
    return x_new

c = 2
print('Fixed Point Iteration:')
print('Solution for x=1-exp(-%.2f x) is: x=%f\n'%(c,fpi(f,0.5,1e-6,True,c)))

c = np.arange(0,3,0.01)
x = np.zeros(len(c))
for i in range(len(c)):
    x[i] = fpi(f,0.5,1e-6,False,c[i])

plt.plot(c,x)
plt.grid()
plt.xlabel('c',size=18)
plt.ylabel('x',size=18)
#plt.savefig('tansition.jpg')

def steffensen_fpi(func,x0, tol, verbose, *args):
    x_old = x0
    x_new = x0+10  #some arbitrary initialization
    counter = 0
    while(np.abs(x_new-x_old)>=tol):
        counter +=1
        x_old = x_new
        g = (func(func(x_old,*args),*args)-func(x_old,*args))/(func(x_old,*args)-x_old)-1
        x_new = x_old-(func(x_old,*args)-x_old)/g
    if (verbose==True): print ("Converged in %d steps"%counter)
    return x_new

c = 2
print('Accelerated Fixed Point Iteration')
print('Solution for x=1-exp(-%.2f x) is: x=%f'%(c,steffensen_fpi(f,0.5,1e-6,True,c)))

plt.show()

"""
Output

Fixed Point Iteration:
Converged in 15 steps
Solution for x=1-exp(-2.00 x) is: x=0.796813

Accelerated Fixed Point Iteration
Converged in 4 steps
Solution for x=1-exp(-2.00 x) is: x=0.796812
"""


from pyccel.decorators import types

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

@types( int )
def sum_natural_numbers( n ):
    x = 0
    for i in range( 1, n+1 ):
        x += i
    return x

# ...
@types( int )
def factorial( n ):
    x = 1
    for i in range( 2, n+1 ):
        x *= i
    return x

# ...
@types( int )
def fibonacci( n ):
    x = 0
    y = 1
    for i in range( n ):
        z = x+y
        x = y
        y = z
    return x

# ...
@types( int )
def double_loop( n ):
    x = 0
    for i in range( 3, 10 ):
        x += 1
        y  = n*x
        for j in range( 4, 15 ):
            z = x-y
    return z

# ...
@types( 'int[:,:](order=C)' )
def double_loop_on_2d_array_C( z ):

    from numpy import shape

    s = shape( z )
    m = s[0]
    n = s[1]

    for i in range( m ):
        for j in range( n ):
            z[i,j] = i-j

# ...
@types( 'int[:,:](order=F)' )
def double_loop_on_2d_array_F( z ):

    from numpy import shape

    s = shape( z )
    m = s[0]
    n = s[1]

    for i in range( m ):
        for j in range( n ):
            z[i,j] = i-j

# ...
@types( 'int[:,:](order=C)' )
def product_loop_on_2d_array_C( z ):

    from numpy     import shape
    from itertools import product

    s = shape( z )
    m = s[0]
    n = s[1]

    x = [i for i in range(m)]
    y = [j for j in range(n)]

    for i,j in product( x, y ):
        z[i,j] = i-j

# ...
@types( 'int[:,:](order=F)' )
def product_loop_on_2d_array_F( z ):

    from numpy     import shape
    from itertools import product

    s = shape( z )
    m = s[0]
    n = s[1]

    x = [i for i in range(m)]
    y = [j for j in range(n)]

    for i,j in product( x, y ):
        z[i,j] = i-j

# ...
@types( 'int[:]' )
def map_on_1d_array( z ):

    @types( int )
    def f( x ):
        return x+5

    res = 0
    for v in map( f, z ):
        res *= v

    return res

# ...
@types( 'int[:]' )
def enumerate_on_1d_array( z ):

    res = 0
    for i,v in enumerate( z ):
        res += v*i

    return res

# ...
@types( int )
def zip_prod( m ):

    x = [  i for i in range(m)]
    y = [2*j for j in range(m)]

    res = 0
    for i1,i2 in zip( x, y ):
        res += i1*i2

    return res


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Interpolation module

We have analytic interpolation using SymPy and numerical interpolation
that returns an evaluable function.
"""
import sympy as sym

#%% Analytic interpolation
def basis_lagrange(x_data, var, i):
    """
    Compute the basis interpolant polynomial corresponding to the
    i-th  point given in x_data

    Parameters
    ----------
    x_data : list
        List with x coordinates for the interpolation.
    var : SymPy symbol
        Variable to be used for the interpolation
    i : int
        Point number for the interpolant.

    Returns
    -------
    poly : SymPy expression
        Interpolant polynomial in variable var.

    """
    prod = 1
    num = len(x_data)
    for j in range(num):
        if j == i:
            continue
        prod = prod * (var - x_data[j])/(x_data[i] - x_data[j])
    return sym.simplify(prod)

def lagrange_poly(x_data, y_data, var):
    """
    Compute the interpolant polynomial corresponding to the
    points given in (x_data, y_data)

    Parameters
    ----------
    x_data : list
        List with x coordinates for the interpolation.
    y_data : list
        List with y coordinates for the interpolation.
    var : SymPy symbol
        Variable to be used for the interpolation

    Returns
    -------
    poly : SymPy expression
        Interpolant polynomial in variable var.

    """
    poly = 0
    num = len(x_data)
    for i in range(num):
        poly = poly + y_data[i] * basis_lagrange(x_data, var, i)
    return poly

#%% Numerical interpolation
def lagrange(x_data, y_data):
    """
    Compute the interpolant polynomial corresponding to the
    points given in (x_data, y_data)

    Parameters
    ----------
    x_data : list
        List with x coordinates for the interpolation.
    y_data : list
        List with y coordinates for the interpolation.

    Returns
    -------
    fun_poly : Python function
        Interpolant polynomial as evaluable function.

    """
    def fun_poly(x):
        num = len(x_data)
        acu = 0
        for i in range(num):
            prod = 1
            for j in range(num):
                if i == j:
                    continue
                prod = prod * (x - x_data[j])/(x_data[i] - x_data[j])
            acu = acu + prod * y_data[i]
        return acu
    return fun_poly


import numpy as np

A = np.array([[1,2],[3,4]])

# matrix inverse, the inverse function is in the linalg module

a_inv = np.linalg.inv(A)
print(a_inv)

# verifying the Inverse by multiplying with A resulting in I

print(a_inv.dot(A))

# matrix determinant

det_a = np.linalg.det(A)
print(det_a)

# diagonal of a matrix

diag_a = np.diagonal(A) # gives diagonal elements in a vector
print(diag_a)

# to make a 2D array of diagonal

diag_2d = np.diag(diag_a)
print(diag_2d)

# remember that if you pass a 2D array to diag it will
# return diagonal elements in a 1D array

## if you pass a 1D array to diag, it will return a 2D array where the
## off diagonals are zero and the original array takes up the diagonal

#Outer Product

a = np.array([1,2])

b = np.array([3,4])

out_prod = np.outer(a,b)
print(out_prod)

# we can also do inner product like below, inner product is the same
# as outer product

inn_prod = np.inner(a,b)
print(inn_prod)

# Trace of a Matrix , is nothing but the sum of the elements of diagonal

#method1
print(A)
print(np.diag(A).sum())

#method2
print(np.trace(A))

x = np.random.randn(100,3) # by convention this is 100 observations
                           # 3 features
#to calculate covariance
cov = np.cov(x)
print(cov)

print(cov.shape) # this results in (100,100) which is wrong
                 # since we have only 3 features , it has to be (3,3)

print(np.cov(np.transpose(x)).shape) # this looks ugly

# there are two methods to calculate eigenvalues and eigenvectors

# np.eig(A) and np.eigh(A) ; eigh is used for symmetric matrices
# and hermitian matrices

# symmetric means a == transpose(a)

# hermitian means a == conjugate transpose ( a )

# since covariance is a symmetric matrix , we are using eigh

eig_cov = np.linalg.eigh(cov) # gives out a tuple containing
                              # in the first eigenvalues
                              # in the second corresponding eigenvectors
print(eig_cov)


from __future__ import print_function
from numpy import *
import numpy as np
from scipy.special import comb

# evaluates cubic bezier at t, return point
def q(ctrlPoly, t):
    return (1.0-t)**3 * ctrlPoly[0] + 3*(1.0-t)**2 * t * ctrlPoly[1] + 3*(1.0-t)* t**2 * ctrlPoly[2] + t**3 * ctrlPoly[3]

# evaluates cubic bezier first derivative at t, return point
def qprime(ctrlPoly, t):
    return 3*(1.0-t)**2 * (ctrlPoly[1]-ctrlPoly[0]) + 6*(1.0-t) * t * (ctrlPoly[2]-ctrlPoly[1]) + 3*t**2 * (ctrlPoly[3]-ctrlPoly[2])

# evaluates cubic bezier second derivative at t, return point
def qprimeprime(ctrlPoly, t):
    return 6*(1.0-t) * (ctrlPoly[2]-2*ctrlPoly[1]+ctrlPoly[0]) + 6*(t) * (ctrlPoly[3]-2*ctrlPoly[2]+ctrlPoly[1])

def bernstein_poly(i, n, t):
    """
     The Bernstein polynomial of n, i as a function of t
    """

    return comb(n, i) * ( t**(n-i) ) * (1 - t)**i

def bezier_curve(points, nTimes=1000):
    """
       Given a set of control points, return the
       bezier curve defined by the control points.

       points should be a list of lists, or list of tuples
       such as [ [1,1],
                 [2,3],
                 [4,5], ..[Xn, Yn] ]
        nTimes is the number of time steps, defaults to 1000

        See http://processingjs.nihongoresources.com/bezierinfo/
    """

    nPoints = len(points)
    xPoints = np.array([p[0] for p in points])
    yPoints = np.array([p[1] for p in points])

    t = np.linspace(0.0, 1.0, nTimes)

    polynomial_array = np.array([ bernstein_poly(i, nPoints-1, t) for i in range(0, nPoints)   ])

    xvals = np.dot(xPoints, polynomial_array)
    yvals = np.dot(yPoints, polynomial_array)

    return xvals, yvals

if __name__ == "__main__":
    from matplotlib import pyplot as plt

    nPoints = 4
    points = np.random.rand(nPoints,2)*200
    xpoints = [p[0] for p in points]
    ypoints = [p[1] for p in points]

    xvals, yvals = bezier_curve(points, nTimes=1000)
    plt.plot(xvals, yvals)
    plt.plot(xpoints, ypoints, "ro")
    for nr in range(len(points)):
        plt.text(points[nr][0], points[nr][1], nr)

    plt.show()


from __future__ import annotations  # See: https://stackoverflow.com/a/33533514

import numpy as np

class Vector(object):

    def __init__(self, coordinates: iter, tolerance: float = 1e-10) -> None:
        try:
            if not coordinates:
                raise ValueError

            self.coordinates = np.array(coordinates, dtype=np.float64)
            self.dimension = len(coordinates)
            self.tolerance = tolerance

        except ValueError:
            raise ValueError('The coordinates must be nonempty')

        except TypeError:
            raise TypeError('The coordinates must be an iterable')

    def __str__(self) -> str:
        return f'Vector: {self.coordinates}'

    def __eq__(self, other: Vector) -> bool:
        assert isinstance(other, Vector)

        return self.coordinates == other.coordinates

    def __add__(self, other: Vector) -> Vector:
        assert isinstance(other, Vector)
        assert self.dimension == other.dimension

        coord = [i + j for i, j in zip(self.coordinates, other.coordinates)]

        return Vector(coordinates=coord)

    def __sub__(self, other: Vector) -> Vector:
        assert isinstance(other, Vector)
        assert self.dimension == other.dimension

        coord = [i - j for i, j in zip(self.coordinates, other.coordinates)]

        return Vector(coordinates=coord)

    def scalar_mul(self, scalar: (int, float)) -> Vector:
        assert isinstance(scalar, (int, float))

        coordinates = [coord * scalar for coord in self.coordinates]

        return Vector(coordinates=coordinates)

    @property
    def magnitude(self) -> float:
        return np.sqrt(np.sum(np.power(self.coordinates, 2)))

    @property
    def direction(self) -> Vector:
        try:
            unit_coord = [coord / self.magnitude for coord in self.coordinates]
            return Vector(coordinates=unit_coord)

        except ZeroDivisionError:
            raise Exception('Cannot normalize the zero vector')

    def dot_product(self, other):
        assert isinstance(other, Vector)

        return sum(
            [i * j for i, j in zip(self.coordinates, other.coordinates)]
        )

    def get_angle_with(self, other: Vector, in_degrees: bool = False) -> float:
        assert isinstance(other, Vector)

        unit_dot_prod = self.direction.dot_product(other.direction)
        angle = np.arccos(unit_dot_prod)

        if in_degrees:
            angle *= 180/np.pi

        return angle

    @property
    def is_null_vector(self) -> bool:
        return self.magnitude < self.tolerance

    def is_parallel_to(self, other: Vector) -> bool:
        assert isinstance(other, Vector)

        return (
            self.is_null_vector or
            other.is_null_vector or
            self.get_angle_with(other=other) in (0, np.pi)
        )

    def is_orthogonal_to(self, other: Vector) -> bool:
        assert isinstance(other, Vector)

        return np.abs(self.dot_product(other=other)) < self.tolerance

    def component_parallel_to(self, basis):
        assert isinstance(basis, Vector)

        if self.is_null_vector or basis.is_null_vector:
            raise Exception('Cannot project from/to null vector')

        u = basis.direction
        weight = self.dot_product(other=u)

        return u.scalar_mul(scalar=weight)

    def component_orthogonal_to(self, basis: Vector) -> Vector:
        assert isinstance(basis, Vector)

        if self.is_null_vector or basis.is_null_vector:
            raise Exception('Cannot project from/to null vector')

        projection = self.component_parallel_to(basis=basis)
        rejection = self - projection

        return rejection

    @staticmethod
    def pad_third_dim(vector: Vector) -> Vector:
        if vector.dimension == 2:
            new_coordinates = vector.coordinates + (0, )
            vector = Vector(coordinates=new_coordinates)

        if vector.dimension != 3:
            raise Exception(f'Wrong number of dimensions for vector {vector}')

        return vector

    def cross_product(self, other: Vector) -> Vector:
        assert isinstance(other, Vector)

        x1, y1, z1 = self.pad_third_dim(vector=self).coordinates
        x2, y2, z2 = self.pad_third_dim(vector=other).coordinates

        return Vector([
            y1*z2 - y2*z1,
            -(x1*z2 - x2*z1),
            x1*y2 - x2*y1
        ])

    def get_area_parallelogram(self, other: Vector) -> float:
        assert isinstance(other, Vector)

        return self.cross_product(other=other).magnitude

    def get_area_triangle(self, other: Vector) -> float:
        assert isinstance(other, Vector)

        return self.get_area_parallelogram(other=other) / 2

if __name__ == '__main__':
    # Test case 1
    vector1 = Vector(coordinates=(8.218, -9.341))
    vector2 = Vector(coordinates=(-1.129, 2.111))
    print(vector1 + vector2)

    # Test case 2
    vector3 = Vector(coordinates=(7.119, 8.215))
    vector4 = Vector(coordinates=(-8.223, 0.878))
    print(vector3 - vector4)

    # Test case 3
    vector5 = Vector(coordinates=(1.671, -1.012, -0.318))
    print(vector5.scalar_mul(scalar=7.41))

    # Test case 4
    vector6 = Vector(coordinates=(-0.221, 7.437))
    print(vector6.magnitude)

    vector7 = Vector(coordinates=(8.813, -1.331, 6.247))
    print(vector7.magnitude)

    # Test case 5
    vector8 = Vector(coordinates=(5.581, -2.136))
    print(vector8.direction)

    vector9 = Vector(coordinates=(1.996, 3.108, -4.554))
    print(vector9.direction)

    # Test case 6
    vector10 = Vector(coordinates=(7.887, 4.138))
    vector11 = Vector(coordinates=(-8.802, 6.776))
    print(vector10.dot_product(other=vector11))

    vector12 = Vector(coordinates=(-5.955, -4.904, -1.874))
    vector13 = Vector(coordinates=(-4.496, -8.755, 7.103))
    print(vector12.dot_product(other=vector13))

    # Test case 7
    vector14 = Vector(coordinates=(3.183, -7.627))
    vector15 = Vector(coordinates=(-2.668, 5.319))
    print(vector14.get_angle_with(other=vector15))

    vector16 = Vector(coordinates=(7.35, 0.221, 5.188))
    vector17 = Vector(coordinates=(2.751, 8.259, 3.985))
    print(vector16.get_angle_with(other=vector17, in_degrees=True))

    # Test case 8
    vector18 = Vector(coordinates=(-7.579, -7.88))
    vector19 = Vector(coordinates=(22.737, 23.64))
    print('Are parallel?', vector18.is_parallel_to(other=vector19))
    print('Are orthogonal?', vector18.is_orthogonal_to(other=vector19))

    vector20 = Vector(coordinates=(-2.029, 9.97, 4.172))
    vector21 = Vector(coordinates=(-9.231, -6.639, -7.245))
    print('Are parallel?', vector20.is_parallel_to(other=vector21))
    print('Are orthogonal?', vector20.is_orthogonal_to(other=vector21))

    vector22 = Vector(coordinates=(-2.328, -7.284, -1.214))
    vector23 = Vector(coordinates=(-1.821, 1.072, -2.94))
    print('Are parallel?', vector22.is_parallel_to(other=vector23))
    print('Are orthogonal?', vector22.is_orthogonal_to(other=vector23))

    vector24 = Vector(coordinates=(2.118, 4.827))
    vector25 = Vector(coordinates=(0, 0))
    print('Are parallel?', vector24.is_parallel_to(other=vector25))
    print('Are orthogonal?', vector24.is_orthogonal_to(other=vector25))

    # Test case 9
    vector26 = Vector(coordinates=(3.039, 1.879))
    vector27 = Vector(coordinates=(0.825, 2.036))
    print(vector26.component_parallel_to(basis=vector27))

    # Test case 10
    vector28 = Vector(coordinates=(-9.88, -3.264, -8.159))
    vector29 = Vector(coordinates=(-2.155, -9.353, -9.473))
    print(vector28.component_orthogonal_to(basis=vector29))

    # Test case 11
    vector30 = Vector(coordinates=(3.009, -6.172, 3.692, -2.51))
    vector31 = Vector(coordinates=(6.404, -9.144, 2.759, 8.718))
    v_parallel = vector30.component_parallel_to(basis=vector31)
    v_orthogonal = vector30.component_orthogonal_to(basis=vector31)
    v = v_parallel + v_orthogonal
    print(v_parallel, v_orthogonal)

    # Test case 12
    vector32 = Vector(coordinates=(8.462, 7.893, -8.187))
    vector33 = Vector(coordinates=(6.984, -5.975, 4.778))
    print(vector32.cross_product(other=vector33))

    # Test case 13
    vector34 = Vector(coordinates=(-8.987, -9.838, 5.031))
    vector35 = Vector(coordinates=(-4.268, -1.861, -8.866))
    print(vector34.get_area_parallelogram(other=vector35))

    # Test case 14
    vector36 = Vector(coordinates=(1.5, 9.547, 3.691))
    vector37 = Vector(coordinates=(-6.007, 0.124, 5.772))
    print(vector36.get_area_triangle(other=vector37))


import numpy as np
'''
# min-max
a = np.array([[3,7,5],[8,4,3],[2,4,9]])
print("input array: \n", a)
print("min: ", np.amin(a))
print("min axis =0 ", np.amin(a, axis=0))
print("min axis =1 ", np.amin(a, axis=1))
print("max: ", np.amax(a))
print("max axis =0 ", np.amax(a, axis=0))
print("max axis =1 ", np.amax(a, axis=1))
'''
# TODO: tpt and percentile methods
'''
# median & mean
a = np.array([[30,65,70],[80,95,10],[50,90,60]])
print("input array: \n", a)
print("median: ", np.median(a))
print("median axis =0 ", np.median(a, axis=0))
print("median axis =1 ", np.median(a, axis=1))
print("mean: ", np.mean(a))
print("mean axis =0 ", np.mean(a, axis=0))
print("mean axis =1 ", np.mean(a, axis=1))
'''
'''
# weighted average

a = np.array([1,2,3,4])
wts = np.array([4,3,2,1])
print ("weighted avg: ", np.average(a,weights = wts))
'''

# std deviation
# Formula: std = sqrt(mean(abs(x - x.mean())**2))
print ("standard deviation: ", np.std([1,2,3,4]))

# variance
# Formula mean(abs(x - x.mean())**2)
print("variance: ", np.var([1,2,3,4]))


import numpy as np
import statsmodels.api as sm
from scipy.stats import linregress

from framework.symbol import *
from framework.stats_basic import *
from framework.base import *

# def lr(y):
#     X = np.arange(y.shape[0])
#     X = sm.add_constant(X)
#     model = sm.OLS(y, X).fit()
#     pred = model.predict(X)
#     pred = name(pd.Series(pred, y.index), y.name + " fit")
#     return pred

def lr(y, X=None, print_r2=False, ret_coef=False):
    if X is None:
        X = np.arange(len(y))
        if is_series(y):
            xs = y.index
        else:
            xs = X
        name_ = get_name(y) + " fit"
    else:
        xs = X
        name_ = "fit"

    X = sm.add_constant(X)
    model = sm.OLS(y, X).fit()
    if print_r2:
        print(f"R^2: {model.rsquared:.2f}")
    if ret_coef:
        return model.params

    pred = model.predict(X)
    pred = name(pd.Series(pred, xs), name_)
    pred = pred.sort_index()
    return pred

def lr_predict(y, X):
    params = lr(*sync(y, X), ret_coef=True, print_r2=True)
    print(params)
    return params.iloc[0] + params.iloc[1] * X

def lr_beta(y, X=None, pvalue=False):
    if X is None:
        X = np.arange(y.shape[0])
        X = sm.add_constant(X)
        model = sm.OLS(y, X).fit()
        if pvalue:
            return model.params[1], model.pvalues[1]
        return model.params[1]
    else:
        X = sm.add_constant(X)
        model = sm.OLS(y, X).fit()
        if pvalue:
            return model.params[1], model.pvalues[1]
        return model.params[1]

def lrret_beta(y, X, freq=None, pvalue=False):
    y = get(y, freq=freq)
    X = get(X, freq=freq)
    y = logret(y)
    X = logret(X)
    y, X = sync(y, X)
    return lr_beta(y, X, pvalue=pvalue)

def lr_expanding(s, freq="W"):
    def last_lr(x):
        if len(x) == 0:
            return None
        x = lr(x)
        if len(x) == 0:
            return None
        return x.iloc[-1]
    df = pd.DataFrame(s)
    elr = df.asfreq(freq).expanding().apply(last_lr)
    return name(elr.iloc[:, 0], f"{get_name(s)} elr")

def lr_rolling(s, n=52, freq="W"):
    def last_lr(x):
        if len(x) == 0:
            return None
        x = lr(x)
        if len(x) == 0:
            return None
        return x.iloc[-1]
    df = pd.DataFrame(s)
    elr = df.asfreq(freq).rolling(n).apply(last_lr)
    return name(elr.iloc[:, 0], f"{get_name(s)} rlr")

def corr(a, b, p_value=False):
    a, b = sync(a, b)
    res = linregress(a, b)
    if p_value:
        return (res.rvalue, res.pvalue)
    return res.rvalue

import numpy,from scipy,import statsmodels


import pandas as pd
import numpy as np

def params_mle(data):
    '''
    Fit data to a Guassian distribution with Maximum Likelihood Estimation (MLE)

    Parameters
    ----------
    data : ndarray, list, dict, or DataFrame
        Data to be tested for normality

    Returns
    -------
    mle_params : Dataframe
        Dataframe with the first row containing the estimated means and
        the second row containing the estimated variance. The columns
        present the original variables in the data

    Examples
    --------
    iris_data = pd.DataFrame({"length": [1,2,3,4], "width": [5,6,7,8])
    make_qqplot(iris_data)
    '''

    ## PREPROCESSING
    ## =============

    # Set default column names as indexes
    try: # 2D data
        n_var = data.shape[1]
        var_names = range(n_var)
    except: # 1D data
        var_names = [0]

    # Address different input types
    if isinstance(data, pd.DataFrame):
        var_names = list(data)
        data = np.array(data)

    elif isinstance(data, pd.Series):
        if data.name != None:
            var_names = [data.name]
        data = np.array(data)

    elif isinstance(data, list):
        var_names = range(len(data))
        data = np.transpose(np.array(data))

    n_obs = data.shape[0]

    ## Calculations
    ## =============

    # Calculate mu estimates
    mu = np.sum(np.array(data), axis = 0)/n_obs

    # Calculate sigma estimates
    variance = np.sum((data - mu)**2, axis = 0)/n_obs
    sigma = variance**(1/2)

    ## Return results
    ## ==============
    mle_params = pd.DataFrame(np.vstack((mu, variance)), index = ["Mean", "Variance"], columns = var_names)
    return(mle_params)


from __future__ import division

import math
import numpy as np
from typing import Optional

def build_sinusoidal_positional_embedding(
    num_embeddings: int,
    embedding_dim: int,
    padding_idx: Optional[int] = None,
    dtype=np.float32
):
    """
    Build sinusoidal embeddings
    """
    half_dim = embedding_dim // 2
    emb = math.log(10000) / (half_dim - 1)
    emb = np.exp(-emb * np.arange(half_dim, dtype=dtype))
    emb = np.arange(num_embeddings, dtype=dtype)[:, None] * emb[None, :]
    emb = np.concatenate([np.sin(emb), np.cos(emb)], axis=1)
    emb = np.reshape(emb, [num_embeddings, -1])
    if embedding_dim % 2 == 1:
        # zero pad
        emb = np.concatenate(
            [emb, np.zeros(shape=[num_embeddings, 1], dtype=dtype)],
            axis=1
        )
    if padding_idx is not None:
        emb[padding_idx, :] = 0
    return emb


'''
How many defaults might we expect?
100xp

Let's say a bank made 100 mortgage loans. It is possible that anywhere between
0 and 100 of the loans will be defaulted upon. You would like to know the probability
of getting a given number of defaults, given that the probability of a default is
p = 0.05. To investigate this, you will do a simulation. You will perform 100 Bernoulli
trials using the perform_bernoulli_trials() function you wrote in the previous exercise
and record how many defaults we get. Here, a success is a default. (Remember that the
word "success" just means that the Bernoulli trial evaluates to True, i.e., did the loan
recipient default?) You will do this for another 100 Bernoulli trials. And again and again
until we have tried it 1000 times. Then, you will plot a histogram describing the probability
of the number of defaults.

Instructions
-Seed the random number generator to 42.
-Initialize n_defaults, an empty array, using np.empty(). It should contain 1000 entries,
since we are doing 1000 simulations.
-Write a for loop with 1000 iterations to compute the number of defaults per 100 loans using
the perform_bernoulli_trials() function. It accepts two arguments: the number of trials n -
in this case 100 - and the probability of success p - in this case the probability of a default,
which is 0.05. On each iteration of the loop store the result in an entry of n_defaults.
-Plot a histogram of n_defaults. Include the normed=True keyword argument so that the height of
the bars of the histogram indicate the probability.
-Show your plot.
'''
import numpy as np
import matplotlib.pyplot as plt

def perform_bernoulli_trials(n, p):
    """Perform n Bernoulli trials with success probability p
    and return number of successes."""
    # Initialize number of successes: n_success
    n_success = 0

    # Perform trials
    for i in range(n):
        # Choose random number between zero and one: random_number
        random_number = np.random.random()

        # If less than p, it's a success so add one to n_success
        if random_number < p:
            n_success += 1

    return n_success

# Seed random number generator
np.random.seed(42)

# Initialize the number of defaults: n_defaults
n_defaults = np.empty(1000)

# Compute the number of defaults
for i in range(1000):
    n_defaults[i] = perform_bernoulli_trials(100, 0.05)

# Plot the histogram with default number of bins; label your axes
_ = plt.hist(n_defaults, normed=True)
_ = plt.xlabel('number of defaults out of 100 loans')
_ = plt.ylabel('probability')

# Show the plot
plt.show()


import numpy as np

def EulerMethod(f, xi, yi, xmax, h):
    result = {}

    n = int((xmax - xi)/h) + 1

    x = np.linspace(xi, xmax, n)
    y = yi

    result[0] = {'x': x[0], 'y': y}

    for i in range(1, n):
        y = y + f(x[i-1], y) * h
        result[i] = {'x': x[i],'y': y}
    return result

def HeunsMethod(f_xy, xi, yi, h, xmax, its):
    n = int((xmax - xi)/h) + 1

    x = np.linspace(xi, xmax, n)
    y = yi

    result = {}
    result[0] = {'x': x[0], 'y': y}

    for i in range(1, n):
        y_p = y + f_xy(x[i-1], y) * h
        for _ in range(its):
            y_p = y + h * (f_xy(x[i-1], y) + f_xy(x[i], y_p))/2
        y = y_p
        result[i] = {'x': x[i],'y': y}
    return result

def MidPointMethod(f_xy, xi, yi, h, xmax):

    n = int((xmax - xi)/h) + 1

    x = np.linspace(xi, xmax, n)
    y = yi

    result = {}
    result[0] = {'x': x[0], 'y': y}

    for i in range(1, n):
        y_1b2 = y + f_xy(x[i-1], y) * h/2
        y = y + f_xy((x[i] + x[i-1])/2, y_1b2) * h
        result[i] = {'x': x[i],'y': y}
    return result

def Butcher_Tableau(method=None):
    if method == None:
        Meth_list = [   'Forward Euler',
                        'Explicit Midpoint',
                        'Ralston',
                        'Kutta-3rd',
                        'Classic-4th'   ]
        return Meth_list
    elif method == 'Forward Euler':
        C =  [  0   ]
        A = [[  0   ]]
        B =  [  1   ]
    elif method == 'Explicit Midpoint':
        C =  [  0,      0.5 ]
        A = [[  0,      0   ],
             [  0.5,    0   ]]
        B =  [  0 ,     1   ]
    elif method == 'Ralston':
        C =  [  0,      2/3 ]
        A = [[  0,      0   ],
             [  2/3,    0   ]]
        B =  [  1/4,    3/4 ]
    elif method == 'Kutta-3rd':
        C =  [  0,      1/2,    1   ]
        A = [[  0,      0,      0   ],
             [  1/2,    0,      0   ],
             [  -1,     2,      0   ]]
        B =  [  1/6,    2/3,    1/6 ]
    elif method == 'Classic-4th':
        C =  [  0,      1/2,    1/2,    1   ]
        A = [[  0,      0,      0,      0   ],
             [  1/2,    0,      0,      0   ],
             [  0,      1/2,    0,      0   ],
             [  0,      0,      1,      0   ]]
        B =  [  1/6,    1/3,    1/3,    1/6 ]

    return {'s':    len(C),
            'C':    np.array(C),
            'A':    np.array(A),
            'B':    np.array(B)}

def RungeKutta_General(F :list, xi :float, yi :list, h :float, xmax :float, Bt ):
    itr = int((xmax - xi)/h) + 1
    x = np.linspace(xi, xmax, itr)

    result = {}
    yn = yi
    var = len(yn)
    result[xi] = yn.copy()

    hk = np.zeros((var,Bt['s']))

    for n in range(itr - 1):
        xn = x[n]

        hk.fill(0)

        # k_i
        for i in range(Bt['s']):
            xt = xn + Bt['C'][i] * h
            yt = yn.copy()

            yt += hk.dot(Bt['A'][i])
            for m in range(var):
                hk[m, i] = h * F[m](xt, *yt)

        # y_{n+1}
        for i in range(var):
            yn[i] += np.array(Bt['B']).dot(hk[i])
        result[x[n+1]] = yn.copy()
    return result


import numpy as np

def hypergeometric(N, K):
    """
    This function compute the pmf of the Hypergeometric(N, K, n)
    for each possible value of n (i.e. n in {0,1,2,...,N})

    In the context where an urn with N marbles contains K white marbles,
    this function outputs an array P of shape (N+1,K+1) where
    P[i, j] correspond to the probabillity that with i samples without
    replacement we select j white marbles.

    Parameters
    ----------
    N : int
        The number of marbles in the urn
    K : int
        The number of white marbles in the urn

    Returns
    -------
    P : numpy.ndarray
        P[i, j] is the probability that with i samples without replacement
        we select j white marbles. P.shape is (N+1, K+1)

    Notes
    -----
    This is equivalent to
    np.array([scipy.stats.hypergeom(N, K, n).pmf(range(0,K+1)) for n in range(N+1)])
    but faster, if only a row is needed (e.g. P[i]) than using scipy is faster.

    This algorithm uses the following recursive formula
    P[i, j] = P[i-1, j]*(1-Q[i-1, j]) + P[i-1, j-1]*Q[i-1, j-1]
    with Q[i, j] = (K-j)/(N-i) the probability of sampling a white marble
    given that i marbles where sampled from which j were white
    """
    P = np.zeros((N+1, K+1))
    P[0,0] = 1 #P[0, j] = 1 if j==0 else 0
    P[N,K] = 1 #P[N, j] = 1 if j==K else 0

    #To compute Q We don't need to bother with
    #value > 1 since they will be multiplied by 0
    Q = (K-np.arange(K+1))[None]/(N-np.arange(N))[:,None]

    for n in range(1,N):
        P[n] = P[n-1]*(1-Q[n-1])
        P[n,1:] += P[n-1,:-1]*Q[n-1,:-1]

    return P

def superdupergeometric(N, K):
    """
    This is the scenario where we have an urn with N marbles
    and K of them are white. We sample from the urn without replacement
    until we obtain k white marbles. The function gives the probability
    that we need n samples to obtain k white marbles.

    Parameters
    ----------
    N : int
        The number of marbles in the urn
    K : int
        The number of white marbles in the urn

    Returns
    -------
    SP : numpy.ndarray
        SP[i, j] is the probability that it requires i samples without replacement
        to select j white marbles. SP.shape is (N+1, K+1)

    Notes
    -----
    Probabibly related to the negative hypergeometric distribution

    This uses the hypergeometric (hence the name)
    SP[i, j] = P[i-1, j-1]*Q[i-1, j-1]
    where P = hypergeometric(N, K) and with
    Q[i, j] = (K-j)/(N-i) the probability of sampling a white marble
    given that i marbles where sampled from which j were white
    """

    P = hypergeometric(N, K)
    SP = np.zeros((N+1, K+1))
    SP[0, 0] = 1 #we only need 0 sample to get 0 white marbles

    #To compute Q We don't need to bother with
    #value > 1 since they will be multiplied by 0
    Q = (K-np.arange(K+1))[None]/(N-np.arange(N))[:,None]

    for n in range(1,N+1):
        SP[n,1:] += P[n-1,:-1]*Q[n-1,:-1]

    return SP

def superdupergeometric_expectations(N, K):
    """
    This is the scenario where we have an urn with N marbles
    and K of them are white. We sample from the urn without replacement
    until we obtain k white marbles. The function gives the  expected
    number of samples n requires to get k white marbles (for each k).

    Parameters
    ----------
    N : int
        The number of marbles in the urn
    K : int
        The number of white marbles in the urn

    Returns
    -------
    ESP : numpy.ndarray
        ESP[k] is the expected number of samples without replacement
        requires to get k white marbles. ESP.shape is (K+1,)

    Notes
    -----
    This is equivalent (but way faster) to (SP*np.arange(N+1)[:,None]).sum(axis=0)
    where SP = superdupergeometric(N, K)
    """
    ESP = (N+1)/(K+1)*np.arange(K+1)
    return ESP


import numpy as np
import scipy.linalg as la
import time

def read_matrix_dimension():
    print("Enter dimension of matrix A(m >= n):", end=' ')
    try:
        m, n = (int(i) for i in input().strip().split())
        if m < n:
            print("Number of rows cannot be less than number of columns in LSP problem!")
            raise Exception()
    except Exception:
        print("Invalid input for matrix dimension. Try again by entering two space-separated integers:")
        return read_matrix_dimension()
    return m, n

def read_matrix(m, n):
    A = np.zeros((m, n), dtype=np.complex)
    print("Enter rows of the matrix A(separate elements of each row with space):")
    try:
        for i in range(m):
            A[i, :] = np.array([j for j in input().strip().split()]).astype(np.complex)
    except Exception as e:
        raise e
        print(e)
        print("Invalid input for matrix A. Please try again.")
        return read_matrix(m, n)
    return A

def read_vector(m):
    b = np.zeros(m, dtype=np.complex)
    print("Enter elements of the vector b of size {}(separate them with space):".format(m))
    try:
        b[:] = np.array([j for j in input().strip().split()]).astype(np.complex)
    except Exception:
        print("Invalid input for vector b. Please try again!")
        return read_vector(m)
    return b

def solve_lsp_using_svd(A, b, n, r):
    u, sigma, vt = la.svd(A)
    bbar = np.transpose(np.conj(u)).dot(b)
    y = np.zeros(n, dtype=np.complex)
    y[:r] = bbar[:r] / sigma[:r]
    if len(y) == 1:
        y = float(y)
    v = np.transpose(np.conj(vt))
    x = v.dot(y)
    remainder = la.norm(bbar[r:]) if r < n else 0
    return x.reshape((n,)), remainder

def solve_lsp_using_qr(A, b, n, r):
    q, R, p = la.qr(A, pivoting=True)
    bbar = np.transpose(np.conj(q)).dot(b)
    y = np.zeros(n, dtype=np.complex)
    y[:r] = la.solve(R[:r, :r], bbar[:r])
    x = y[p]
    remainder = la.norm(bbar[r:]) if r < n else 0
    return x.reshape((n,)), remainder

def lsp_solver(func, A, b, n, r, method):
    start = time.time()
    x, remainder = func(A, b, n, r)
    svd_time = time.time() - start
    print("Solved LSP using {} in {} ms. Here's the results:".format(method, np.round(svd_time * 1000, decimals=2)))
    print("x:", x)
    print("remainder:", remainder)

def __main__():
    m, n = read_matrix_dimension()
    A = read_matrix(m, n)
    b = read_vector(m)
    rank_A = np.linalg.matrix_rank(A)
    lsp_solver(solve_lsp_using_svd, A, b, n, rank_A, 'SVD')
    lsp_solver(solve_lsp_using_qr, A, b, n, rank_A, 'QR')

__main__()


from functools import partial
# import numpy as np

# solve equation with newton method
def newton(func, derFunc, initialValue, delta, epsilon, iterN):
    v = func(*[initialValue])
    print("k ", " x ", " f(x)")
    print(0, initialValue, v)
    if abs(v) < epsilon:
        return initialValue, v
    x0 = initialValue
    for i in range(0, iterN):
        x1 = x0 - v / derFunc(*[x0])
        v = func(*[x1])
        print(i + 1, x1, v)
        if abs(x1 - x0) < delta or abs(v) < epsilon:
            return x1, v
        else:
            x0 = x1
    return x1, v

# solve equation with bisection method
def bisection(func, a, b, delta, epsilon, iterN):
    mid = (a + b) * 0.5
    v = func(*[mid])
    print("k ", " x ", " f(x)")
    print(0, mid, v)
    if abs(v) < epsilon:
        return mid, v
    x0 = a
    x1 = b
    for i in range(0, iterN):
        f0 = func(*[x0])
        f1 = func(*[x1])
        if sign(f0) * sign(v) < 0:
            x1 = mid
        if sign(v) * sign(f1) < 0:
            x0 = mid
        mid = 0.5 * (x0 + x1)
        v = func(*[mid])
        print(i + 1, mid, v)
        if abs(x1 - x0) < delta or abs(v) < epsilon:
            return mid, v
    return mid, v

def sign(x):
    return -1 if x < 0 else 1 if x > 0 else 0

# a sum of a geometric progression with a1 = q = x
def sumFunc(sum, n, x):
    if x == 0 or x == 1:
        return n * x - sum
    return x * (1 - pow(x, n)) / (1 - x) - sum

# the derivative function of the sum function
def derFunc(n, x):
    if x == 0:
        return 1
    if x == 1:
        return (1 + n) * n / 2
    return (1 - (n + 1) * pow(x, n) + n * pow(x, n + 1)) / pow((1 - x), 2)

def main():
    n = 360
    sum = 75000 / 425.84
    iterN = 50
    initialValue = 0.5
    delta = 1e-6
    epsilon = 1e-12
    f = partial(sumFunc, sum, n)
    df = partial(derFunc, n)
    # newton(f, df, initialValue, delta, epsilon, iterN)
    bisection(f, 0, 1, epsilon, epsilon, 2 * iterN)

main()


from sigmoid import sigmoid
import numpy as np

def sigmoidGradient(z):
#SIGMOIDGRADIENT returns the gradient of the sigmoid function
#evaluated at z
#   g = SIGMOIDGRADIENT(z) computes the gradient of the sigmoid function
#   evaluated at z. This should work regardless if z is a matrix or a
#   vector. In particular, if z is a vector or matrix, you should return
#   the gradient for each element.

# The value g should be correctly computed by your code below.
    g = 0

# ====================== YOUR CODE HERE ======================
# Instructions: Compute the gradient of the sigmoid function evaluated at
#               each value of z (z can be a matrix, vector or scalar).
    try:
        if len(z.shape) == 2:
            g = np.multiply(sigmoid(z), np.transpose(np.ones((z.shape[0], z.shape[1]))-sigmoid(z)))
        elif len(z.shape) == 1:
            g = np.multiply(sigmoid(z), np.transpose(np.ones((z.shape[0]))-sigmoid(z)))
    except AttributeError:
        g = sigmoid(z) * (1 - sigmoid(z))

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

    return g


import numpy as np
import pandas as pd
import scipy.optimize as op
import matplotlib.pyplot as plt

# read data
data = pd.read_csv('ex2data1.txt', header=None)

# show imported data details
# print('data = \n', data.head(10))
# print('**************************************')
# print('data.describe = \n', data.describe())
# print('**************************************')

# draw data
y = data.iloc[:, -1]

fig, ax = plt.subplots(figsize= (8,5))
plt.scatter(data.loc[y == 1, 0], data.loc[y == 1, 1], marker= '+', c= 'black',
            linewidths= 3, s= 100, label= 'Admitted')

plt.scatter(data.loc[y == 0, 0], data.loc[y == 0,1], marker= 'o', c= 'yellow',
            linewidths= 1, edgecolors= 'k', s= 80, label= 'Not admitted')

ax.set(title= 'Training Dataset', xlabel= 'Exam 1 score', ylabel= 'Exam 2 score')
ax.legend(loc= 1, shadow= True, borderpad= 1)
plt.show()

# Setup the data matrix appropriately, and add ones for the intercept term
data.insert(0, -1 , 1)
# print('\nnew data = \n', data.head(10))
# print('\n**************************************')

# separate X (training data) from y (target variable)
cols = data.shape[1]
X = data.iloc[:, 0 : cols-1]
y = data.iloc[:, cols-1 : cols]

# print('\nX data = \n', X.head(10))
# print('\n**************************************')
# print('\ny data = \n', y.head(10))
# print('\n**************************************')

# Convert data from data frames to numpy matrices
X = np.matrix(X.values)
y = np.matrix(y.values)

# Initialize fitting parameters
m, n = X.shape
initial_theta = np.zeros((n, 1))

# print('X \n', X)
# print('\nX.shape = ', X.shape)
# print('\n**************************************')
# print('y \n', y)
# print('\ny.shape = ', y.shape)
# print('\n**************************************')
# print('initial_theta \n', initial_theta)
# print('\ninitial_theta.shape = ', initial_theta.shape)
# print('\n**************************************')

# Compute and display initial cost and gradient
def sigmoid(z):
    return 1 / (1 + np.exp(-z))

nums = np.arange(-10, 10, 0.5)

fig, ax = plt.subplots(figsize= (8,5))
ax.plot(nums, sigmoid(nums), 'r')
ax.grid()
plt.show()

def costFunction(theta, X, y):
    m = len(y)
    theta = theta.reshape((X.shape[1],1))

    h = sigmoid(X * theta)
    J = ((y.T * np.log(h)) + ((1 - y).T * np.log(1 - h))) / (-m)
    return J

def Gradient(theta, X, y):
    theta = theta.reshape((X.shape[1],1))
    return ((X.T * (sigmoid(X * theta) - y)) / len(y))

cost = costFunction(initial_theta, X, y)
grad = Gradient(initial_theta, X, y)

print('\nCost at initial theta (zeros): %.3f' %cost)
print('Expected cost (approx): 0.693')
print('\n**************************************')

print('\nGradient at initial theta (zeros): ')
for i in grad.tolist(): print(' %.4f' %i[0])
print('Expected gradients (approx):\n -0.1000\n -12.0092\n -11.2628')
print('\n**************************************')

# Compute and display cost and gradient with non-zero theta
test_theta = np.array([[-24], [0.2], [0.2]])
cost = costFunction(test_theta, X, y)
grad = Gradient(test_theta, X, y)

print('\nCost at test theta: %.3f' %cost)
print('Expected cost (approx): 0.218')
print('\n**************************************')

print('\nGradient at test theta: ')
for i in grad.tolist(): print(' %.3f' %i[0])
print('Expected gradients (approx):\n 0.043\n 2.566\n 2.647')
print('\n**************************************')

# Run fminunc to obtain the optimal theta
# This function will return theta and the cost
Result = op.fmin_tnc(func= costFunction,
                     x0= initial_theta,
                     args= (X, y),
                     fprime= Gradient,
                     disp= False)

theta = Result[0].reshape((X.shape[1], 1))

print('\nNew Theta After Optimization = ')
for i in range(theta.shape[0]): print(' %.3f' %theta[i, 0])
print('Expected theta (approx):')
print(' -25.161\n 0.206\n 0.201')
print('\n**************************************')

newCost = costFunction(Result[0], X, y)
print('\nNew Cost After Optimization = %.3f' %newCost)
print('Expected cost (approx): 0.203')
print('\n**************************************')

# Plot Boundary
# Plot Data
fig, ax = plt.subplots(figsize= (8,5))
plt.scatter(data.loc[y == 1, 0], data.loc[y == 1, 1], marker= '+', c= 'black',
            linewidths= 3, s= 100, label= 'Admitted')

plt.scatter(data.loc[y == 0, 0], data.loc[y == 0,1], marker= 'o', c= 'yellow',
            linewidths= 1, edgecolors= 'k', s= 80, label= 'Not admitted')

# Only need 2 points to define a line, so choose two endpoints
plot_x = np.array([np.min(X[:,1])-2, np.max(X[:,2])+2])

# Calculate the decision boundary line
theta = np.array(Result[0]).reshape((X.shape[1], 1))
plot_y = np.multiply(((-1.) / theta[2]) , (np.multiply(theta[1], plot_x) + theta[0]))

# print('x =\n', plot_x)
# print('y =\n', plot_y)

# Plot, and adjust axes for better viewing
plt.plot(plot_x, plot_y, linewidth= 2, label= 'Decision Boundary')

ax.set(title= 'Training Dataset with Decision Boundary', xlabel= 'Exam 1 score', ylabel= 'Exam 2 score',
        xlim= (28, 101), ylim= (21, 101))
ax.legend(loc= 1, shadow= True)
plt.show()

# Predict and Accuracies
def predict(theta, X):
    # PREDICT Predict whether the label is 0 or 1 using learned logistic
    # regression parameters theta
    p = np.zeros((X.shape[0],1))

    for i in range(X.shape[0]):
        x = X[i, :].T
        if sigmoid(theta.T * x) >= 0.5 :
            p[i] = 1
        else:
            p[i] = 0

    return p

prob = sigmoid(np.matrix([1, 45, 85]) * theta)

print('\nFor a student with scores 45 and 85, we predict an admission probability of %.3f' %prob[0])
print('Expected value: 0.775 +/- 0.002')
print('\n**************************************')

# Compute accuracy on our training set
p = predict(theta, X)

print('Train Accuracy: %% %.1f' %(np.mean(p == y) * 100))
print('Expected accuracy (approx): % 89.0')
print('\n**************************************')


import numpy as np

# We create a 4 x 5 ndarray that contains integers from 0 to 19
X = np.arange(20).reshape(4, 5)

# We print X
print()
print('X = \n', X)
print()

# We select all the elements that are in the 2nd through
# 4th rows and in the 3rd to 5th columns

Z = X[1:4,2:5]

# We print Z
print('Z = \n', Z)

# We can select the sanem elements as above using method 2

W = X[1:, 2:]

# We print W
print()
print('W = \n', W)

# We select all the elements that are in the 1st through
# 3rd rows and in the 3rd to 4th columns

Y = X[:3, 2:5]

# We print Y
print()
print('Y = \n', Y)

# We select all the elements in the 3rd row
v = X[2,:]

# We print v
print()
print('v = ', v)

# We select all the elements in the 3rd column
q = X[:,2]

# We print q
print()
print('q = ', q)

# We select all the elements in the 3rd column but return a rank 2 ndarray
R = X[:,2:3]

# We print R
print()
print('R = \n', R)

## Copying Concept, View Stuffs

# We create a 4 x 5 ndarray that contains integers from 0 to 19
X = np.arange(20).reshape(4, 5)

# We print X
print()
print('X = \n', X)
print()

# We select all the elements that are in the 2nd through 4th rows and in the 3rd to 4th columns
Z = X[1:4,2:5]

# We print Z
print()
print('Z = \n', Z)
print()

# We change the last element in Z to 555
Z[2,2] = 555

# We print X
print()
print('X = \n', X)
print()

# We create a 4 x 5 ndarray that contains integers from 0 to 19
X = np.arange(20).reshape(4, 5)

# We print X
print()
print('X = \n', X)
print()

# create a copy of the slice using the np.copy() function
Z = np.copy(X[1:4,2:5])

#  create a copy of the slice using the copy as a method
W = X[1:4,2:5].copy()

# We change the last element in Z to 555
Z[2,2] = 555

# We change the last element in W to 444
W[2,2] = 444

# We print X
print()
print('X = \n', X)

# We print Z
print()
print('Z = \n', Z)

# We print W
print()
print('W = \n', W)

# It is often useful to use one ndarray to make slices,
# select, or change elements in another ndarray.
# Let's see some examples:

# We create a 4 x 5 ndarray that contains integers from 0 to 19
X = np.arange(20).reshape(4, 5)

# We create a rank 1 ndarray that will serve as indices to select elements from X
indices = np.array([1,3])

# We print X
print()
print('X = \n', X)
print()

# We print indices
print('indices = ', indices)
print()

# We use the indices ndarray to select the 2nd and 4th row of X
Y = X[indices,:]

# We use the indices ndarray to select the 2nd and 4th column of X
Z = X[:, indices]

# We print Y
print()
print('Y = \n', Y)

# We print Z
print()
print('Z = \n', Z)

# Selection of specific elements within ndarrays

# Diagonal

# We create a 4 x 5 ndarray that contains integers from 0 to 19
X = np.arange(25).reshape(5, 5)

# We print X
print()
print('X = \n', X)
print()

# We print the elements in the main diagonal of X
print('z =', np.diag(X))
print()

# We print the elements in the main diagonal of X
print('z =', np.diag(X, k=0))
print()

# We print the elements above the main diagonal of X
print('y =', np.diag(X, k=1))
print()

# We print the elements below the main diagonal of X
print('w = ', np.diag(X, k=-1))

# We print the elements above the main diagonal of X
print('t =', np.diag(X, k=3))
print()

# We print the elements below the main diagonal of X
print('q = ', np.diag(X, k=-3))

## Unique elements

# Create 3 x 3 ndarray with repeated values
X = np.array([[1,2,3],[5,2,8],[1,2,3]])

# We print X
print()
print('X = \n', X)
print()

# We print the unique elements of X
print('The unique elements in X are:',np.unique(X))


from sympy import latex, factor, lambdify
from utils import r

def gauss_seidel_method(x_exp, y_exp, z_exp=None, x_val=0, y_val=0, z_val=0, iter=1):
    out_str = ""
    x, y, z = symbols('x y z')

    new_values = [x_val, y_val, z_val]

    out_str += f"\\textbf{{Iteration {iter}}}\n\n"
    out_str += f"Substitute $y={r(new_values[1])}$, $z={r(new_values[2])}$\n"
    out_str += f"$$ x = {latex(factor(x_exp))} ="
    new_values[0] = float(lambdify((x, y, z), x_exp)(*new_values))
    out_str += f"{r(new_values[0])} $$\n\n"

    out_str += f"Substitute $x={r(new_values[0])}$, $z={r(new_values[2])}$\n"
    out_str += f"$$ y = {latex(factor(y_exp))} ="
    new_values[1] = float(lambdify((x, y, z), y_exp)(*new_values))
    out_str += f"{r(new_values[1])} $$\n\n"

    if z_exp:
        out_str += f"Substitute $x={r(new_values[0])}$, $y={r(new_values[1])}$\n"
        out_str += f"$$ z = {latex(factor(z_exp))} ="
        new_values[2] = float(lambdify((x, y, z), z_exp)(*new_values))
        out_str += f"{r(new_values[2])} $$\n\n"

    out_str += f"After Iteration {iter}\n"
    out_str += f"$$ (x, y, z) = ({', '.join([r(v) for v in new_values])})$$\n\n"

    if iter != 1:
        out_str += f"\\textbf{{Approximate Error}}\n\n"
        out_str += f"$$ \\epsilon_a(x) = \\frac{{\\left|\\text{{latest value of x}} - \\text{{previous value of x}}\\right|}}{{\\left|\\text{{latest value of x}}\\right|}} \\times 100 $$\n"
        out_str += f"$$ \\epsilon_a(x) = \\frac{{\\left|{r(new_values[0])} - ({r(x_val)})\\right|}}{{\\left|{r(new_values[0])}\\right|}} \\times 100 $$\n"

        e_x = abs(new_values[0] - x_val) / abs(new_values[0]) * 100
        out_str += f"$$ \\epsilon_a(x) = {r(e_x, 2)} \% $$\n"

        out_str += f"$$ \\epsilon_a(y) = \\frac{{\\left|\\text{{latest value of y}} - \\text{{previous value of y}}\\right|}}{{\\left|\\text{{latest value of y}}\\right|}} \\times 100 $$\n"
        out_str += f"$$ \\epsilon_a(y) = \\frac{{\\left|{r(new_values[1])} - ({r(y_val)})\\right|}}{{\\left|{r(new_values[1])}\\right|}} \\times 100 $$\n"

        e_y = abs(new_values[1] - y_val) / abs(new_values[1]) * 100
        out_str += f"$$ \\epsilon_a(y) = {r(e_y, 2)} \% $$\n\n"

        if z_exp:
            out_str += f"$$ \\epsilon_a(z) = \\frac{{\\left|\\text{{latest value of z}} - \\text{{previous value of z}}\\right|}}{{\\left|\\text{{latest value of z}}\\right|}} \\times 100 $$\n"
            out_str += f"$$ \\epsilon_a(z) = \\frac{{\\left|{r(new_values[2])} - ({r(z_val)})\\right|}}{{\\left|{r(new_values[2])}\\right|}} \\times 100 $$\n"

            e_z = abs(new_values[2] - z_val) / abs(new_values[2]) * 100
            out_str += f"$$ \\epsilon_a(z) = {r(e_z, 2)} \% $$\n\n"

    if iter < 5:
        x_val, y_val, z_val = new_values
        out_str += gauss_seidel_method(x_exp, y_exp, z_exp, x_val, y_val, z_val, iter+1)

    return out_str

if __name__ == '__main__':
    from sympy import solve, Eq, symbols
    x, y = symbols('x y')

    print(gauss_seidel_method(
        solve(Eq(3*x+2*y, 4), y)[0],
        solve(Eq(x - 2*y, 5), x)[0],
        # solve(Eq(-3*x -y + 7*z, -34), z)[0],
    ))


import numpy

def bisection(f, interval, eps_abs = 1e-10, max_step = 100):
    """
    Use bisection to find x such that f(x) = 0.
    """

    x_lo, x_hi = interval
    x = (x_lo + x_hi) / 2.0
    f_lo = f(x_lo)
    if (abs(f_lo) < eps_abs):
        return x_lo
    f_hi = f(x_hi)
    if (abs(f_hi) < eps_abs):
        return x_hi
    #assert(f_lo*f_hi < 0), "f(Endpoints) must change sign!"
    if f_lo*f_hi > 0:
        print("Warning! f(endpoints) have same sign!")

    f_mid = f(x)
    step = 0
    while (step < max_step) and abs(f_mid) > eps_abs:
        step += 1
        if f_lo * f_mid < 0.0:
            x_hi = x
            f_hi = f(x_hi)
        else:
            x_lo = x
            f_lo = f(x_lo)
        x = (x_lo + x_hi) / 2.0
        f_mid = f(x)

    return x

if __name__ == "__main__":
    def f(x):
        return numpy.exp(x) + x - 2
    def g(x):
        return numpy.sin(x**2) - 0.1 * x

    interval = [0, 1]
    s = bisection(f, interval)
    print("s = {}, f(s) = {}".format(s, f(s)))
    for lower in range(1,9):
        interval = [lower, 10]
        try:
            s = bisection(g, interval)
            print("interval = [{}, 10], s = {}, g(s) = {}".format(lower, s, g(s)))
        except:
            pass