import numpy as np

def softmax(action_values, tau=1.0):
    """
    Args:
        action_values (Numpy array): A 2D array of shape (batch_size, num_actions).
                       The action-values computed by an action-value network.
        tau (float): The temperature parameter scalar.
    Returns:
        A 2D array of shape (batch_size, num_actions). Where each column is a probability distribution over
        the actions representing the policy.
    """

    # Compute the preferences by dividing the action-values by the temperature parameter tau
    preferences = action_values / tau
    # Compute the maximum preference across the actions
    max_preference = np.max(preferences, axis=1)

    # your code here

    # Reshape max_preference array which has shape [Batch,] to [Batch, 1]. This allows NumPy broadcasting
    # when subtracting the maximum preference from the preference of each action.
    reshaped_max_preference = max_preference.reshape((-1, 1))
    # print(reshaped_max_preference)

    # Compute the numerator, i.e., the exponential of the preference - the max preference.
    exp_preferences = np.exp(preferences - reshaped_max_preference)
    # print(exp_preferences)
    # Compute the denominator, i.e., the sum over the numerator along the actions axis.
    sum_of_exp_preferences = np.sum(exp_preferences, axis=1)
    # print(sum_of_exp_preferences)

    # your code here

    # Reshape sum_of_exp_preferences array which has shape [Batch,] to [Batch, 1] to  allow for NumPy broadcasting
    # when dividing the numerator by the denominator.
    reshaped_sum_of_exp_preferences = sum_of_exp_preferences.reshape((-1, 1))
    # print(reshaped_sum_of_exp_preferences)

    # Compute the action probabilities according to the equation in the previous cell.
    action_probs = exp_preferences / reshaped_sum_of_exp_preferences
    # print(action_probs)

    # your code here

    # squeeze() removes any singleton dimensions. It is used here because this function is used in the
    # agent policy when selecting an action (for which the batch dimension is 1.) As np.random.choice is used in
    # the agent policy and it expects 1D arrays, we need to remove this singleton batch dimension.
    action_probs = action_probs.squeeze()
    return action_probs

if __name__ == '__main__':
    rand_generator = np.random.RandomState(0)
    action_values = rand_generator.normal(0, 1, (2, 4))
    tau = 0.5

    action_probs = softmax(action_values, tau)
    print("action_probs", action_probs)

    assert (np.allclose(action_probs, np.array([
        [0.25849645, 0.01689625, 0.05374514, 0.67086216],
        [0.84699852, 0.00286345, 0.13520063, 0.01493741]
    ])))

    action_values = np.array([[0.0327, 0.0127, 0.0688]])
    tau = 1.
    action_probs = softmax(action_values, tau)
    print("action_probs", action_probs)

    assert np.allclose(action_probs, np.array([0.3315, 0.3249, 0.3436]), atol=1e-04)

    print("Passed the asserts! (Note: These are however limited in scope, additional testing is encouraged.)")


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

## Machine Learning Online Class - Exercise 1: Linear Regression

def warmUpExercise():
    return np.eye(5)

def plotData(x, y):
    plt.plot(x, y, 'rx', label='Training data')
    plt.xlabel('Population of City in 10,000s')
    plt.ylabel('Profit in $10,000s')
    plt.title('Scatter plot of training data')
    plt.axis([4, 25, -5, 24])

def computeCost(X, y, theta):
    error = (X @ theta - y)
    return (error.T @ error)/ (2*y.size)

def gradientDescent(X, y, theta, alpha, iterations):
    m = y.size
    for _ in range(iterations):
        theta -= alpha/m * X.T @ (X @ theta - y)
    return theta

# ==================== Part 1: Basic Function ====================
print('Running warmUpExercise ... ')
print('5x5 Identity Matrix:')
print(warmUpExercise())
input('Program paused. Press enter to continue.\n')

# ======================= Part 2: Plotting =======================
# x refers to the population size in 10,000s
# y refers to the profit in $10,000s

scriptdir = os.path.dirname(os.path.realpath(__file__))
print(scriptdir)
data = np.loadtxt(scriptdir + '//ex1data1.txt', delimiter=',')
X = data[:,:-1]
y = data[:,-1]
m = y.size #number of training examples

print('Plotting Data ...')
#Plot Data
plotData(X, y)
plt.show()

# =================== Part 3: Cost and Gradient descent ===================
X = np.c_[np.ones(m), X] # #Add a column of ones to X (interception data)
theta = np.zeros(2) #initialize fitting parameters

# Some gradient descent settings
iterations = 1500
alpha = 0.01

print('\nTesting the cost function ...')
# compute and display initial cost
J = computeCost(X, y, theta)
print(f'With theta = [0 ; 0], Cost computed = {J}')
print('Expected cost value (approx) 32.07')

# further testing of the cost function
J = computeCost(X, y, [-1 , 2])
print(f'\nWith theta = [-1 ; 2], Cost computed = {J}')
print(f'Expected cost value (approx) 54.24')
input('Program paused. Press enter to continue.\n')

print('Running Gradient Descent ...')
# run gradient descent
theta = gradientDescent(X, y, theta, alpha, iterations)

# print theta to screen
print('Theta found by gradient descent:')
print(theta)
print('Expected theta values (approx)')
print(' -3.6303,  1.1664\n')

# Plot the linear fit
plotData(X[:,1], y)
plt.plot(X[:,1], X @ theta, '-', label='Linear regression')
plt.legend()
plt.show()

# Predict values for population sizes of 35,000 and 70,000
predict1 = [1, 3.5]  @ theta
print(f'For population = 35,000, we predict a profit of {predict1*10000}')
predict2 = [1, 7] @ theta
print(f'For population = 70,000, we predict a profit of {predict2*10000}')
input('Program paused. Press enter to continue.\n')

# ============= Part 4: Visualizing J(theta_0, theta_1) =============
print('Visualizing J(theta_0, theta_1) ...')

# Grid over which we will calculate J
theta0_vals = np.linspace(-10, 10, 100)
theta1_vals = np.linspace(-1, 4, 100)

# initialize J_vals to a matrix of 0's
J_vals = np.zeros((theta0_vals.size, theta1_vals.size))

# Fill out J_vals
# Because of the way Surface plot and meshgrid work, we need to swap
# j,i order in J_vals, or else the axes will be flipped
for i in range(theta0_vals.size):
    for j in range(theta1_vals.size):
	    J_vals[j,i] = computeCost(X, y, [theta0_vals[i], theta1_vals[j]])

fig = plt.figure()
ax = fig.gca(projection='3d')
# Surface plot
xx, yy = np.meshgrid(theta0_vals, theta1_vals)
surf = ax.plot_surface(xx, yy, J_vals, antialiased=False)
#LaTex rendering with matplotlib is very sow on MacOS
#ax.set_xlabel(r'$\theta_0$')
#ax.set_ylabel(r'$\theta_1$')
ax.set_xlabel('Theta_0')
ax.set_ylabel('Theta_1')
plt.show()

# Contour plot
# Plot J_vals as 15 contours spaced logarithmically between 0.01 and 100
plt.contour(xx, yy, J_vals, np.logspace(-2, 3, 20))
#LaTex rendering with matplotlib is very sow on MacOS
#plt.xlabel(r'$\theta_0$')
#plt.ylabel(r'$\theta_1$')
plt.xlabel('Theta_0')
plt.ylabel('Theta_1')

plt.plot(theta[0], theta[1], 'rx', ms=10, lw=2)
plt.show()


"""This file contains code used in "Think Stats",
by Allen B. Downey, available from greenteapress.com

Copyright 2014 Allen B. Downey
License: GNU GPLv3 http://www.gnu.org/licenses/gpl.html
"""

from __future__ import print_function, division

import thinkstats2
import thinkplot

import math
import random
import numpy as np

def MeanError(estimates, actual):
    """Computes the mean error of a sequence of estimates.

    estimate: sequence of numbers
    actual: actual value

    returns: float mean error
    """
    errors = [estimate-actual for estimate in estimates]
    return np.mean(errors)

def RMSE(estimates, actual):
    """Computes the root mean squared error of a sequence of estimates.

    estimate: sequence of numbers
    actual: actual value

    returns: float RMSE
    """
    e2 = [(estimate-actual)**2 for estimate in estimates]
    mse = np.mean(e2)
    return math.sqrt(mse)

def Estimate1(n=7, m=1000):
    """Evaluates RMSE of sample mean and median as estimators.

    n: sample size
    m: number of iterations
    """
    mu = 0
    sigma = 1

    means = []
    medians = []
    for _ in range(m):
        xs = [random.gauss(mu, sigma) for _ in range(n)]
        xbar = np.mean(xs)
        median = np.median(xs)
        means.append(xbar)
        medians.append(median)

    print('Experiment 1')
    print('rmse xbar', RMSE(means, mu))
    print('rmse median', RMSE(medians, mu))

def Estimate2(n=7, m=1000):
    """Evaluates S and Sn-1 as estimators of sample variance.

    n: sample size
    m: number of iterations
    """
    mu = 0
    sigma = 1

    estimates1 = []
    estimates2 = []
    for _ in range(m):
        xs = [random.gauss(mu, sigma) for _ in range(n)]
        biased = np.var(xs)
        unbiased = np.var(xs, ddof=1)
        estimates1.append(biased)
        estimates2.append(unbiased)

    print('Experiment 2')
    print('mean error biased', MeanError(estimates1, sigma**2))
    print('mean error unbiased', MeanError(estimates2, sigma**2))

def Estimate3(n=7, m=1000):
    """Evaluates L and Lm as estimators of the exponential parameter.

    n: sample size
    m: number of iterations
    """
    lam = 2

    means = []
    medians = []
    for _ in range(m):
        xs = np.random.exponential(1/lam, n)
        L = 1 / np.mean(xs)
        Lm = math.log(2) / np.median(xs)
        means.append(L)
        medians.append(Lm)

    print('Experiment 3')
    print('rmse L', RMSE(means, lam))
    print('rmse Lm', RMSE(medians, lam))
    print('mean error L', MeanError(means, lam))
    print('mean error Lm', MeanError(medians, lam))

def SimulateSample(mu=90, sigma=7.5, n=9, m=1000):
    """Plots the sampling distribution of the sample mean.

    mu: hypothetical population mean
    sigma: hypothetical population standard deviation
    n: sample size
    m: number of iterations
    """
    def VertLine(x, y=1):
        thinkplot.Plot([x, x], [0, y], color='0.8', linewidth=3)

    means = []
    for _ in range(m):
        xs = np.random.normal(mu, sigma, n)
        xbar = np.mean(xs)
        means.append(xbar)

    stderr = RMSE(means, mu)
    print('standard error', stderr)

    cdf = thinkstats2.Cdf(means)
    ci = cdf.Percentile(5), cdf.Percentile(95)
    print('confidence interval', ci)
    VertLine(ci[0])
    VertLine(ci[1])

    # plot the CDF
    thinkplot.Cdf(cdf)
    thinkplot.Save(root='estimation1',
                   xlabel='sample mean',
                   ylabel='CDF',
                   title='Sampling distribution')

def main():
    thinkstats2.RandomSeed(17)

    Estimate1()
    Estimate2()
    Estimate3(m=1000)
    SimulateSample()

if __name__ == '__main__':
    main()

DSC 530 - Data Exploration and Analysis/ThinkStats2/code/estimation.py


"""
The Frobenius equation is the Diophantine equation,

a_1 x_1 +... + a_n x_n = b

where a_i> 0 are positive integers, b> 0 is a positive integer, and the solution x_i consists
of non-negative integers. Here is a sample run,

->>> solvefrob([1,2,3,5],10)
 [(0, 0, 0, 2),
  (0, 1, 1, 1),
  (0, 2, 2, 0),
  (0, 5, 0, 0),
  (1, 0, 3, 0),
  (1, 2, 0, 1),
  (1, 3, 1, 0),
  (2, 0, 1, 1),
  (2, 1, 2, 0),
  (2, 4, 0, 0),
  (3, 1, 0, 1),
  (3, 2, 1, 0),
  (4, 0, 2, 0),
  (4, 3, 0, 0),
  (5, 0, 0, 1),
  (5, 1, 1, 0),
  (6, 2, 0, 0),
  (7, 0, 1, 0),
  (8, 1, 0, 0),
  (10, 0, 0, 0)]

Hint: Use Numpy broadcasting effectively. There is a timeout in the test-case,
so if it takes too long to compute (e.g, you used too many for loops), it will be marked wrong.
The function signature is solvefrob(coefs,b) where coefs is the list of a_i coefficients.
You can only use Numpy for this problem. No other third party packages.
"""
import numpy as np
import time

def solvefrob(coefs, b):
    """
    Solves frobenius equation

    Bounding the domain space makes enormous difference in performance
    :param coefs:
    :param b:
    :return:
    """

    # input type validation
    for elem in coefs:
        assert isinstance(elem, int)
        assert elem > 0

    assert isinstance(b, int)
    assert b > 0

    # construct candidate matrix C of dims MxN where N = len(coefs)
    arr = None
    for i, coef in enumerate(coefs[::-1]):
        max = b // coef  # restricts the max value of candidate solution at
        # a given index to bound solution space, vastly increases performance
        r = np.arange(max+1)[:, None]
        if arr is None:
            arr = r
        else:
            new_r = np.repeat(r, len(arr))[:, None]
            new_arr = np.concatenate([arr for _ in range(len(r))])
            arr = np.concatenate([new_r, new_arr], axis=1)

    # extract candidate solutions where C * coefs == b
    out = np.matmul(arr, coefs)
    results = arr[out == b].tolist()
    results = [tuple(result) for result in results]
    return results

if __name__ == '__main__':
    ##### Arguments
    coefs = [1,2,3,5,6,4,6,4]
    b = 15
    ##### End Arguments

    s = time.time()
    results = solvefrob(coefs, b)
    e = time.time()
    print(f"{solvefrob.__name__}, time: {e-s} seconds")
    print(f"Coeffs = {coefs}, B = {b}")
    print(f"number of solutions: {len(results)}\n")
    print("solution | coeffs | dot product")
    print("-"*20)
    for result in results:
        print("{} | {} | {}".format(result, coefs, np.dot(result, coefs)))


## Leetcode problem 210: Course schedule II.
#https://leetcode.com/problems/course-schedule-ii/
#based on topological sorting.

import numpy as np
import algorith.clr_book.ch22_elemtary_graph.graph as gr
from typing import List

class Solution():
    def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
        self.lst = []
        g=gr.Graph().init_from_edge_list(numCourses,prerequisites)
        self.dfs_topolo_sort(g)
        return self.lst

    def dfs_topolo_sort(self,g):
        for v in g.vertices:
            if v.color=="white":
                try:
                    self.dfs_visit(g,v)
                except:
                    self.lst=[]
                    break

    def dfs_visit(self,g,u):
        u.color="grey"
        for v in g.adj[u]:
            if v.color=="grey":
                raise Exception("cycle detected!")
            elif v.color=="white":
                self.dfs_visit(g,v)
        u.color="black"
        self.lst.append(u.key)

if __name__ == "__main__":
    numCourses = 4; prerequisites = [[1,0],[2,0],[3,1],[3,2]]; output_1= [0,2,1,3]; output_2= [0,1,2,3]
    lst = Solution().findOrder(numCourses,prerequisites)
    print(lst)
    numCourses = 2; prerequisites = [[1,0]]; output= [0,1]
    lst = Solution().findOrder(numCourses,prerequisites)
    print(lst)
    numCourses = 2; prerequisites = [[0,1]]; output= [1,0]
    lst = Solution().findOrder(numCourses,prerequisites)
    print(lst)
    numCourses = 2; prerequisites = [[0,1],[1,0]]; output= []
    lst = Solution().findOrder(numCourses,prerequisites)
    print(lst)


import numpy as np

def euclidean_distances(X, Y):
    """Compute pairwise Euclidean distance between the rows of two matrices X (shape MxK)
    and Y (shape NxK). The output of this function is a matrix of shape MxN containing
    the Euclidean distance between two rows.

    Args:
        X {np.ndarray} -- First matrix, containing M examples with K features each.
        Y {np.ndarray} -- Second matrix, containing N examples with K features each.

    Return:
        D {np.ndarray}: MxN matrix with Euclidean distances between rows of X and rows of Y.
    """
    a = X.shape[0]
    b = Y.shape[0]
    D = np.zeros(shape=(a,b))
    n = 0
    for i in X:
        m = 0
        for j in Y:
            elem = np.linalg.norm(i-j)
            D[n][m] = elem
            m += 1
        n += 1
    return D

def manhattan_distances(X, Y):
    """Compute pairwise Manhattan distance between the rows of two matrices X (shape MxK)
    and Y (shape NxK). The output of this function is a matrix of shape MxN containing
    the Manhattan distance between two rows.

    Args:
        X {np.ndarray} -- First matrix, containing M examples with K features each.
        Y {np.ndarray} -- Second matrix, containing N examples with K features each.

    Returns:
        D {np.ndarray}: MxN matrix with Manhattan distances between rows of X and rows of Y.
    """
    a = X.shape[0]
    b = Y.shape[0]
    D = np.zeros(shape=(a,b))
    n = 0
    for i in X:
        m = 0
        for j in Y:
            elem = np.linalg.norm(i-j, ord=1)
            D[n][m] = elem
            m += 1
        n += 1
    return D


import numpy as np
from scipy import stats
def mean_diff_test(a, b, check_col, sample_size=2000):
    ## Define 2 random distributions
    #Sample Size
    a = a[[check_col]]
    b = b[[check_col]]

    ## Calculate the Standard Deviation
    # Calculate the variance to get the standard deviation
    a = np.array(a)
    b = np.array(b)

    a = np.array(a).astype(np.float)
    b = np.array(b).astype(np.float)

    # For unbiased max likelihood estimate we have to divide the var by N-1, and therefore the parameter ddof = 1
    var_a = a.var(ddof=1)
    var_b = b.var(ddof=1)

    # std deviation
    s = np.sqrt((var_a + var_b)/2)

    ## Calculate the t-statistics
    t = (a.mean() - b.mean())/(s*np.sqrt(2/sample_size))

    # Compare with the critical t-value
    #Degrees of freedom
    df = 2 * sample_size - 2

    # p-value after comparison with the t
    p = 1 - stats.t.cdf(t, df=df)

    print("t = " + str(t))
    print("p = " + str(2*p))
    # Note that we multiply the p value by 2 because its a twp tail t-test
    # You can see that after comparing the t statistic with the critical t value (computed internally)
    # we get a good p value of 0.0005 and thus we reject the null hypothesis and thus it proves that the mean of the
    # two distributions are different and statistically significant.

    # Cross Checking with the internal scipy function
    t2, p2 = stats.ttest_ind(a,b)
    print("t = " + str(t2))
    print("p = " + str(2*p2))


import numpy as np

def PLU_decomposition(A,verbose=False):
    m=A.shape[0]
    n=A.shape[1]
    P=np.eye(n)
    L=np.zeros([n,n])
    U=np.array(A).copy()
    if m!=n:
        print "This matrix cannot realize PLU_decomposition because it is not a square matrix!"
        return
    else:
        for i in range(n-1):
            for j in range(i+1,n):
                for k in range(n-1,i,-1):
                    if U[i,i] < U[k,i]:
                        L[i, :], L[k, :] = np.array([L[k, :], L[i, :]]).copy()
                        P[i, :], P[k, :] = np.array([P[k, :], P[i, :]]).copy()
                        U[i, :], U[k, :] = np.array([U[k, :], U[i, :]]).copy()
                if U[i,i]==0:
                    L[j,i]=0
                else:
                    L[j,i]=U[j,i]/float(U[i,i])
                    U[j,:]=U[j,:]-L[j,i]*U[i,:]

        for i in range(n): L[i,i]=1

        if verbose :
            print"The PLU_decomposition decomposition of A is:"
            print "The P matrix is:"
            print (P)
            print "The L matrix is"

            print (L)
            print "The U matrix is"
            print (U)

        return P,L,U

def Gram_Schimidt(A,verbose=False):
    m = A.shape[0]
    n = A.shape[1]
    R=np.zeros([m,n])
    Q=np.zeros([m,m])
    R[0,0]=np.linalg.norm(A[:,0])
    if R[0, 0] == 0:
        print"cannot realiaze Gram_Schimidt decomposition"
    else:
        Q[:,0]=A[:,0]/R[0,0]
    for i in range(1,n):
        Q[:,i]=A[:,i]
        for j in range(i):
            R[j,i]=np.dot(Q[:,j].T,Q[:,i])
            Q[:,i]-=R[j,i]*Q[:,j]
        R[i,i]=np.linalg.norm(Q[:,i])
        if R[i,i]==0:
            print"cannot realiaze Gram_Schimidt decomposition"
        else:
            Q[:, i] = Q[:, i] / R[i, i]
    if verbose:
        print"The Gram_Schimidt decomposition of A is:"
        print ("The Q matrix is:")
        print (Q)
        print ("The R matrix is:")
        print (R)
    return  Q,R

def householder_reduction(A,verbose=False):
    m = A.shape[0]
    n = A.shape[1]
    E=np.eye(m)
    T=np.array(A).copy()
    P=np.eye(m)
    for i in range(n-1):
        u=T[i:,i]-np.linalg.norm(T[i:n,i])*E[i:,i]
        u=np.array([u])
        u=u.T
        I = np.eye(m)
        I[i:,i:]=np.eye(n-i)-2*np.dot(u,u.T)/(np.linalg.norm(u)*np.linalg.norm(u))
        T=np.dot(I,T)
        P=np.dot(I,P)
        Q=P.T
    if verbose:
        print "The householder_reduction of A is:"
        print "The Q matrix is:"
        print Q
        print "The R matrix is:"
        print T
    return  Q,T

def Givens_reduction(A,verbose=False):
    m = A.shape[0]
    n = A.shape[1]
    P=np.eye(m)
    R=np.array(A).copy()
    for k in range(m):
        for j in range(n-1,k,-1):
            tmpa = R[k,k]
            tmpb = R[j,k]
            mag = np.sqrt(tmpa*tmpa+tmpb*tmpb)
            c= tmpa / mag
            s= tmpb / mag
            I=np.eye(m)
            I[k,k]=c
            I[k,j]=s
            I[j,j]=c
            I[j,k]=-s
            P=np.dot(I,P)
            R=np.dot(I,R)
            Q=P.T
    if verbose:
        print"The Givens_reduction of A is:"
        print ("The Q matrix is:")
        print (Q)
        print ("The R matrix is:")
        print (R)
    return  Q,R
# A=np.array([[1 , 2 , -3 , 4],[4 , 8 , 12 , -8],[2 , 3 , 2 , 1 ],[-3 , -1 , 1 , -4 ]])
# A = np.array([[1, 19, -34], [-2, -5, 20], [2, 8, 37]])
#A = np.array([[0, -20, -14], [3, 27, -4], [4, 11, -2]])
# A = np.array([[3, 2, 1], [2, -3, 4], [5, 1, -1], [7, 4, 2]])
# A=np.array([[0,0],[0,0]])
if __name__=='__main__':
    #A = np.random.randn(9,9)
    A = np.random.randint(0,10000,[4,4])
    print"the A matrix is:"
    print A
    print "please choose the way to decompose A . "
    print  "Press '1' to realize the PLU_decomposition of A. "
    print  "Press '2' to realize the Gram_Schimidt of A."
    print  "Press '3' to realize the householder_reduction of A."
    print  "Press '4' to realize the Givens_reduction of A."
    Q = np.empty(A.shape)
    R = np.empty(A.shape)
    choose_mode=input("please choose the mode:")
    if choose_mode==1:
        P, L, U=PLU_decomposition(A)
        print  np.sum(np.linalg.norm(np.dot(P,A) - np.dot(L,U)))
        print "The P matrix is:"
        print (P)
        print "The L matrix is"

        print (L)
        print "The U matrix is"
        print (U)

    elif choose_mode==2:
        Q, R  = Gram_Schimidt(A)
        print  np.sum(np.linalg.norm(A - np.dot(Q, R)))

    elif choose_mode==3:
        Q, R = householder_reduction(A)
        print  np.sum(np.linalg.norm(A - np.dot(Q, R)))
    elif choose_mode==4:
        Q,R = Givens_reduction(A)
        print  np.sum(np.linalg.norm(A - np.dot(Q, R)))

    if choose_mode is not 1:
        print ("The Q matrix is:")
        print (Q)
        print ("The R matrix is:")
        print (R)

        print "QR decomposition in numpy is \n", np.linalg.qr(A)


from math import sin, cos, tan, atan, asin, acos, atan2
from XCat_Objects import pi, DtoR, D2R, R2D
import numpy as np
from numpy.linalg import norm

# RA  = phi
# DEC = pi/2 - theta (theta = pi/2 - DEC)
def DEC2theta(DEC):
    return np.pi/2. - DEC

def theta2DEC(theta):
    return np.pi/2. - theta

def Cartesian2Spherical(x,y,z):
    cart_vector = [x, y, z]
    r = norm(cart_vector)
    unit = cart_vector / r
    theta = acos(unit[2])
    phi = atan2(unit[1], unit[0])
    return r, theta, phi

def Spherical2Cartesian(r,RA,DEC):
    cos_DEC = cos(DEC)
    x = r * cos(RA) * cos_DEC
    y = r * sin(RA) * cos_DEC
    z = r * sin(DEC)
    return x, y, z

def Cartesian2RADEC(x,y,z,noNeg=True):
    cart_vector = [x, y, z]
    r = norm(cart_vector)  # it is not checking for r=1.0 but it should
    unit = cart_vector / r
    theta = acos(unit[2])
    phi = atan2(unit[1], unit[0])  # RA = phi, DEC = theta2DEC(theta)
    if noNeg:
        while (phi < 0.0): phi += 2. * np.pi
    return phi, theta2DEC(theta)

# RA_o and DEC_o such that RA_o and DEC_o
# in spherical coordinate become zeros
def CoordTrans(x,y,z,RA_o,DEC_o):
    cosD = cos(DEC_o); sinD = sin(DEC_o)
    cosR = cos(RA_o);  sinR = sin(RA_o)
    xn =  cosD*cosR*x + cosD*sinR*y + sinD*z
    yn =      -sinR*x +      cosR*y
    zn = -sinD*cosR*x - sinD*sinR*y + cosD*z
    return xn,yn,zn

def invCoordTrans(x,y,z,RA_o,DEC_o):
    cosD = cos(DEC_o); sinD = sin(DEC_o)
    cosR = cos(RA_o);  sinR = sin(RA_o)
    xn =  cosD*cosR*x - sinR*y - cosR*sinD*z
    yn =  sinR*cosD*x + cosR*y - sinR*sinD*z
    zn =    sinD*x             +   cosD*z
    return xn,yn,zn

# RA, DEC befor rotation (in degree)
# RA_o, DEC_o the new origin for coordinate (in degree)
# return new RA, DEC (in degree)
def rotatingCoord(RA,DEC,RA_o,DEC_o,noNeg=True):
    x,y,z = Spherical2Cartesian(1.0,D2R*RA,D2R*DEC)
    x,y,z = CoordTrans(x,y,z,D2R*RA_o,D2R*DEC_o)
    RAn,DECn = Cartesian2RADEC(x,y,z,noNeg=noNeg)
    return R2D*RAn,R2D*DECn

def EquatorialTOGalactic(RA,DEC):
    # l
    gLong = 303.0*DtoR - atan( sin(192.25*DtoR-RA*DtoR) / (cos(192.25*DtoR-RA*DtoR)*sin(27.4*DtoR) - tan(DEC*DtoR)*cos(27.4*DtoR)) )
    # b
    gLati = asin(sin(DEC*DtoR)*sin(27.4*DtoR) + cos(DEC*DtoR)*cos(27.4*DtoR)*cos(192.25*DtoR-RA*DtoR))
    return (gLong/DtoR,gLati/DtoR)

''' EXAMPLES
#EXAMPLE (WORKING)
#print "RA & DEC :", rotatingCoord(20.0,30.0,20.0,30.0)
#RA_o  = 00.*D2R
#DEC_o = 30.*D2R
#RA  = 00.*D2R
#DEC = 20.*D2R
#x,y,z = Spherical2Cartesian(1.0,RA,DEC)
#x,y,z = CoordTrans(x,y,z,RA_o,DEC_o)
#RA,DEC = Cartesian2RADEC(x,y,z)
#print "RA  : ", R2D*RA; print "DEC : ", R2D*DEC

#x=4.0; y=1.0; z=3.0
#x,y,z = CoordTrans(x,y,z,1.0,1.0)
#x,y,z = invCoordTrans(x,y,z,1.0,1.0)
#print x,y,z

RA_o  = 30.*D2R
DEC_o = 20.*D2R
r     = 1.0

RA  = 50.*D2R
DEC = 20.*D2R
r   = 1.0

print 'RA  = ', R2D*RA
print 'DEC = ', R2D*DEC
print 'r   = ', r

x,y,z = Spherical2Cartesian(r,RA,DEC)
x,y,z = CoordTrans(x,y,z,RA_o,DEC_o)
r, theta, phi = Cartesian2Spherical(x,y,z)

print '--------------------------------'
print 'RA  = ', R2D*phi
print 'DEC = ', R2D*theta2DEC(theta)
print 'r   = ', r
'''


# Implementation of Multiple linear regression using scikit-learn
# OLS can find the best parameters using of the following methods:
# Solving the model parameters analytically using closed-form equations
# Using an optimization algorithm (Gradient Descent, Stochastic Gradient Descent, Newton’s Method)

# Importing required packages
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error

def multiple_linear_regr(df, x, y):
    """
    Multiple linear regression uisng least squares.
    Prints coefficient, y-intercept, and evaluation metrics:
    Mean absolute error,  Mean squared error, Residual sum of squares (MSE), variance score
    :param df: pandas data frame
    :param x: List of independent variables (features)
    :param y: dependent variable
    :return:
    """

    # Creating train and test dataset: Train/Test Split
    # Lets split our dataset into train and test sets, 80% of the entire data for training, and the 20% for
    # testing. We create a mask to select random rows using np.random.rand() function:
    msk = np.random.rand(len(df)) < 0.8
    train = df[msk]
    test = df[~msk]

    # Modeling with training set
    train_x = np.asanyarray(train[[i for i in x]])
    train_y = np.asanyarray(train[[y]])

    # Model initialization
    regression_model = LinearRegression()
    regression_model.fit(train_x, train_y)

    # Get the coefficient and y-intercept
    print("Coefficient and y-intercept:")
    print("Coefficient: {}".format(regression_model.coef_))
    print("Intercept: {}".format(regression_model.intercept_))

    # Predict the response, based on test set
    test_x = np.asanyarray(test[[i for i in x]])
    test_y = np.asanyarray(test[[y]])
    y_pred = regression_model.predict(test_x)

    # Evaluate the model
    # There are different model evaluation metrics to calculate the accuracy of the model
    # Calculate evaluation metrics
    mae = mean_squared_error(test_y, y_pred)
    rmse = np.mean(np.absolute(y_pred - test_y))
    mse = np.mean((y_pred - test_y) ** 2)
    var = regression_model.score(test_x, test_y)

    print("\nEvaluation metrics:")
    print("Mean absolute error: {:.2f}".format(mae))
    print("Mean squared error: {:.2f}".format(rmse))
    print("Residual sum of squares (MSE): {:.2f}".format(mse))
    print("Variance score: {:.2f}".format(var))

if __name__ == "__main__":

    # Reading the data in
    df = pd.read_csv("../tests/test_data/FuelConsumption.csv")
    # print(df.head())
    # print(df.columns)

    # Select some features to explore more
    # Features taken: 'ENGINESIZE', 'CO2EMISSIONS'
    cdf = df[['ENGINESIZE','CYLINDERS','FUELCONSUMPTION_COMB', 'CO2EMISSIONS']]
    # print(cdf.head(10))

    # plot features using scatterplot
    plt.scatter(cdf.ENGINESIZE, cdf.CO2EMISSIONS, color='blue')
    plt.xlabel("Engine size")
    plt.ylabel("Emission")
    plt.show(block=False)
    plt.pause(3)
    plt.close()

    # multiple_linear_regr(cdf, ['ENGINESIZE', 'CYLINDERS'], "CO2EMISSIONS")

    multiple_linear_regr(cdf, ['ENGINESIZE','CYLINDERS','FUELCONSUMPTION_COMB'], "CO2EMISSIONS")


import numpy as np
from scipy.stats.mstats import gmean

def get_mean(array=None):
    """
    Find mean of the data array
    :param array: data array
    :return: mean of the data
    """
    if array is not None:
        return np.mean(array)

def get_rms(array=None):
    """
    Computes the root mean square of an array/audio signal
    :param array: audio array
    :return: RMS of an array
    """
    if array is not None:
        return np.sqrt(np.mean(array**2))
    else:
        print("[-] Array not provided")
        return 0

def get_max(audio=None):
    """
    Find the max value of array
    :param audio:
    :return: max_value in array
    """
    if audio is not None:
        return np.max(audio)
    else:
        print("[-] No Audio Provided")

def get_min(audio=None):
    """
    Find the min value of array
    :param audio:
    :return: min_value in array
    """
    if audio is not None:
        return np.min(audio)
    else:
        print("[-] No Audio Provided")

def get_geometric_mean(audio=None):
    """
    returns the geometric mean of an array
    :param audio: audio array
    :return: geo-metric mean
    """
    if audio is not None:
        return gmean(audio)
    else:
        print("[-] No audio provided")
        return 0

def geo_mean(audio):
    """
    finds geometric mean of an array
    :param audio: array
    :return: geometric mean
    """
    a = np.array(audio)
    return a.prod() ** (1.0 / len(a))

def get_flatness(audio=None):
    """
    This algorithm computes the flatness of an array, which is defined as the ratio between
    the geometric mean and the arithmetic mean.
    :param audio: the input array
    :return:  the flatness (ratio between the geometric and the arithmetic mean of the input array)

    """
    if audio is not None:

        gm = get_geometric_mean(audio)
        _mean = get_mean(audio)
        flatness = gm / _mean
        return flatness


# %% [markdown]
# # Fitting a normal distribution with tensorflow probability
# Here we look at fitting a normal distribution to some data using Tensorflow Probability.
# %%
import numpy as np
import pandas as pd

import tensorflow as tf
import tensorflow_probability as tfp

import matplotlib.pyplot as plt

plt.style.use("seaborn-whitegrid")

# %% [markdown]
# Generate some data from a normal distribution:
# %%
n = 1000
true_mu = 2.0
true_std = 3.4
x = np.random.normal(loc=true_mu, scale=true_std, size=n)
plt.hist(x, 30)
plt.show()
# %% [markdown]
# # Model setup
# Tensorflow probability allows us to fit a network where the final layer output is not a scalar value,
# but a probability distribution.
# The normal distribution is parameterised by the mean and standard deviation.
# As such the model needs to output two values.
#
# We are fitting just a static distribution to the data.
# This is equivalent to fitting a network where the inputs are all 0.
# In this situation we only use the network biases as the weights become irrelevant with inputs of 0.
#
# To build the above - the model has a dense layer with 2 output nodes.
# For the final output layer we utilise the normal distribution object from tensorflow probability.
# The `DistributionLambda` layer allows us to use a lambda function to split the dense layer outputs
# and connect them to the `loc` and `scale` (mean and standard deviation) inputs of the normal distribution.
# The scale uses a `softplus` function to ensure the standard deviation is always positive.
# %%
model = tf.keras.Sequential(
    [
        tf.keras.layers.Dense(1 + 1),
        tfp.layers.DistributionLambda(
            lambda t: tfp.distributions.Normal(
                loc=t[..., :1],
                scale=1e-3 + tf.math.softplus(0.05 * t[..., 1:]),
            )
        ),
    ]
)
# %% [markdown]
# The cost function is setup as the negative loglikelihood:
# Tensorflow probably objects have a `log_prob(y)` method which returns the log probablility of the sample `y`.
# %%
def negloglik(y, distr):
    return -distr.log_prob(y)
# %% [markdown]
# We fit the model as normal using gradient descent and the `Adam` optimiser.
# As mentioned before, the input to the network is a vector of zeros.
# The target variable is our random data we want to fit the normal distribution to.
# %%
model.compile(optimizer=tf.optimizers.Adam(learning_rate=0.2), loss=negloglik)

dummy_input = np.zeros(x.shape)[:, np.newaxis]
history = model.fit(dummy_input, x, epochs=100, verbose=0)
# %% [markdown]
# The loglikelihood has converged over the training:
# %%
plt.plot(history.history['loss'])
plt.show()
# %% [markdown]
# # Results
# We can obtain the fitted parameters from the `model.weights` biases.
# They do not match the exact parameters of the true distribution.
# They converge to the maximum likelihood estimator for a normal distribution.
# In this case it corresponds to the sample mean and standard deviation.
#
# We can compare these different estimates by creating probability distribution objects
# and then plotting the probability distribution function.
# As we can see the estimates are reasonable approximations of the true distribution.
# %% Compare to sampled estimators and true values
p_y1 = tfp.distributions.Normal(
    loc=model.weights[1][0], scale=tf.math.softplus(0.05 * model.weights[1][1])
)
p_y2 = tfp.distributions.Normal(loc=np.mean(x), scale=np.std(x))
p_y3 = tfp.distributions.Normal(loc=true_mu, scale=true_std)

x_t = np.linspace(-10, 10, 50)
y_t1 = p_y1.prob(x_t)
y_t2 = p_y2.prob(x_t)
y_t3 = p_y3.prob(x_t)

plt.hist(x, bins=30, density=True, label="y")
plt.plot(x_t, y_t1, label="y_est")
plt.plot(x_t, y_t2, label="y_samp")
plt.plot(x_t, y_t3, label="y_true")
plt.legend()
plt.show()


import numpy as np
import time    # check documentation for further use
import matplotlib.pyplot as plt
from qr_decomp import *
from mat_inversion_method import *

# Import and Store the Data
x_data = []
y_data = []

filename = 'data1.txt'
data = np.loadtxt(filename, delimiter=' ', dtype=float)
x_data = data[:,0]
y_data = data[:,1]

# PART 1

# Form the required Matrix
A = []
for data_point in x_data:
    A.append([1, data_point, data_point*data_point])
B = y_data

# Finding COEFFICIENTS, TIME TAKEN and ERROR
## finding coefficients using QR Decomposition while noting time
t1 = time.time()    # works like a stop-watch
b_qr_decomp = qr_decomposition_solver(A,B)
t2 = time.time()
# thus time taken by QR decomposition approach is t2-t1
time1_in_milli_sec = (t2-t1)*(10**3)
print("time taken when solved by QR decomposition : %.5f ms" % time1_in_milli_sec)
print(b_qr_decomp)

#WHY DOES THIS SHOW A SLIGHTLY DIFFERENT TIME EVERYTIME
#in the result, show a range of values of time

## finding coefficients using Matrix Inversion while noting time
t3 = time.time()
b_mat_inversion = mat_inv_method(A,B)
t4 = time.time()
# thus time taken by matrix inversion approach is t4-t3
time2_in_milli_sec = (t4-t3)*(10**3)
print("time taken when solved by Matrix inversion : %.5f ms" % time2_in_milli_sec)
print(b_mat_inversion)

# Calculating the ERROR in each methods
# We will evaluate the accuracy of the models based on r r-squared values
# we can directly compare the errors, but for uniformity of evaluation wrt different models
# we will compare the results based on r-squared values
rms_data = 0
rms_qr = 0
rms_mi = 0

# predicted values y_cap
#QR Decomposition
y1_cap = [(b_qr_decomp[2]*(i**2) + b_qr_decomp[1]*i + b_qr_decomp[0]) for i in x_data]
#Matrix Inversion
y2_cap = [(b_mat_inversion[2]*(i**2) + b_mat_inversion[1]*i + b_mat_inversion[0]) for i in x_data]

y_bar = B.mean()

#finding rms error in data
for element in B:
    rms_data = rms_data + ((element-y_bar)**2)

# finding rms errors in both methods
for element in y1_cap:
    rms_qr = rms_qr + ((element-y_bar)**2)
for element in y2_cap:
    rms_mi = rms_mi + ((element-y_bar)**2)

r_squared_qr = rms_qr/rms_data
r_squared_mi = rms_mi/rms_data
print("R-square for QR method Regression: {}".format(r_squared_qr))
print("R-square for Matrix Inversion method Regression: {}".format(r_squared_mi))

# PART 2
# finding terms for biquadratic regression equation

# Finding COEFFICIENTS, TIME TAKEN and ERROR
## finding coefficients using QR Decomposition
A = []
for data_point in x_data:
    A.append([1, data_point, data_point**2, data_point**3, data_point**4])
t5 = time.time()
b_biquadratic_qr_decomp = qr_decomposition_solver(A,B)
t6 = time.time()
time3_in_milli_sec = (t6-t5)*(10**3)
print("time taken when solved for biquadratic regression using QR method is : %.5f ms" % time3_in_milli_sec)
print(b_biquadratic_qr_decomp)

# calculating the fit of the model using R-square value
y_bar = B.mean()
y_biquad_cap = [(b_biquadratic_qr_decomp[4]*(i**4) + b_biquadratic_qr_decomp[3]*(i**3) + b_biquadratic_qr_decomp[2]*(i**2) + b_biquadratic_qr_decomp[1]*i + b_biquadratic_qr_decomp[0]) for i in x_data]
rms_biquad_qr = 0
for element in y_biquad_cap :
    rms_biquad_qr = rms_biquad_qr + ((element-y_bar)**2)
r_squared_bq = rms_biquad_qr/rms_data
print("R-square for Biquadratic Regression: {}".format(r_squared_bq))

"""
# PRINTING AND SAVING THE PLOTS

x_axis = [j for j in range(0,11,1) ]
y1_axis = [(b_qr_decomp[2]*(i**2) + b_qr_decomp[1]*i + b_qr_decomp[0]) for i in x_axis]
y2_axis = [(b_mat_inversion[2]*(i**2) + b_mat_inversion[1]*i + b_mat_inversion[0]) for i in x_axis]
y_biquad_axis = [(b_biquadratic_qr_decomp[4]*(i**4) + b_biquadratic_qr_decomp[3]*(i**3) + b_biquadratic_qr_decomp[2]*(i**2) + b_biquadratic_qr_decomp[1]*i + b_biquadratic_qr_decomp[0]) for i in x_axis]

plt.scatter(x_data,y_data,marker = '.')
plt.plot(x_axis, y1_axis,label='QR Decomposition')
plt.plot(x_axis, y2_axis,label='Matrix Inversion')
plt.plot(x_axis, y_biquad_axis,label='Biquadratic Regression')
#plt.title('Least Squares Plot')
plt.xlabel('x')
plt.ylabel('y')
plt.legend(loc='best')
plt.grid()
plt.savefig('Task5.png')
plt.show()
"""


from scipy.integrate import odeint
from numpy import sin, pi, linspace, arange

# First Order ODE:
dy = lambda y, x: x*y   # Equation to be solved, y' = xy
y0 = 1                  # Initial condition, y(0) = 1
x = linspace(0, 2, 5)   # Divides [0,2] into (2-0)/.5 = 4 (+1 as 0 inc.) points

# Odeint Function
y = odeint(dy, y0, x)

print('odeint(dy, y0, x) =', y, sep='\n', end='\n\n')

# First Order ODE:
dy = lambda y, x: y+3   # Equation to be solved, y' = y+3
y0 = -2                 # Initial condition, y(0) = -2
x = linspace(2, 4, 21)  # Divides [2,4] into (4-2)/.1 = 20 (+1 as 0 inc.) points

# Odeint Function
y = odeint(dy, y0, x)

print('odeint(dy, y0, x) =', y, sep='\n', end='\n\n')

# Second Order ODE:
def  dy(y, x):              # Equation to be solved, y'' = 4*x + 10*sin(x) - y
    y, u  = y
    dydx = [u, 4*x + 10*sin(x) - y]
    return dydx

y0 = [0, 2]                 # Initial conditions, y(π) = 1, u(π) = 2
x = arange(pi, 2*pi, .5)    # Arange [π,2π[ in .5 step size

# Odeint Function
sol = odeint(dy, y0, x)

print('odeint(dy, y0, x) =\n    y \t\ty\'', sol, sep='\n')

6. Ordinary Differential Equations/0. ODE solving functions of SciPy.py


### ch3.1.4 Lpノルムの作図

#%%

# 3.1.4項で利用するライブラリ
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

#%%

## Lpノルムの作図

# 値を指定
p = 1

# wの値を指定
w_vals = np.arange(-10.0, 10.1, 0.1)

# 作図用のwの点を作成
W1, W2 = np.meshgrid(w_vals, w_vals)

# Lpのノルムを計算
Lp = (np.abs(W1)**p + np.abs(W2)**p)**(1.0 / p)

#%%

# Lpノルムの3Dグラフを作成
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(projection='3d') # 3D用の設定
ax.plot_surface(W1, W2, Lp, cmap='jet') # 曲面図
ax.contour(W1, W2, Lp, cmap='jet', offset=0) # 等高線図
ax.set_xlabel('$w_1$')
ax.set_ylabel('$w_2$')
ax.set_zlabel('$||w||_p$')
ax.set_title('p=' + str(np.round(p, 1)), loc='left')
fig.suptitle('$||w||_p = {}^p\sqrt{\sum_{j=1}^M |w_j|^p}$')
#ax.view_init(elev=90, azim=270) # 表示アングル
plt.show()

#%%

# Lpノルムの2Dグラフを作成
plt.figure(figsize=(9, 8))
plt.contour(W1, W2, Lp, cmap='jet') # 等高線図
#plt.contour(W1, W2, Lp, cmap='jet', levels=1) # 等高線図:(値を指定)
#plt.contourf(W1, W2, Lp, cmap='jet') # 塗りつぶし等高線図
plt.xlabel('$w_1$')
plt.ylabel('$w_2$')
plt.title('p=' + str(np.round(p, 1)), loc='left')
plt.suptitle('$||w||_p = {}^p\sqrt{\sum_{j=1}^M |w_j|^p}$')
plt.colorbar(label='$||w||_p$')
plt.grid()
plt.gca().set_aspect('equal')
plt.show()

#%%

## 正則化項の作図

# 正則化項を計算
E_W = (np.abs(W1)**p + np.abs(W2)**p) / p

# 正則化項の3Dグラフを作成
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(projection='3d') # 3D用の設定
ax.plot_surface(W1, W2, E_W, cmap='jet') # 曲面図
ax.contour(W1, W2, E_W, cmap='jet', offset=0) # 等高線図
ax.set_xlabel('$w_1$')
ax.set_ylabel('$w_2$')
ax.set_zlabel('$E_W(w)$')
ax.set_title('p=' + str(np.round(p, 1)), loc='left')
fig.suptitle('$E_W(w) = \\frac{1}{p} \sum_{j=1}^M |w_j|^p$')
#ax.view_init(elev=90, azim=270) # 表示アングル
plt.show()

#%%

## pとグラフの形状の関係

# 使用するpの値を指定
p_vals = np.arange(0.1, 10.1, 0.1)

# 図を初期化
fig = plt.figure(figsize=(6, 6))
ax = fig.add_subplot(projection='3d') # 3D用の設定
fig.suptitle('Lp-Norm', fontsize=20)

# 作図処理を関数として定義
def update(i):
    # 前フレームのグラフを初期化
    plt.cla()

    # i回目の値を取得
    p = p_vals[i]

    # Lpノルムを計算
    Lp = (np.abs(W1)**p + np.abs(W2)**p)**(1.0 / p)

    # Lpノルムの3Dグラフを作成
    ax.plot_surface(W1, W2, Lp, cmap='jet') # 曲面図
    ax.contour(W1, W2, Lp, cmap='jet', offset=0) # 等高線図
    ax.set_xlabel('$w_1$')
    ax.set_ylabel('$w_2$')
    ax.set_zlabel('$||w||_p$')
    ax.set_title('p=' + str(np.round(p, 1)), loc='left')

# gif画像を作成
anime_norm3d = FuncAnimation(fig, update, frames=len(p_vals), interval=100)

# gif画像を保存
anime_norm3d.save('PRML/Fig/ch3_1_4_LpNorm_3d.gif')

#%%

# 図を初期化
fig = plt.figure(figsize=(6, 6))

# 作図処理を関数として定義
def update(i):
    # 前フレームのグラフを初期化
    plt.cla()

    # i回目の値を取得
    p = p_vals[i]

    # Lpノルムを計算
    Lp = (np.abs(W1)**p + np.abs(W2)**p)**(1.0 / p)

    # Lpノルムの2Dグラフを作成
    plt.contour(W1, W2, Lp, cmap='jet') # 等高線図
    #plt.contourf(W1, W2, Lp, cmap='jet') # 塗りつぶし等高線図
    plt.xlabel('$w_1$')
    plt.ylabel('$w_2$')
    plt.title('p=' + str(np.round(p, 1)), loc='left')
    plt.suptitle('Lp-Norm', fontsize=20)
    plt.grid()
    plt.axes().set_aspect('equal')

# gif画像を作成
anime_norm2d = FuncAnimation(fig, update, frames=len(p_vals), interval=100)

# gif画像を保存
anime_norm2d.save('PRML/Fig/ch3_1_4_LpNorm_2d.gif')


import numpy as np

def geom_mean(v):
    """Geometrical mean of the elements of a numpy array."""
    return np.prod(v) ** (1/v.shape[0])

def angle_mean(angles):
    """ Compute average angle of a vector of angles in radians."""
    return np.angle(np.sum([np.exp(1j * ang) for ang in angles]))

def compute_angle(u, v=None):
    '''
    Computes the angle between vectors u and v.
    If v is None, computes the angle of u wrt y=0
    '''
    # if v is None: v = np.array([1, 0])
    if v is None:
        if all(u == 0):
            return 0
        ang = np.arccos(u[0] / np.linalg.norm(u))
        return ang if u[1] >= 0 else -ang
    else:
        if u.sum() == 0 or v.sum() == 0 or u == v:
            return 0
        cos_theta = np.dot(u, v) / np.linalg.norm(u) / np.linalg.norm(v)
        theta = np.arccos(np.clip(cos_theta, a_min=-1, a_max=1))
        # if(u[0]*v[1] - u[1]*v[0] < 0): theta *= -1
        return theta

def angle_diff(x, y):
    """ Compute the difference between two angles in radians."""
    # abs_diff = np.abs(x - y)
    # return min(abs_diff, 2 * np.pi - abs_diff)
    return min((x - y) % (2 * np.pi), (y - x) % (2 * np.pi))

def normalize(v):
    """ Normalize a numpy array."""
    return v / np.linalg.norm(v)

def eigendecomposition(C):
    """ Eigendecomposition of matrix C. """
    eigenvals, B = np.linalg.eig(C)
    D = np.diag(eigenvals)
    return B, D, B.T

def toroidal_difference(v, u):
    v_diff = v - u
    abs_diff = np.abs(v_diff)
    res = v_diff.copy()
    res[abs_diff > 500] = 1000 - abs_diff[abs_diff > 500]
    res[abs_diff > 500] *= -np.sign(v_diff[abs_diff > 500])
    # (-1, 1)[v_diff[abs_diff > 500] > 0] # Correct sign
    # import pdb; pdb.set_trace()
    return res

#! PASAR A DISTANCES
def circle_distance(alpha, beta):
    #! Check that |alpha - beta| <= 2pi
    return min(np.abs(alpha - beta), 2*np.pi - np.abs(alpha - beta))


# -*- coding: utf-8 -*-

import numpy as np
import argparse

parser = argparse.ArgumentParser(description='EAO exercises')
parser.add_argument('-v', "--verbose", type=int,
                    help='If verbosity, then -v 1', default=0)

args = parser.parse_args()

if args.verbose == 1:
    print("\nModo Debugger\n")
################ Ejercicio 1
"""
    Ptos criticos, 0, 2, 4. (0, 0) mínimo, (2, 2) punto de silla, (4, 4) mínimo.
"""

"""
    Condición suficiente de optimalidad de segundo orden: dado que los autovalores
    de la matriz Hessiana son mayores que 0, ésta es definida positiva. Por lo tanto
    se satisface la condición de optimalidad de segundo orden.
"""

def f(x):
    return 9*x[0]**2 - 2*x[0]*x[1]+x[1]**2-4*x[0]**3 + 0.5*x[0]**4

def g(x):
    return np.array([18*x[0]-2*x[1] -12*x[0]**2 + 2*x[0]**3, -2*x[0]+2*x[1]])

def H(x):
    return np.array([[-24*x[0]+6*x[0]**2+18, -2], [-2, 2]])

def eigvals(x1=0, x2=0):
    hessian = H([x1, x2])
    return np.linalg.eig(hessian)[0]

def is_DP():
    return eigvals().all() > 0

print("¿Es definida positiva la matriz hessiana? {}".format(is_DP()))

################# Ejercicio 2
from mpl_toolkits import mplot3d
import matplotlib.pyplot as plt

fig, ax = plt.subplots(subplot_kw={"projection": "3d"})

n_points = 50

x = np.linspace(-1, 5, n_points)
y = np.linspace(-3, 8, n_points)

[x1, y1] = np.meshgrid(x, y)

z1 = f([x1, y1]).reshape(n_points, n_points)

plt.title("Superficie")
ax.plot_surface(x1, y1, z1, cmap=plt.cm.coolwarm,
                       linewidth=1)
plt.show()

################# Ejercicio 3
plt.title("Curvas de nivel")
plt.contour(x1, y1, z1, 50) # 50 curvas de nivel
plt.show()

################# Ejercicio 4
def gradient_descent(x0=[0.4, -1.9], it=40):
    x = x0
    pts, steps_gr = [], []
    m, M = np.linalg.eig(H([0, 0]))[0]
    alpha = 2/(m+M)
    steps_gr.append(f(x.copy()))

    for i in range(it):
        pts.append(x.copy())
        x += -alpha*g(x)
        steps_gr.append(f(x.copy()))

    pts.append(x.copy())

    return f(x), np.array(pts), steps_gr

gradient_descent_minimum, pts_gd, steps_gr = gradient_descent(x0=[0.4, -1.9], it=40)

if args.verbose == 1:
    print("\n\nGradiente Descendente")
    print("\n", steps_gr, "\n")

print("Gradiente Descendente mínimo {}".format(gradient_descent_minimum))

plt.title("Gradiente descendente. Alpha constante")
plt.contour(x1, y1, z1, 50)
plt.plot(pts_gd[:, 0], pts_gd[:, 1], marker="o")
plt.show()

################# Ejercicio 5
def metodo_newton(x0=[0.4, -1.9], it=5):
    x = x0
    z = x0
    pts, steps_new = [], []

    steps_new.append(f(x.copy()))

    for i in range(it):
        pts.append(x.copy())
        x += np.linalg.solve(H(x), -g(x))
        #z += -np.linalg.inv(H(z))@g(z)
        steps_new.append(f(x.copy()))

    pts.append(x.copy())

    return f(x), np.array(pts), steps_new

newton_minimum, pts_newton, steps_new = metodo_newton(x0=[0.4, -1.9], it=5)

if args.verbose == 1:
    print("\n\nMétodo de Newton")
    print("\n", steps_new, "\n")

print("Newton mínimo {}".format(newton_minimum))

plt.title("Newton")
plt.contour(x1, y1, z1, 50)
plt.plot(pts_newton[:, 0], pts_newton[:, 1], marker="o")
plt.show()

##################### Ejercicio 6
from scipy.optimize import minimize

def busqueda_lineal_exacta(x0=[0.4, -1.9], it=40):
    x = x0
    pts, steps_ls = [], []

    steps_ls.append(f(x.copy()))

    for i in range(it):
        pts.append(x.copy())
        alpha = minimize(lambda alpha : f(x - alpha*g(x)), x0=1 )
        x += -alpha.x*g(x)
        steps_ls.append(f(x.copy()))

    return f(x), np.array(pts), steps_ls

linear_search_minimum, pts_ls, steps_ls = busqueda_lineal_exacta(x0=[0.4, -1.9], it=40)

if args.verbose == 1:
    print("\n\nBúsqueda lineal exacta")
    print("\n", steps_ls, "\n")

print("Búsqueda lineal exacta mínimo {}".format(linear_search_minimum))

plt.title("Descenso del Gradiente. Búsqueda exacta.")
plt.contour(x1, y1, z1, 50)
plt.plot(pts_ls[:, 0], pts_ls[:, 1], marker="o")
plt.show()

"""
    El método de Newton encuentra el mínimo, con un menor número de iteraciones
    que el resto de métodos.
    El método del gradiente con la búsqueda lineal exacta, como cabe esperar, encuentra el mínimo
    con menos iteraciones que el gradiente con el tamaño de paso constante. Sin embargo,
    hay que tener en cuenta, que la búsqueda lineal exacta es más costosa computacionalmente.
"""

plt.contour(x1, y1, z1, 50)

plt.title("Comparativa")
plt.plot(pts_gd[:, 0], pts_gd[:, 1], marker="o", label="Descenso del Gradiente. Alpha constante.")
plt.plot(pts_newton[:, 0], pts_newton[:, 1], marker="o", label="Newton.")
plt.plot(pts_ls[:, 0], pts_ls[:, 1], marker="o", label="Descenso del Gradiente. Búsqueda exacta.")
plt.legend(loc="upper right")
plt.show()

if args.verbose == 1:
    # Nota: Para poder hacer la comparativa, se ha puesto 40 iteraciones en el método de Newton.q
    newton_minimum, pts_new, steps_new = metodo_newton(x0=[0.4, -1.9], it=40)
    iterations = np.arange(30)

    plt.title("Comparativa")
    plt.xlabel("iteraciones")
    plt.ylabel("||Xk - Xminimo||")

    distancia_gradiente = [ np.linalg.norm([pts_gd[i, ...], pts_gd[-1, ...]]) for i in iterations ]
    distancia_blineal = [ np.linalg.norm([pts_ls[i, ...], pts_ls[-1, ...]]) for i in iterations ]
    distancia_newton = [ np.linalg.norm([pts_new[i, ...], pts_new[-1, ...]]) for i in iterations ]

    plt.plot(iterations, distancia_newton, marker = "*", label="Newton.")
    plt.plot(iterations, distancia_blineal, marker = "*" , label="Descenso del Gradiente. Búsqueda exacta.")
    plt.plot(iterations, distancia_gradiente, marker = "*", label="Descenso del Gradiente. Alpha constante.")
    plt.legend(loc="upper right")
    plt.show()

##################### Ejercicio 7
def newton_gradient(alpha, x=[0.4, -1.9]):
    pts_g = []
    pts_n = []
    for c_alpha in alpha:
        pg = -g(x)/np.linalg.norm(g(x))

        #hessian_inverse = np.linalg.solve(H(x), -g(x))
        #pn = hessian_inverse / np.linalg.norm(hessian_inverse)
        pn = - (np.linalg.inv(H(x)))@(g(x))  / np.linalg.norm((np.linalg.inv(H(x)))@(g(x)))

        phi_g = f(x+c_alpha*pg)
        phi_n = f(x+c_alpha*pn)

        pts_g.append(phi_g)
        pts_n.append(phi_n)

    return pts_g, pts_n

alphas = np.arange(0, 2, 0.01)
pts_g, pts_n = newton_gradient(alpha = alphas)

index_gd = pts_g.index(min(pts_g))
index_new = pts_n.index(min(pts_n))

print("Minimo en el Gradiente {} con alpha {}".format(pts_g[index_gd], alphas[index_gd]) )
print("Minimo en el Newton {} con alpha {}".format(pts_n[index_new], alphas[index_new]) )

plt.scatter([alphas[index_gd], alphas[index_new]], [pts_g[index_gd], pts_n[index_new]])

plt.title("Comparativa")
plt.xlabel("valor de alpha")
plt.ylabel("f(x)")

plt.plot(alphas, pts_g, c="blue", label="gradiente")
plt.plot(alphas, pts_n, c="red", label="newton")
plt.legend(loc="upper left")
plt.show()

"""
    a) Para una búsqueda lineal exacta sería mejor la dirección de newton dado que tiene un mínimo
    inferior al mínimo de la función con el método del gradiente.

    b) La función con el método del gradiente decrece más rápido que con el método de newton
    para el tramo inicial de valores de alpha. Al tener tamaños de paso muy pequeños nos interesa lo que
    ocurre en las iteraciones "iniciales" y por lo tanto el método del gradiente es mejor.
"""


'''
By starting at the top of the triangle below and moving to adjacent numbers on the row below,
the maximum total from top to bottom is 23.

   3
  7 4
 2 4 6
8 5 9 3

That is, 3 + 7 + 4 + 9 = 23.

Find the maximum total from top to bottom of the triangle below:

                         75
                       95 64
                      17 47 82
                    18 35 87 10
                  20 04 82 47 65
                 19 01 23 75 03 34
                88 02 77 73 07 63 67
              99 65 04 28 06 16 70 92
             41 41 26 56 83 40 80 70 33
            41 48 72 33 47 32 37 16 94 29
           53 71 44 65 25 43 91 52 97 51 14
         70 11 33 28 77 73 17 78 39 68 17 57
       91 71 52 38 17 14 91 43 58 50 27 29 48
      63 66 04 68 89 53 67 30 73 16 69 87 40 31
    04 62 98 27 23 09 70 98 73 93 38 53 60 04 23
'''

'''
final answer: 1074
'''

import numpy as np

# init constant
t = np.array([
    [75],
    [95, 64],
    [17, 47, 82],
    [18, 35, 87, 10],
    [20, 4, 82, 47, 65],
    [19, 1, 23, 75, 3, 34],
    [88, 2, 77, 73, 7, 63, 67],
    [99, 65, 4, 28, 6, 16, 70, 92],
    [41, 41, 26, 56, 83, 40, 80, 70, 33],
    [41, 48, 72, 33, 47, 32, 37, 16, 94, 29],
    [53, 71, 44, 65, 25, 43, 91, 52, 97, 51, 14],
    [70, 11, 33, 28, 77, 73, 17, 78, 39, 68, 17, 57],
    [91, 71, 52, 38, 17, 14, 91, 43, 58, 50, 27, 29, 48],
    [63, 66, 4, 68, 89, 53, 67, 30, 73, 16, 69, 87, 40, 31],
    [4, 62, 98, 27, 23, 9, 70, 98, 73, 93, 38, 53, 60, 4, 23],
])

# compute the maximum sum path
def compute():
    # all we need to do is start from the one to the last row and replace every number with the maximum of the current
    # number plus the bottom adjacent number and work our way up to the top
    # note: t.shape[0] = number of rows, since it starts from 0 and we want one to the last -> t.shape[0]-2
    for i in range(t.shape[0] - 2, -1, -1):
        max_num = 0
        for j in range(len(t[i])):
            t[i][j] = t[i][j] + max(t[i + 1][j], t[i + 1][j + 1])

    return t[0][0]  # this would be the max

if __name__ == '__main__':
    print("The maximum path sum is " + str(compute()))


# -*- coding: utf-8 -*-
"""
Spyder Editor

This is a temporary script file.
"""
#%% imports

import numpy as np
import matplotlib.pyplot as plt
plt.rcParams.update({'font.size': 22, 'figure.autolayout': True, 'figure.figsize': (15,5)})
factorial = np.math.factorial
SEED = 999

np.random.seed(SEED)
#%% Uniform distribution
'''
What is the probability of getting a 2 in a die roll?
'''

# Empty list to hold the ratio of 2s in each set of drawings
p = []

nr_trials = np.logspace(1,3,100)
for i in nr_trials:
    N = int(i) # number of drawings
    # generate N uniform random number between 1 and 6 (7 is exclusive)
    uniform_samples = np.random.randint(1, 7, size = N)
    # count the number of 2s we got
    count = np.sum(uniform_samples == 2)
    pval = count/N # ratio (tends to probability)
    p.append(pval)

fig1 = plt.figure()
plt.plot(nr_trials,p,'o-')
plt.plot(nr_trials,[1/6]*len(nr_trials),'--r')
plt.grid()
plt.xlim(nr_trials[0],nr_trials[-1])
plt.xlabel('Number of drawings')
plt.ylabel('p(X=2)')
plt.title(f'Theoretical p(X=2) = {1/6:.3f}')

#%% Bernoulli distribution
'''
Consider getting either a 1 or a 2 in a die roll as sucess, while any
other number as a failure. This can be modeled as Bernoulli distribution
with $theta$ as p(X=success) and $1-\theta$ as p(X=fail)
'''

# Theoreticallly we know that each number in the roll of a die comes
# from the uniform distribution so getting either 1 or 2 should be
# p(X=sucess) =p(X=1) + p(X=2), where $X \in {1,2,3,4,5,6}$

theoryBernoulli = 1/6 + 1/6
print(f'Theoretical probability = {theoryBernoulli :.3f}')

# create a list to hold the probabilities
p = []

nr_trials = np.logspace(1,3,100)
# Perform an experiment drawing uniform numbers and counting the amount of
# 1 or 2 and dividing the count by the number of drawings
for i in nr_trials :
    N = int(i)
    uniform_samples = np.random.randint(1, 7, size = N)
    count = np.isin(uniform_samples,[1,2])
    pval = sum(count)/N
    p.append(pval)

fig2 = plt.figure()
plt.plot(nr_trials,p,'o-')
plt.plot(nr_trials,[theoryBernoulli]*len(nr_trials),'--r')
plt.grid()
plt.xlim(nr_trials[0],nr_trials[-1])
plt.xlabel('Number of drawings')
plt.ylabel('p(X=success)')
plt.title(f'Theoretical p(X=sucess) = {theoryBernoulli:.3f}')
#%% Binomial distribution
'''
Say we toss a coin 3 times, what is the probability of getting a head
all of the times?
n = 3
k = 3
theta = 0.5
'''
n = 3
k = 3
theta = 0.5
# calculate p(k=3) by the analytic formula
theoryBinomial = factorial(n)/(factorial(n-k)*factorial(k))*theta**k*(1-theta)**(n-k)

# calculate p(k=3) using numpy for an experiment
numpylBinomial = sum(np.random.binomial(n,theta,int(2e5))==3)/int(2e5)

print(theoryBinomial)
print(numpylBinomial)

#%% save figures

fig1.savefig('uniform.png')
fig2.savefig('bernoulli.png')


import numpy as np
import matplotlib.pyplot as plt

h=0.01
min_x = 0.0
max_x = 6.0
n_points = int((max_x-min_x)/h)
x = np.zeros(n_points)
y_1 = np.zeros(n_points)
y_2 = np.zeros(n_points)

def func_prime_1(x, y_1, y_2):
    return y_2

def func_prime_2(x, y_1, y_2):
    return -4*np.sin(y_1)

def RungeKuttaFourthOrderStep(x_old, y1_old, y2_old):

    k_1_prime1 = func_prime_1(x_old,y1_old, y2_old)
    k_1_prime2 = func_prime_2(x_old,y1_old, y2_old)

    #first step
    x1 = x_old+ (h/2.0)
    y1_1 = y1_old + (h/2.0) * k_1_prime1
    y2_1 = y2_old + (h/2.0) * k_1_prime2
    k_2_prime1 = func_prime_1(x1, y1_1, y2_1)
    k_2_prime2 = func_prime_2(x1, y1_1, y2_1)

    #second step
    x2 = x_old + (h/2.0)
    y1_2 = y1_old + (h/2.0) * k_2_prime1
    y2_2 = y2_old + (h/2.0) * k_2_prime2
    k_3_prime1 = func_prime_1(x2, y1_2, y2_2)
    k_3_prime2 = func_prime_2(x2, y1_2, y2_2)

    #third
    x3 = x_old + h
    y1_3 = y1_old + h * k_3_prime1
    y2_3 = y2_old + h * k_3_prime2
    k_4_prime1 = func_prime_1(x3, y1_3, y2_3)
    k_4_prime2 = func_prime_2(x3, y1_3, y2_3)

    #fourth step
    average_k_1 = (1.0/6.0)*(k_1_prime1 + 2.0*k_2_prime1 + 2.0*k_3_prime1 + k_4_prime1)
    average_k_2 = (1.0/6.0)*(k_1_prime2 + 2.0*k_2_prime2 + 2.0*k_3_prime2 + k_4_prime2)

    x_new = x_old + h
    y_1_new = y1_old + h * average_k_1
    y_2_new= y2_old + h * average_k_2
    return x_new, y_1_new, y_2_new

x[0]   = min_x
y_1[0] = 0.0
y_2[0] = 0.1

for i in range(1,n_points):
    x[i],y_1[i],y_2[i] = RungeKuttaFourthOrderStep(x[i-1], y_1[i-1], y_2[i-1])
    print x[i], "  ", y_1[i]


# Implementation of simple linear regression using python numpy
# Linear regression is the most basic type of regression commonly used for predictive analysis.
# We try to best fit a regression line or least square line through dataset and estimate the parameters.

# Importing required packages
import numpy as np
import matplotlib.pyplot as plt
import time

def linear_regression(x, y, title=None, x_label=None, y_label=None, filename=None, show=True):
    """

    Simple Linear Regression Model using ordinary least squares (OLS). Fits regression line. Returns intercept and coefficient.
    :param x: Independent variables; list or array-likes, NumPy Array
    :param y: Dependent variable; list or array-likes, NumPy Array
    :param title: title of the plot
    :param x_label: x-label or independent variable
    :param y_label: y-label or dependent variable
    :param filename: filename to save figure
    :param show: It true shows current figure (default True)
    :return: simple linear regression model
    """

    # Estimate the coefficients
    b = estimate_coef(x, y)
    # print("Intercept: b_0 = {}".format(b[0]))
    # print("Coefficients: b_1 = {}".format(b[1]))

    # Plot the regression line
    fit_regression_line(x, y, b, title, x_label, y_label, filename, show)
    return b

def estimate_coef(x, y):

    # get the number of observations
    n = np.size(x)
    # print(n)

    # mean of x and y
    x_mean = np.mean(x)
    y_mean = np.mean(y)

    # Calculate Cov[x,y] and Var[x]
    cov_xy = n*np.sum(x*y) - (np.sum(x)*np.sum(y))
    var_x = n*np.sum(x*x) - (np.sum(x)*np.sum(x))

    # Calculate intercept b_0 and coefficient b_1
    b_1 = cov_xy/var_x
    b_0 = y_mean - b_1*x_mean

    return [b_0, b_1]

def fit_regression_line(x, y, b, title, x_label, y_label, filename, show):

    # plotting the observational points as scatter plot
    plt.scatter(x, y, color="b", marker="o", s=30)

    # predicted response vector y_hat
    y_hat = b[0] + b[1]*x

    # plo the regression line
    plt.plot(x, y_hat, color='r')

    plt.title(title)
    plt.xlabel(x_label)
    plt.ylabel(y_label)
    if filename:
        plt.savefig(filename)
    if show:
        plt.show(block=False)
        plt.pause(3)
        plt.close()

# Evaluating the performance of the model
# We will be using Residual sum of squares error (MSE) and Coefficient of Determination(R² score) to evaluate our model.

# Calculate Residual sum of squares (MSE)
def get_mse(y_hat, y_actual):
    """
    RMSE is the square root of the average of the sum of the squares of residuals.
    :param y_hat:
    :param y_actual:
    :return:
    """
    mse = np.mean((y_hat - y_actual)**2)

    return mse

# Calculate coefficient of determination: r2 score or r-square
def r2_score(y_hat, y_actual):
    """
    R² score or the coefficient of determination explains
    how much the total variance of the dependent variable
    can be reduced by using the least square regression.
    :return:
    """
    # calculate sum of squares of residuals
    ss_res = np.sum((y_actual - y_hat)**2)

    # calculate total sum of squares
    ss_tot = np.sum(y_actual - np.mean(y_actual)**2)

    # calculate r2 score
    r2 = 1 - (np.absolute(ss_res/ss_tot))

    return r2

# predict the value of dependent variable y_pred
# from independent variable x using this regression model
def predict(x, b):

    # predicted response vector y_hat
    y_pred = b[0] + b[1]*x

    return y_pred


# -*- coding: utf-8 -*-
import matplotlib.pyplot as plt
import numpy as np

def main(xi,yi):
    qtd = len(xi)
    tx = total(xi)
    ty = total(yi)

    xy = list()
    x2 = list()
    for i in xrange(0, qtd):
        xy.append(xi[i] * yi[i])
        x2.append(xi[i] ** 2.0)

    txy = total(xy)
    tx2 = total(x2)

    d = tx / qtd
    e = ty / qtd
    a1 = ((qtd*txy) - (tx*ty)) / ((qtd*tx2) - (tx**2))
    a0 = e - (a1 * d)

    print 'a1 =',a1
    print 'a0 =',a0

    y = list()
    for i in xrange(0, qtd):
        y.append(a1*xi[i]+a0)

    plt.plot(range(1,qtd+1), yi, 'bo')
    plt.plot(range(1,qtd+1), y)

def total(lst):
    total = float()
    for i in lst:
        total += i
    return total

if __name__ == '__main__':
    plt.title('Minimos Quadrados')
    plt.grid(True)
    plt.axis([-2, 8, -2, 8])
    plt.plot([-100,100],[0,0], 'k-') # Linha X
    plt.plot([0,0],[-100,100], 'k-') # Linha Y

    fl = open('input-mq.txt', 'r')
    x = [float(i) for i in fl.readline().split(' ')]
    y = [float(i) for i in fl.readline().split(' ')]
    fl.close()

    if len(x) == len(y):
        main(x,y)
        plt.show()
    else:
        print 'Valores de X e Y precisam ter a mesma quantidade.'


import numpy as np
import matplotlib.pyplot as plt

def make_gauss(N, sig, mu):
    return lambda x: N/(sig * (2*np.pi)**.5) * np.e ** (-(x-mu)**2/(2 * sig**2))

def main():
    ax = plt.figure().add_subplot(1,1,1)
    x = np.arange(-5, 5, 0.01)
    s = np.sqrt([0.2, 1, 5, 0.5])
    m = [0, 0, 0, -2]
    c = ['b','r','y','g']

    for sig, mu, color in zip(s, m, c):
        gauss = make_gauss(1, sig, mu)(x)
        ax.plot(x, gauss, color, linewidth=2)

    plt.xlim(-5, 5)
    plt.ylim(0, 1)
    plt.legend(['0.2', '1.0', '5.0', '0.5'], loc='best')
    plt.show()

if __name__ == '__main__':
   main()


# -*- coding: utf-8 -*-
"""
SYS-611: Dice Fighters Example

@author: Paul T. Grogan, pgrogan@stevens.edu
"""

# import the python3 behavior for importing, division, and printing in python2
from __future__ import absolute_import, division, print_function

# import the numpy library and refer to it as `np`
import numpy as np

# import the scipy.stats library and refer to it as `stats`
import scipy.stats as stats

# define the round_number state variable, initialize to 0
round_number = 0
# define the red_size state variable, initialize to 20
red_size = 20
# define the blue_size state variable, initialize to 10
blue_size = 10
# define the red_chance_hit state variable, initialize to 1/6
red_chance_hit = 1/6
# define the blue_chance_hit state variable, initialize to 3/6
blue_chance_hit = 3/6

# define the generate_red_hits function
def generate_red_hits():
    # return the number of hits
    return stats.binom.ppf(np.random.rand(), red_size, red_chance_hit)
    """
    note: the code above could be replaced by a built-in process generator:

    return np.random.binomial(red_size, red_chance_hit)
    """

# define the generate_blue_hits function
def generate_blue_hits():
    # return the number of hits
    return stats.binom.ppf(np.random.rand(), blue_size, blue_chance_hit)
    """
    note: the code above could be replaced by a built-in process generator:

    return np.random.binomial(blue_size, blue_chance_hit)
    """

# define the red_suffer_losses function with an argument for the number of opponent hits
def red_suffer_losses(opponent_hits):
    # (note: red_size must be declared as a global variable to update!)
    global red_size
    # update the red_size based on the number of opponent hits
    red_size -= opponent_hits

# define the blue_suffer_losses function with an argument for number of opponent hits
def blue_suffer_losses(opponent_hits):
    # (note: blue_size must be declared as a global variable to update!)
    global blue_size
    # update the blue_size based on number of opponent hits
    blue_size -= opponent_hits

# define the is_complete function
def is_complete():
    # return True if either red_size or blue_size is less than or equal to zero
    return (red_size <= 0 or blue_size <= 0)

# define the next_round state change function
def next_round():
    # (note: round_number must be declared as a global variable to update!)
    global round_number
    # advance the round_number
    round_number += 1

# main execution loop: continue while the game is not complete
while not is_complete():
    # generate the number of red hits
    red_hits = generate_red_hits()
    # generate the number of blue hits
    blue_hits = generate_blue_hits()
    # red team suffers losses of blue hits
    red_suffer_losses(blue_hits)
    # blue team suffers losses of red hits
    blue_suffer_losses(red_hits)
    # advance to the next round
    next_round()
    # print out the current state for debugging
    print("Round {}: {} Red, {} Blue".format(
            round_number,
            red_size,
            blue_size
        ))

# after main loop exists, check who won (whichever team still has fighters!)
if red_size > 0:
	print("Red Wins")
elif blue_size > 0:
	print("Blue Wins")
else:
	print("Tie - Mutual Destruction!")


import numpy as np

# LU Decomposition
# Inputs: A = matrix to be decomposed, sep (boolean) determines whether an LU combined
# matrix or two separate matrices.
# Outputs: LU is a matrix with all elements of U and all non-diagonal elements
# of L, or L and U which are two matrices separated into lower and upper triangular
# matrices.

def LU(A, sep = True):
    if A.shape[0] != A.shape[1]:
        print("Not a square matrix")
    else:
        LU = np.zeros([A.shape[0], A.shape[1]])
        L = np.zeros([A.shape[0], A.shape[1]])
        U = np.zeros([A.shape[0], A.shape[1]])
        for i in np.arange(A.shape[0]):
            for j in np.arange(A.shape[1]):
                LU[i, j] = A[i, j] # since every element in the LU matrix is
                # derived from the respective element in the input matrix.
                count = 0
                if i <= j: # means on the upper portion, use beta formula
                    while count < i:
                        LU[i, j] = LU[i, j] - LU[i, count]*LU[count, j]
                        count = count + 1
                    U[i, j] = LU[i, j]
                else: # on the lower portion, use alpha formula
                    while count < j:
                        LU[i, j] = LU[i, j] - LU[i, count]*LU[count, j]
                        count = count + 1
                    LU[i, j] = LU[i, j] / LU[j, j] # this step is not in the
                    L[i, j] = LU[i, j]
                    # while loop because ALL alpha's need to take, even if
                    # nothing is subtracted.
                L[i, i] = 1 # diagonal values
        if sep == True:
            return L, U
        else:
            return LU

# Calculates the determinant of a matrix using LU decomposition.
# Input: A = matrix for which the determinant is to be found

def det(A):
    L, U = LU(A)
    det = 1.
    for i in np.arange(A.shape[0]):
        det = det * U[i, i] # multiply by all diagonal elements in the upper matrix
    return det

# Solves a matrix equation using LU decomposition.
# Inputs: L and U are the decomposed lower and upper triangular matrices
# respectively, and b is the vector with all the constants.
# Output: vector containing all the x values

def solve(A, b):
    if A.shape[0] == np.size(b):
        L, U = LU(A)
        y = b.copy() # so that the value of b does not change with y
        for i in np.arange(np.size(b)):
            count = 0
            while count < i:
                y[i] = y[i] - L[i, count]*y[count]
                count = count + 1
        x = y.copy()
        for j in np.arange(1, np.size(b) + 1):
            count = j
            while count > 1:
                x[-j] = x[-j] - U[-j, -count + 1]*x[-count + 1]
                count = count - 1
            x[-j] = x[-j] / U[-j, -j]
        return x
    else:
        print("Size mismatch")

# To solve for A^-1 using LU decomposition we just combine the solutions to
# vectors(1, 0, 0), (0, 1, 0) and (0, 0, 1).

def inv(A):
    I = np.identity(A.shape[0])
    inv = np.zeros([A.shape[0], A.shape[1]])
    for j in np.arange(A.shape[1]):
        x = solve(A, I[j])
        for i in np.arange(A.shape[0]):
            inv[i, j] = x[i]
    return inv


import numpy as np
import matplotlib.pyplot as plt

def get_y(x):
    f_x = (x + 2)**2 - 16*np.exp(-((x-2)**2))
    return f_x

x = np.arange(-8,8,0.001)
y = list(map(lambda u: get_y(u),x))
plt.plot(x,y)
# plt.show()

def get_grad(x):
    deriv = (2*x + 4)-16*(-2*x+4)*np.exp(-((x-2)**2))
    return deriv

def gradient_descent(start_x, func, grad):
    # Precision of the solution
    prec = 0.0001
    # Use a fixed small step size
    step_size = 0.1
    # max iterations
    max_iter = 100
    x_new = start_x
    res = []
    for i in range(max_iter):
        x_old = x_new
        # Use beta = -1 for gradient descent
        x_new = x_old - step_size * grad(x_new)
        f_x_new = func(x_new)
        f_x_old = func(x_old)
        res.append([x_new, f_x_new])
        if(abs(f_x_new-f_x_old)<prec):
            print("change in function values too small, leaving")
            return np.array(res)
    print("exceed maximum number of iterations, leaving")
    return np.array(res)

x_0 = -8
res = gradient_descent(x_0, get_y, get_grad)
plt.plot(res[:,0], res[:,1], '+')

x_1 = 8
res = gradient_descent(x_1, get_y, get_grad)
plt.plot(res[:,0], res[:,1], '+')

plt.show()


import math
import numpy as np
import copy

def coordinate_transformation_in_angle(positions, base_angle):
    '''
    Transformation the coordinate in the angle

    Parameters
    -------
    positions : numpy.ndarray
        this parameter is composed of xs, ys
        should have (2, N) shape
    base_angle : float [rad]

    Returns
    -------
    traslated_positions : numpy.ndarray
        the shape is (2, N)

    '''
    if positions.shape[0] != 2:
        raise ValueError('the input data should have (2, N)')

    positions = np.array(positions)
    positions = positions.reshape(2, -1)

    rot_matrix = [[np.cos(base_angle), np.sin(base_angle)],
                  [-1*np.sin(base_angle), np.cos(base_angle)]]

    rot_matrix = np.array(rot_matrix)

    translated_positions = np.dot(rot_matrix, positions)

    return translated_positions

def coordinate_transformation_in_position(positions, base_positions):
    '''
    Transformation the coordinate in the positions

    Parameters
    -------
    positions : numpy.ndarray
        this parameter is composed of xs, ys
        should have (2, N) shape
    base_positions : numpy.ndarray
        this parameter is composed of x, y
        shoulg have (2, 1) shape

    Returns
    -------
    traslated_positions : numpy.ndarray, shape(2, N)

    '''

    if positions.shape[0] != 2:
        raise ValueError('the input data should have (2, N)')

    positions = np.array(positions)
    positions = positions.reshape(2, -1)
    base_positions = np.array(base_positions)
    base_positions = base_positions.reshape(2, 1)

    translated_positions = positions - base_positions

    return translated_positions

def coordinate_transformation_in_matrix_angles(positions, base_angles):
    '''
    Transformation the coordinate in the matrix angle

    Parameters
    -------
    positions : numpy.ndarray
        this parameter is composed of xs, ys
        should have (2, N) shape
    base_angle : float [rad]

    Returns
    -------
    traslated_positions : numpy.ndarray
        the shape is (2, N)

    '''
    if positions.shape[0] != 2:
        raise ValueError('the input data should have (2, N)')

    positions = np.array(positions)
    positions = positions.reshape(2, -1)
    translated_positions = np.zeros_like(positions)

    for i in range(len(base_angles)):
        rot_matrix = [[np.cos(base_angles[i]), np.sin(base_angles[i])],
                    [-1*np.sin(base_angles[i]), np.cos(base_angles[i])]]

        rot_matrix = np.array(rot_matrix)

        translated_position = np.dot(rot_matrix, positions[:, i].reshape(2, 1))

        translated_positions[:, i] = translated_position.flatten()

    return translated_positions.reshape(2, -1)

# def coordinate_inv_transformation
if __name__ == '__main__':
    positions_1 = np.array([[1.0], [2.0]])
    base_angle = 1.25

    translated_positions_1 = coordinate_transformation_in_angle(positions_1, base_angle)
    print(translated_positions_1)


import numpy

def functional_iteration(f, x0, max_steps=100, tol=1e-10):
    x = numpy.zeros(max_steps+1)
    x[0] = x0
    step = 0
    g = lambda x : x - f(x)
    while abs(f(x[step])) > tol and step < max_steps:
        step = step + 1
        x[step] = g(x[step-1])
    return x[:step+1]

def chord(f, x0, m, max_steps=100, tol=1e-10):
    x = numpy.zeros(max_steps+1)
    x[0] = x0
    step = 0
    g = lambda x : x - m * f(x)
    while abs(f(x[step])) > tol and step < max_steps:
        step = step + 1
        x[step] = g(x[step-1])
    return x[:step+1]

def newton(f, df, x0, max_steps=100, tol=1e-10):
    x = numpy.zeros(max_steps+1)
    x[0] = x0
    step = 0
    g = lambda x : x - f(x) / df(x)
    while abs(f(x[step])) > tol and step < max_steps:
        step = step + 1
        x[step] = g(x[step-1])
    return x[:step+1]

def secant(f, x0, x1, max_steps=100, tol=1e-10):
    x = numpy.zeros(max_steps+1)
    x[0] = x0
    x[1] = x1
    step = 1
    while abs(f(x[step])) > tol and step < max_steps:
        step = step + 1
        x[step] = x[step-1] - f(x[step-1]) * (x[step-1] - x[step-2]) / \
                    (f(x[step-1]) - f(x[step-2]))
    return x[:step+1]

if __name__=="__main__":
    def f(x):
        return x - numpy.cos(x)
    def df(x):
        return 1 + numpy.sin(x)

    x_func_iteration = functional_iteration(f, 0)
    print("Functional iteration")
    print("s={}, f(s)={}, in {} steps".format(x_func_iteration[-1],
          f(x_func_iteration[-1]), len(x_func_iteration)))
    x_chord = chord(f, 0, 1.08)
    print("Chord, m=1.08")
    print("s={}, f(s)={}, in {} steps".format(x_chord[-1],
          f(x_chord[-1]), len(x_chord)))
    x_chord = chord(f, 0, 0.8)
    print("Chord, m=0.8")
    print("s={}, f(s)={}, in {} steps".format(x_chord[-1],
          f(x_chord[-1]), len(x_chord)))
    x_newton = newton(f, df, 0)
    print("Newton")
    print("s={}, f(s)={}, in {} steps".format(x_newton[-1],
          f(x_newton[-1]), len(x_newton)))
    x_secant = secant(f, 0, 1)
    print("Secant")
    print("s={}, f(s)={}, in {} steps".format(x_secant[-1],
          f(x_secant[-1]), len(x_secant)))


from typing import Union
import numpy as np

def sigmoid_der(y: Union[int, float, np.array]) -> Union[int, float, np.array]:
    return np.multiply(np.subtract(1.0, y), y)

def tanh_derivative(y: Union[int, float, np.array]) -> Union[int, float, np.array]:
    return np.subtract(1.0, np.square(y))

def sigmoid(x: Union[int, float, np.array]) -> Union[int, float, np.array]:
    return np.divide(1.0, np.add(1.0, np.exp(np.negative(x))))

def relu(x: Union[int, float, np.ndarray]) -> Union[int, float, np.ndarray]:
    if isinstance(x, np.ndarray):
        y = x.copy()
        y[x < 0] = 0.0
        return y
    elif isinstance(x, float):
        return x if x > 0.0 else 0.0
    else:
        return x if x > 0 else 0

def relu_derivative(y: Union[int, float, np.ndarray]) -> Union[int, float, np.ndarray]:
    if isinstance(y, np.ndarray):
        dx = y.copy()
        dx[y > 0] = 1.0
        return y
    elif isinstance(y, float):
        return 1.0 if y > 0.0 else 0.0
    else:
        return 1 if y > 0 else 0

class ActivationFunctions:
    TANH = np.tanh
    SIGMOID = sigmoid
    RELU = relu

class ActivationFunctionsDerivatives:
    TANH_DERIVATIVE = tanh_derivative
    SIGMOID_DERIVATIVE = sigmoid_der
    RELU_DERIVATIVE = relu_derivative


import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf

from scipy.stats import probplot
import pylab

def mse(originais, previsoes):
  return np.sum( (originais - previsoes) ** 2 ) / len(originais)

def rmse(originais, previsoes):
  return np.sqrt( mse(originais, previsoes) )

def nrmse(originais, previsoes):
  amplitude = np.max(originais) - np.min(originais)
  return rmse(originais, previsoes) / amplitude

def mape(originais, previsoes):
  return (np.sum( np.abs(originais - previsoes) / np.abs(originais) ) / len(originais)) * 100

def u(originais, previsoes):
  n = len(originais)
  ret = 0
  for i in range(1, n):
    ret += np.abs(originais[i] - previsoes[i]) / np.abs(originais[i] - originais[i-1])
  return ret/n

def r2(originais, previsoes):
  mx = np.mean(originais)
  num = np.sum( (originais - previsoes) ** 2 )
  den = np.sum( (originais - mx) ** 2 )

  return 1 - num/den

def mde(originais, previsoes):
  a = np.sign( originais[1:] - previsoes[:-1] )
  b = np.sign( originais[:-1] - originais[1:] )
  return np.sum(np.where(a == b, 1, 0)) / len(originais)

def medir(originais, previsoes, ordem):
  return pd.DataFrame([[mse(originais[ordem:], previsoes), rmse(originais[ordem:], previsoes), \
                       nrmse(originais[ordem:], previsoes), mape(originais[ordem:], previsoes), \
                       u(originais[ordem:], previsoes), r2(originais[ordem:], previsoes),
                       mde(originais[ordem:], previsoes)]], \
                      columns=['MSE','RMSE','nRMSE','MAPE','U','R2','MDE'] )

def analise_residuos(originais, previsoes, ordem):

  residuos = originais[ordem:] - previsoes

  fig, ax = plt.subplots(1, 3, figsize=(15, 5))

  # ACF
  ax[0].plot(residuos)
  ax[0].set_title("Resíduos")

  # ACF
  plot_acf(residuos, lags=20, ax=ax[1])
  ax[1].set_title("ACF")

  # Q-Q Plot
  probplot(residuos, dist="norm", plot=ax[2])
  ax[2].set_title("Quantil-Quantil")

  plt.tight_layout()

import numpy,from scipy,from statsmodels


from scipy.optimize import minimize
import numpy as np
from numpy.linalg import norm, inv
import sys

def func(N, vec):
    x1, x2 = vec
    return  2 * N * x1 * x1 + 3 * N * x2 * x2 - 8 * N * x1 + 6 * N * x2 + 7

def gradient(N, vec):
    x1, x2 = vec
    return [4 * N * x1 - 8 * N, 6 * N * x2 + 6 * N]

def hessian(N, vec):
    return np.array([[4 * N, 0.0], [0.0, 6 * N]])

def is_pos_def(A):
    try:
        np.linalg.cholesky(A)
        return True
    except LinAlgError:
        return False

def main():
    N = int(sys.argv[1])
    M = int(sys.argv[2])
    x_curr = [0.0, 0.0]
    x_next = x_curr.copy()
    res = x_next.copy()
    hess = []
    eps = 0.9
    was_first_check_succ = False

    for k in range(M):
        x_curr = x_next
        print(f'x_{k} = {x_curr}')
        grad = gradient(N, x_curr)
        print(f'grad(f(x_{k})) = {grad}')
        norm_grad = norm(grad)
        print(f'||grad(f(x_{k}))|| = {norm_grad}')
        if norm_grad <= eps:
            res = x_curr
            print(f'norm of gradient is < eps.\nResult is:\n{res}')
            break

        hess = hessian(N, x_curr)
        print(f'H(x) =\n{hess}')
        hess_inv = inv(hess)
        print(f'H(x)^(-1) =\n{hess_inv}')
        d = grad
        t = np.inf

        if is_pos_def(hess_inv):
            print(f'H(x) > 0')
            d = -hess_inv @ grad
            t = 1.0

        else:
            print(f'H(x) ≯ 0')
            d = -grad
            param_f = lambda t: func(N, x_curr + t * d)
            t = minimize(param_f, x_curr).x

        print(f'd = {d}\nt = {t}')
        x_next = x_curr + t * d
        print(f'x_{k + 1} = {x_next}')

        vec_diff_norm = norm(x_next - x_curr)
        print(f'||x_{k + 1} - x_{k}|| = {vec_diff_norm}')
        func_diff_norm = norm(func(N, x_next) - func(N, x_curr))
        print(f'||f(x_{k + 1}) - f(x_{k})|| = {func_diff_norm}')

        if  vec_diff_norm < eps and func_diff_norm < eps:
            if was_first_check_succ == False:
                print("First check passed")
                was_first_check_succ = True
            elif was_first_check_succ == True:
                res = x_next
                print(f'Second check passed result is:\n{res}')
                exit()
            else:
                print("First check failed")
                was_first_check_succ = False

    exit()

if __name__ == "__main__":
    main()


import numpy as np
import copy
import scipy
import scipy.linalg

def gaussElimin(a,b):
    L = [[0 for i in range(len(a))]for j in range(len(a))] #inicializar matriz de ceros para la trinagular inferior

    n = len(b)
# Eliminación
    for j in range(0,n-1): # colunmas
        for i in range(j+1,n): # renglones
            if a[i,j] != 0.0:
                lam = a[i,j]/a[j,j]
                L[i][j] = lam
                a[i,j:n] = a[i,j:n] - lam*a[j,j:n]
                b[i] = b[i] - lam*b[j]
    # Sustitución regresiva
    for i in range(n-1,-1,-1):
        b[i] = (b[i] - np.dot(a[i,i+1:n],b[i+1:n]))/a[i,i]
    return b, L, a

def Imprime(M):
    for l in M:
        print(l)
    print("\n")

if __name__ == "__main__":

    a = np.array([[4,1,2,-3,5], [-3,3,-1,4,-2], [-1,2,5,1,3], [5,4,3,-1,2],[1,-2,3,-4,5.]])
    a_copy = copy.deepcopy(a)

    b = np.array ([-16, 20, -4, -10, 3.])[np.newaxis]
    b = b.T
    b_copy = copy.deepcopy(b)

    L = []

    x, L, a = gaussElimin(a,b)
    # P, L2, U = scipy.linalg.lu(a_copy)
    for i in range (0, len(a)):
        L[i][i] = 1
    print("La matriz de eliminacion es:")
    Imprime(L)

    print (f"La matriz A resultante es: \n")
    Imprime(a)

    print ("El vector b original", "\n",b_copy)
    print("\n")
    print("La solucion al sistema: ","\n", x, "\n")

    print(f"A = LU {np.dot(L,a)} \n")
    print (f"la comprobacion del sistema es: \n {np.linalg.solve(a_copy,b_copy)}")

#%%


import numpy as np
import math

def is_prime(n: int) -> bool:
    """Return if n is a prime number."""
    if n > 2 and n % 2 == 0:
        return False
    return n == 2 or np.all([n % x for x in range(3, int(np.sqrt(n)) + 1)])

def prime_factors(n: int) -> list:
    prime_factors_list = []
    for i in range(2,int(math.sqrt(n))+1):
        while n % i == 0:
            n /= i
            prime_factors_list.append(i)

    if n > 2:
        prime_factors_list.append(n)
    return prime_factors_list


import streamlit as st
import plotly.graph_objects as go
import numpy as np

st.title('Superquadrics')
st.sidebar.subheader('Formulas')
# st.latex(r'\left|\frac{x}{a}\right|^s + \left|\frac{y}{b}\right|^t + \left|\frac{z}{c}\right|^u = 1')
# st.markdown('where $r$, $s$, and $t$ are positive real numbers that determine the main features of the superquadric, and $a$, $b$, and $c$ are scaling parameters.')
st.sidebar.latex(r'\left(\left|\frac{x}{a}\right|^{n_2} + \left|\frac{y}{b}\right|^{n_2}\right)^\frac{n_1}{n_2} + \left|\frac{z}{c}\right|^{n_1} = 1')
st.sidebar.markdown('where $n_1$ and $n_2$ are shape parameters, and $a$, $b$, and $c$ are scaling parameters.')
# st.subheader('Formula python code')
# with st.echo():
import numpy as np
@st.cache
def superquadrics(a,b,c,n1,n2,x,y,z):
    values = np.power(np.power(np.abs(x/a),n2) \
                    + np.power(np.abs(y/b),n2),n1/n2) \
            + np.power(np.abs(z/c),n1)
    return values
X, Y, Z = np.mgrid[-1:1:20j, -1:1:20j, -2:2:40j]
st.sidebar.subheader('Parameters')
a  = 1.
b  = 1.
c  = st.sidebar.slider('c (a=b=1)', .2, 2., 1., .2)
n1 = st.sidebar.slider('n1', 1, 10, 10, 1)
n2 = st.sidebar.slider('n2', 1, 10, 2, 1)
fig = go.Figure(data=go.Isosurface(
    x = X.flatten(),
    y = Y.flatten(),
    z = Z.flatten(),
    value = superquadrics(a,b,c,n1,n2,X,Y,Z).flatten(),
    isomin = .5,
    isomax = 1,
    showscale = False,
))
fig.update_layout(
    scene = dict(
        xaxis_title='',
        yaxis_title='',
        zaxis_title='',
        xaxis = dict(
            showbackground = False,
            showline = False,
            showticklabels = False,
        ),
        yaxis = dict(
            showbackground = False,
            showline = False,
            showticklabels = False,
        ),
        zaxis = dict(
            showbackground = False,
            showline = False,
            showticklabels = False,
        ),
    )
)
st.write(fig)
st.subheader('Code')
st.markdown('* https://github.com/ken2s/superquadrics/blob/main/st_superquadrics.py')
st.subheader('References')
st.markdown('* https://en.wikipedia.org/wiki/Superquadrics')


# -*- coding: utf-8 -*-
"""
Exercise 03: Chapter 03, Kinder & Nelson

A common way to determine the value of a function is to sum over a series.
For example, the Maclaurin series for sin(x) is

    sin(x) = x - x**3/3! + x**5/5! - x**7/7! + ...

Perform a series expansion to derive the equation above. Next, write down
a general expression for the sum of the series that is valid between n = 0
and n = N, where N ≥ 0. This will serve as your algorithm for summing
the series.

One problem with the algorithm is that we do not know which value
of N is suitable when calcualting the series. Instead of guessing, have
your code proceed with the summation until the Nth term contributes a
negligible amount to the final summation, say 1 part in 10**8.

Before writing any lines of code, discuss an approach with your neighbor
and write out on paper how your code should proceed. Code up your approach
in Spyder once you're done.

Here are your tasks:

   1. Perform a Maclaurin series expansion of the function sin(x) to
      derive the equation in the README.
   2. Derive a generalized, finite summation form for the series based
      on your Maclaurin series expansion.
   3. Discuss with your neighbor about how to approach coding the problem
      and write out on paper how you code should proceed.
   4. Code your approach in Spyder once you are finished.
   5. Show that, for small values of x, the series converges.
   6. Which value for N was required to reach the desired precision and
      obtain convergence?
   7. Compare your results to the value determined using NumPy's sine
      function.
   8. Steadily increase x and write down the relative error between your
      calculated value for sin(x) and the NumPy function's value.
   9. What do you notice about the relative error?
  10. Will there be a time when the series does not converge? Make a plot
      of the relative error vs x to support your answer.

Created on Tue Aug 20 11:02:00 2019

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

# below is my basic approach for finding sin (x)

# first, we need to set our x at what we are trying to solve. Here, I have set x
# equal to 200. A randomly chosen, yet large, number.
# At the same time, we must make sure that n is set to zero, so the MacLaurin series
# can properly sum from the 0th term.

x, n = 200, 0
sum = 0

# As I will show afterwards, large values of x will blow up and be impossible for
#a computer to calculate. However, as sin bounces between -1 and 1 around the unit circle,
# we can simply subtract 2pi a number of times, until x is sufficiently small, and we
# will get the same result.

while (x>2*np.pi):
    x = x-(2*np.pi)

# Here is our MacLaurin series. I have set it up, as instructed, to only calculate until the
# term is sufficiently small. We have also created a variable, "sum", that will add up the terms
# this variable will be the result for the value of sin (x) we are calculating.

while (np.absolute((((-1)**n/(np.math.factorial(2*n+1))) * x**(2*n+1)))>(1/10**8)):

    a = (((-1)**n/(np.math.factorial(2*n+1))) * x**(2*n+1))
    sum+=a
    n = n+1

print(sum)
# As stated previously, this will not work for large values of x.

#If we just run

x, n, sum, = 50, 0, 0

while (np.absolute((((-1)**n/(np.math.factorial(2*n+1))) * x**(2*n+1)))>(1/10**8)):

    a = (((-1)**n/(np.math.factorial(2*n+1))) * x**(2*n+1))
    sum+=a
    n = n+1

# we will wither get a result that falls outside of the bounds of sin, or receive an error
#because the computer cannot handle the calculation.
# This one happens to be calculable, but is well outside the bounds of sin.

print(sum)

#So, if we put our code to "shrink" x, back in:

while (x>2*np.pi):
    x = x-(2*np.pi)

#x becomes much smaller, and we are able to calculate it within the bounds of x.

# To know how accurate our MacLaurin series is, we can compare the result with the np.sin() function

sin = np.sin(x)

error = sum-sin
print (error)

# By gradually increasing x, we find that around the x value of 35 is where the summation really
# starts to separate itself from the actual sin() function. Prior to this point, the error was very,
# very small, on the order of 10^-9. Between 35 and 40, it has separated itself enough that the
#value we get using the MacLaurin series is outside of the bounds of the actual sin function.


#------------------------------------------------------------------------------------------
#   QR Factorization of Matrix A In Python 3.8.1 Numpy 1.19.2
#
#   The following sample demonstrates the QR factorization of
#   a randomly generated matrix A of real or complex numbers using:
#
#        * Gram-Schmidt Orthogonalization;
#        * Householder Reflections;
#
#   , and surveys the complexity and performance (single-threaded) of these both methods
#
#   GNU Public License (C) 2021 Arthur V. Ratz
#------------------------------------------------------------------------------------------

import time
import math
import random
import pandas as pd

import numpy as np
import numpy.linalg as lin

from qr_gschmidt import *
from qr_gs_schwrt import *
from qr_householder import *

mat_shape      = { 'min': 3,   'max': 15   }
mat_shape_perf = { 'min': 750, 'max': 950  }

qr_alg         = [ { 'alg': qr_gs,       'name': 'Gram-Schmidt       ' },
                   { 'alg': qr_gs_modsr, 'name': 'Schwarz-Rutishauser' },
                   { 'alg': qr_hh,       'name': 'Householder        ' } ]

mat_types      = [ 'real   ', 'complex' ]

checkup_status = [ 'failed', 'passed' ]

checkup_banner = "\n[ Verification %s... ]"
stats_banner   = "%s Matrix A Statistics:\n"
qr_test_banner = "\nQR Factorization Of A `%s` Matrix Using %s Algorithm:"
survey_banner  = "Matrix: %s    WINS: [ %s : %d secs  ] LOOSES: [ %s : %d secs ]"
perf_stats     = "%s : [ type: `%s` exec_time: %d secs verification: %s ]"

app_banner     = "QR Factorization v.0.0.1 CPOL License (C) 2021 by Arthur V. Ratz"

# Function: perf(A, qr, type=complex) evaluates the qr factorization method's execution wall-time in nanoseconds,
#           returns the tuple of the resultant matrices Q,R and the execution time

def perf(A, qr, type=complex):
    t_d = time.time(); Q,R = qr(A, type); \
        return Q, R, (time.time() - t_d)

def check(M1, M2):
    v1 = np.reshape(M1,-1)
    v2 = np.reshape(M2,-1)
    if len(v1) != len(v2):
       return False
    else: return 0 == len(np.where(np.array(\
       [ format(c1, '.4g') == format(c2, '.4g') \
        for c1,c2 in zip(v1, v2) ]) == False)[0])

def rand_matrix(rows, cols, type=complex):
    np.set_printoptions(precision=8)
    if type == complex:
        return np.reshape(\
              np.random.uniform(1, 10, rows * cols) + \
              np.random.uniform(-10, 10, rows * cols) *  1j, (rows, cols))
    else: return np.reshape(10 * np.random.uniform(\
            0.01, 0.99, rows * cols), (rows, cols))

def print_matrix(M, alias):
    np.set_printoptions(\
        precision=2, suppress=True, \
        formatter='complexfloat')
    if isinstance(M, complex):
        eps = np.finfo(float).eps; tol = 100
        M = [np.real(m) if np.imag(m)<tol*eps else m for m in M]
        M = [np.asscalar(np.real_if_close(m)) for m in M]
    print("\nMatrix %s (%dx%d):" % \
        (alias, len(M), len(M[0])),"\n")
    pd.set_option('precision', 2); \
        df = pd.DataFrame(M)
    df = df.to_string(index=False).replace('j','i')
    print(df)

def logo():
    print(app_banner)
    print(''.join(['=' for p in range(len(app_banner))]))

def qr_demo(s, qr, type=complex):

    print(qr_test_banner % (\
        "Complex" if type == complex \
            else "Real", s.replace(' ', '')))

    rows = np.random.randint(\
        mat_shape['min'], mat_shape['max'])

    cols = np.random.randint(\
        mat_shape['min'], mat_shape['max'])

    A = rand_matrix(rows, cols, type); print_matrix(A, "A")

    Q,R,T = perf(A, qr, type)

    status = check(A, Q.dot(R))

    A = np.around(A, decimals=2)
    Q = np.around(Q, decimals=2)
    R = np.around(R, decimals=2)

    print_matrix(Q, "Q")
    print_matrix(R, "R")

    print(checkup_banner % (checkup_status[status]),"\n")

    return status

def qr_perf():

    print ("\nPerformance Assessment:")
    print ("=======================================")

    rows = np.random.randint(\
        mat_shape_perf['min'], mat_shape_perf['max'])

    cols = np.random.randint(\
        mat_shape_perf['min'], mat_shape_perf['max'])

    d = np.random.randint(5)
    cols += d if random.uniform(0,1) > 0.5 else -d

    A = [ rand_matrix(rows, cols, float), \
          rand_matrix(rows, cols, complex) ]

    print ("\nMatrix A (%d x %d):" % (rows, cols))
    print ("============================\n")

    exec_time = np.zeros((len(mat_types), len(qr_alg)))
    survey = np.zeros((len(mat_types), 1), dtype=object)

    status = 0
    for s_j in range(len(mat_types)):
        for s_i in range(len(qr_alg)):
            qr, name = \
                qr_alg[s_i]['alg'], qr_alg[s_i]['name']; \
                Q, R, exec_time[s_j][s_i] = perf(A[s_j],   \
                    qr, float if s_j % len(mat_types) == 0 else complex); \
                status = checkup_status[check(A[s_j], Q.dot(R))]
            print(perf_stats % (name, mat_types[s_j].replace(' ','').lower(), \
                exec_time[s_j][s_i], status))

            if status == "failed": break

        if status == "failed":
           print_matrix(A[s_j], "A")
           print_matrix(Q, "Q")
           print_matrix(R, "R")

           print("\n*** FAILURE!!! ****\n")

           break

        wr_time, lr_time = \
            np.min(exec_time[s_j]), \
            np.max(exec_time[s_j])

        wi = np.where(exec_time[s_j] == wr_time)[0][0]
        li = np.where(exec_time[s_j] == lr_time)[0][0]

        s_w = qr_alg[wi]['name']; s_l = qr_alg[li]['name']

        survey[s_j] = { 'alg_w': s_w, 'tm_w': wr_time, \
                        'alg_l': s_l, 'tm_l': lr_time }

        print("\n")

    for s_j in range(len(mat_types)):
        print(survey_banner % (mat_types[s_j].lower(),
              survey[s_j][0]['alg_w'], survey[s_j][0]['tm_w'], \
              survey[s_j][0]['alg_l'], survey[s_j][0]['tm_l']))

def main():

    logo()

    np.random.seed(int(time.time()))

    status = 0
    for s_j in range(len(qr_alg)):
        for s_i in range(len(mat_types)):
            status = qr_demo(qr_alg[s_j]['name'], \
                qr_alg[s_j]['alg'], complex if s_i % 2 else float)
            if status == False: break

        if status == False:
           print("\n*** FAILURE!!! ****\n"); break

    qr_perf(); print("\n")

if __name__ == "__main__":
    main()