import math
import numpy as np
from collections import Counter
#-------------------------------------------------------------------------

#-----------------------------------------------
#    Utility functions
#-----------------------------------------------

#--------------------------
def entropy(Y):
    '''
        Compute the entropy of a list of values.
        Input:
            Y: a list of values, a numpy array of int/float/string values.
        Output:
            e: the entropy of the list of values, a float scalar
    '''

    n = len(Y) # total number of values
    c = Counter(Y) # create a counter on the list
    e = 0.
    for k,v in c.items():
        p = v/n
        e -=  p*math.log(p,2)
    return e
#--------------------------
def conditional_entropy(Y,X):
    '''
        Compute the conditional entropy of y given x.
        Input:
            Y: a list of values, a numpy array of int/float/string values.
            X: a list of values, a numpy array of int/float/string values.
        Output:
            ce: the conditional entropy of y given x, a float scalar
    '''
    n = len(Y) # total number of values
    c = Counter(X) # create a counter on the list
    ce = 0.
    for k,v in c.items():
        ce += entropy(Y[X==k])*v/n
    return ce
#--------------------------
def information_gain(Y,X):
    '''
        Compute the information gain of y after splitting over attribute x
        Input:
            X: a list of values, a numpy array of int/float/string values.
            Y: a list of values, a numpy array of int/float/string values.
        Output:
            g: the information gain of y after splitting over x, a float scalar
    '''
    g = entropy(Y) - conditional_entropy(Y,X)
    return g

#-----------------------------------------------
#     A Decision Tree Node
#-----------------------------------------------
class Node:
    '''
        The class of a Decision Tree Node

        Properties
            isleaf: whether or not this node is a leaf node, a boolean scalar
            p: the label to be predicted on the node (i.e., most common label in the node).
            i: the index of the attribute being tested in the node, an integer scalar
            th: the threshold on the attribute, a float scalar.
            C1: the first child node for values smaller than threshold
            C2: the second child node for values larger than threshold
    '''
    #--------------------------
    def __init__(self,X,Y):
        '''
        Create a decision tree node
        Inputs:
            X: the data instances in the node, a numpy matrix of shape p by n.
               Each element can be int/float.
               Here n is the number data instances in the node, p is the number of attributes.
               Each row of X represents one attribute, each column of X represents a data instance.
            Y: the class labels in the node, a numpy array of length n.
               Each element can be int/float/string.
        '''
        assert len(X)>0
        assert len(Y)>0
        self.X = X
        self.Y = Y
        # compute the most common label in the node for prediction
        self.p = Counter(Y).most_common(1)[0][0]
        # test whether or not if the node is a leaf node
        self.isleaf = True
        for f in X:
            if not len(np.unique(f))==1:
                self.isleaf = False
                break
        if (len(np.unique(Y))==1):
            self.isleaf = True

    #--------------------------
    def cutting_points(self,i):
        '''
            Find all possible cutting points in the i-th attribute.
            (1) sort unique attribute values in X, like, x1, x2, ..., xn
            (2) consider splitting points of form (xi + x(i+1))/2
            (3) only consider splitting between instances of different classes
            Input:
                i: the index of the attribute to be used, an integer scalar
            Output:
                cp: the list of  potential cutting points, a float numpy vector.
        '''
        x = self.X[i] # the i-th attribute
        cp = []
        z = sorted(np.unique(x)) # unique values in the attribute
        for i in range(len(z)-1):
            idx = np.logical_or(x==z[i], x==z[i+1])
            ys = self.Y[idx]
            if len(np.unique(ys))>1:
                cp.append((z[i] + z[i+1])/2.) # add a candidate cutting point
        return cp

    #--------------------------
    def best_threshold(self,i):
        '''
            Find the best threshold among all possible cutting points in the i-th attribute.
            Input:
                i: the index of the attribute to be used, an integer scalar
            Output:
                th: the best threshold, a float scalar.
                g: the information gain by using the best threshold, a float scalar.
        '''
        X = self.X[i] # the i-th attribute
        cp = self.cutting_points(i)
        assert len(cp)>0
        g= -math.inf
        for c in cp:
            x = X>=c
            g_new = information_gain(self.Y,x)
            if g_new>g:
                g = g_new
                th = c
        return th,g
    #--------------------------
    def best_attribute(self):
        '''
            Find the best attribute to split the node. The attributes have continuous values (int/float).
            Here we use information gain to evaluate the attributes.
            Output:
                i: the index of the attribute to split, an integer scalar
                th: the threshold of the attribute to split, a float scalar
        '''
        g= -math.inf
        for k in range(self.X.shape[0]):
            if len(np.unique(self.X[k]))>1:
                tk,gk = self.best_threshold(k)
                if gk>g:
                    g = gk
                    self.i = k
                    self.th = tk

    #--------------------------
    def add_children_nodes(self):
        '''
            build the children nodes C1 and C2 with the best attribute and threshold in the node
            Output:
                C1: the child node for values smaller than threshold
                C2: the child node for values larger than (or equal to) threshold
        '''
        self.best_attribute()
        i,th = self.i, self.th
        x = self.X[i]
        self.C1 = Node(self.X[:,x<th], self.Y[x<th])
        self.C2 = Node(self.X[:,x>=th], self.Y[x>=th])

    #--------------------------
    def build_tree(self):
        '''
            Recursively build a subtree from the current tree node.
        '''
        if self.isleaf==False:
            self.add_children_nodes()
            self.C1.build_tree()
            self.C2.build_tree()
    #--------------------------
    def predict_1(self,x):
        '''
            Using the decision tree starting from the current node to predict the label on one test instance
            Input:
                x: the attribute vector, a numpy vector of shape p.
                   Each attribute value can be int/float
            Output:
                p: the label prediction on the test instance, a scalar, can be int/float/string.
        '''
        if self.isleaf:
            return self.p
        if x[self.i] <= self.th:
            return self.C1.predict_1(x)
        else:
            return self.C2.predict_1(x)

#-----------------------------------------------
#     A Decision Tree
#-----------------------------------------------
class DecisionTree:
    '''
        The Class of Decision Tree
        Properties
            root: the root node of the decision tree
    '''
    #--------------------------
    def __init__(self,X,Y):
        '''
        Create a decision tree
        Inputs:
            X: the data instances in the node, a numpy matrix of shape p by n.
               Each element can be int/float.
               Here n is the number data instances in the node, p is the number of attributes.
               Each row of X represents one attribute, each column of X represents a data instance.
            Y: the class labels in the node, a numpy array of length n.
               Each element can be int/float/string.
        '''
        self.root = Node(X,Y)  # create root node
        self.root.build_tree() # build the tree

    #--------------------------
    def predict_1(self,x):
        '''
            Using the decision tree to predict the label on one test instance
            Input:
                x: the attribute vector, a numpy vector of shape p.
                   Each attribute value can be int/float
            Output:
                p: the label prediction on the test instance, a scalar, can be int/float/string.
        '''
        return self.root.predict_1(x)

    #--------------------------
    def predict(self,X):
        '''
            Using the decision tree to predict the label on all test instances
            Input:
                X: the feature matrix of all test instances, a numpy matrix of shape p by n.
                   Each element can be int/float.
                   Here n is the number data instances in the dataset, p is the number of attributes.
            Output:
                P: the predicted class labels on all test instances, a numpy array of length n.
                   Each element can be int/float/string.
        '''
        return np.array([self.predict_1(x) for x in X.T])


import numpy as np

#Questions on NumPy Statistics
# Compute the median of the flattened NumPy array
#median Compute the median along the specified axis.
np.median(np.array([1, 2, 3, 4, 5, 6, 7]))

# Find Mean of a List of Numpy Array
#mean Compute the arithmetic mean along the specified axis.
np.mean(np.array([1, 2, 3]))

# Calculate the mean of array ignoring the NaN value
#nanmean Compute the arithmetic mean along the specified axis, ignoring NaNs.
np.nanmean(np.array([[20, 15, 37], [47, 13, np.nan]]))

# Get the mean value from given matrix
#matrix.mean Return mean value from given matrix
np.matrix('[64, 1; 12, 3]').mean()

# Compute the variance of the NumPy array
# var Return Variance of the array (a scalar value if axis is none)
# or array with variance values along specified axis.
np.var([20, 2, 7, 1, 34]  , dtype = np.float32)

# Compute the standard deviation of the NumPy array
# std Return tandard Deviation of the array (a scalar value if axis is none)
# or array with standard deviation values along specified axis.
np.std([20, 2, 7, 1, 34]  , dtype = np.float32)

# Compute pearson product-moment correlation coefficients of two given NumPy arrays
# corrcoef Pearson product-moment correlation coefficients
np.corrcoef( np.array([0, 1, 2]), np.array([3, 4, 5]))

# Calculate the mean across dimension in a 2D NumPy array
np.mean(np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]), axis=1)

# Calculate the average, variance and standard deviation in Python using NumPy
# average Return Default is False. If True, the tuple is returned, otherwise
# only the average is returned
np.average([2, 4, 4, 4, 5, 5, 7, 9] )
np.var([20, 2, 7, 1, 34]  , dtype = np.float32)
np.std([20, 2, 7, 1, 34]  , dtype = np.float32)

# Describe a NumPy Array in Python
# amin takes a NumPy array as an argument and returns the minimum
# amax takes a NumPy array as an argument and returns maximum.
# ptp takes a NumPy array as an argument and returns the range of the data.
arr = np.array([4, 5, 8, 5, 6, 4, 9, 2, 4, 3, 6])
# measures of central tendency
mean = np.mean(arr)
median = np.median(arr)
  # measures of dispersion
np.amin(arr)
np.amax(arr)
np.ptp(arr)
np.var(arr)
np.std(arr)

NumpyTutorial/Questions on NumPy Statistics.py


import cupy

# Collection of activation functions
# Reference: https://en.wikipedia.org/wiki/Activation_function

class Sigmoid():
    def __call__(self, x):
        return 1 / (1 + cupy.exp(-x))

    def gradient(self, x):
        return self.__call__(x) * (1 - self.__call__(x))

class Softmax():
    def __call__(self, x):
        e_x = cupy.exp(x - cupy.max(x, axis=-1, keepdims=True))
        return e_x / cupy.sum(e_x, axis=-1, keepdims=True)

    def gradient(self, x):
        p = self.__call__(x)
        return p * (1 - p)

class TanH():
    def __call__(self, x):
        return 2 / (1 + cupy.exp(-2*x)) - 1

    def gradient(self, x):
        return 1 - cupy.power(self.__call__(x), 2)

class ReLU():
    def __call__(self, x):
        return cupy.where(x >= 0, x, 0)

    def gradient(self, x):
        return cupy.where(x >= 0, 1, 0)

class LeakyReLU():
    def __init__(self, alpha=0.2):
        self.alpha = alpha

    def __call__(self, x):
        return cupy.where(x >= 0, x, self.alpha * x)

    def gradient(self, x):
        return cupy.where(x >= 0, 1, self.alpha)

class ELU():
    def __init__(self, alpha=0.1):
        self.alpha = alpha

    def __call__(self, x):
        return cupy.where(x >= 0.0, x, self.alpha * (cupy.exp(x) - 1))

    def gradient(self, x):
        return cupy.where(x >= 0.0, 1, self.__call__(x) + self.alpha)

class SELU():
    # Reference : https://arxiv.org/abs/1706.02515,
    # https://github.com/bioinf-jku/SNNs/blob/master/SelfNormalizingNetworks_MLP_MNIST.ipynb
    def __init__(self):
        self.alpha = 1.6732632423543772848170429916717
        self.scale = 1.0507009873554804934193349852946

    def __call__(self, x):
        return self.scale * cupy.where(x >= 0.0, x, self.alpha*(cupy.exp(x)-1))

    def gradient(self, x):
        return self.scale * cupy.where(x >= 0.0, 1, self.alpha * cupy.exp(x))

class SoftPlus():
    def __call__(self, x):
        return cupy.log(1 + cupy.exp(x))

    def gradient(self, x):
        return 1 / (1 + cupy.exp(-x))


# -*- coding: utf-8 -*-
"""
Linear regression example using least squares from COMP551 lecture 2
@author: Demetrios Koziris
"""

import numpy as np
from numpy.linalg import inv
import matplotlib.pyplot as plt

"""
Least-Squares solution
"""
def least_squares(order=1, save_plots=False):
    #print('Least-Squares:\n')

    x = [0.86, 0.09, -0.85, 0.87, -0.44, -0.43, -1.1, 0.40, -0.96, 0.17]
    y = [2.49, 0.83, -0.25, 3.10, 0.87, 0.02, -0.12, 1.81, -0.83, 0.43]

    # input matrix of feature vectors
    X = np.array([np.power(x,p) for p in range(order+1)]).T
    # target vector
    Y = np.array(y).T

    Xt_X = X.T @ X
    # print(f'X^tX = \n{Xt_X}\n')
    Xt_Y = X.T @ Y
    # print(f'X^tY = \n{Xt_Y}\n')
    w =  inv(Xt_X) @ Xt_Y
    # print(f'W = (X^tX)^-1(X^tY) = \n{w}\n')

    def fit_function(x):
        return sum([weight*(x**p) for p,weight in enumerate(w)])

    def fit_function_label():
        terms = [f'{weight:.2f}x^{p}' for p,weight in enumerate(w)]
        return '$y = ' + ' + '.join(terms[::-1]) + '$'

    plot_title = f'Least-Squares Linear Regression Order {order}'
    x_interval = np.linspace(-1.6, 1.6, 1000)
    plt.plot(x_interval, fit_function(x_interval), 'r', label=fit_function_label())
    plt.scatter(x, y, marker='x')
    plt.xlim(-1.6, 1.6);
    plt.ylim(-2, 5);
    plt.title(plot_title)
    plt.legend(loc=2)
    if (save_plots):
        plt.savefig(plot_title.lower().replace(' ','_'), bbox_inches="tight")
    plt.show()

for i in range(10):
    least_squares(i, True)


# --------------

# Code starts here
#The adjacency matrix adj_mat is provided to you.
import numpy as np
# Adjacency matrix
adj_mat = np.array([[0,0,0,0,0,0,1/3,0],
                   [1/2,0,1/2,1/3,0,0,0,0],
                   [1/2,0,0,0,0,0,0,0],
                   [0,1,0,0,0,0,0,0],
                  [0,0,1/2,1/3,0,0,1/3,0],
                   [0,0,0,1/3,1/3,0,0,1/2],
                   [0,0,0,0,1/3,0,0,1/2],
                   [0,0,0,0,1/3,1,1/3,0]])

#For this matrix perform the eigen vector decomposition using .linalg.eig() method of numpy. This function returns a tuple and save them as eigenvalues and eigenvectors
eigenvalues,eigenvectors=np.linalg.eig(adj_mat)

#The following is a single step divided into small steps:
#Find the eigen vector corresponding to 1 from eigenvectors (first column of eigenvectors), that is abs(eigenvectors[:,0])
#Normalize this by dividing with np.linalg.norm(eigenvectors[:,0],1). Save it as eigen_1
eigen_1= abs(eigenvectors[:,0])/np.linalg.norm(eigenvectors[:,0],1)

#Next save the most important page number by finding the index with highest value within eigen_1. This can be done by using the .where() method
#from numpy Save it as page and print it out.
page=np.where(eigen_1==eigen_1.max())[0][0]+1
print(page)
# Code ends here

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

# Initialize stationary vector I
#The hyperlink matrix adj_mat is already defined for you. Initialize a stationary matrix init_I which has 1 at the first position and 0s in the rest 7 blocks of the numpy ndarray.
init_I=np.array([1,0,0,0,0,0,0,0])

#Use a for loop over 10 iterations where you update adj_mat according to the rule I^{k+1} = HI^k
# this can be done by .dot(adj_mat, init_I) . Also normalize init_I at every iteration using np.linalg.norm(init_I, 1)
# Perform iterations for power method
for i in range(10):
  init_I=np.dot(adj_mat, init_I)/np.linalg.norm(init_I, 1)

#Save the page number with highest importance as power_page. This can be found by .where() as done in the previous task.
power_page=np.where(init_I==init_I.max())[0][0]+1
print(power_page)

# Code ends here

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

#Problem with power method
#The new adjacency matrix this time for the new webpage connection structure shown in the above image. It is provided as new_adj_mat
# New Adjancency matrix
new_adj_mat = np.array([[0,0,0,0,0,0,0,0],
                   [1/2,0,1/2,1/3,0,0,0,0],
                  [1/2,0,0,0,0,0,0,0],
                   [0,1,0,0,0,0,0,0],
                   [0,0,1/2,1/3,0,0,1/2,0],
                   [0,0,0,1/3,1/3,0,0,1/2],
                   [0,0,0,0,1/3,0,0,1/2],
                   [0,0,0,0,1/3,1,1/2,0]])

#Initialize a stationary matrix new_init_I in the same manner as you did for the previous task
new_init_I=np.array([1,0,0,0,0,0,0,0])

#Use a for loop to iterate 10 times and update in the similar manner as you did for the previous task i.e. first take dot product np.dot(new_adj_mat, new_init_I)
#and then normalize as done in previous task.
# Perform iterations for power method
for i in range(10):
  new_init_I=np.dot(new_adj_mat, new_init_I)/np.linalg.norm(new_init_I, 1)

#Print out new_init_I to check out its result. Observe how you get pagerank value for 3rd webpage as zero. Is it not possible as it has incoming connections.
print(new_init_I)

# Code ends here

# --------------
#Code Starts here
# Alpha value
alpha = 0.85

# Code starts here
# Modified adjancency matrix
#Initialize new hyperlink matrix G with the help of the mathematical formula given above. In the formula n can be taken as len(new_adj_mat))
#and 1 as np.ones(new_adj_mat.shape.Save it as G
n=len(new_adj_mat)
S=np.ones(new_adj_mat.shape)/n
I=np.ones(new_adj_mat.shape)
#G=αS+(1−α)*(1/n)*I
G=alpha*S+(I-alpha)*(1/n)

#Initialize stationary vector as final_init_I consisting of 1 at its beginning and rest all zeros in a 1D NumPy array
final_init_I=np.array([1,0,0,0,0,0,0,0])

#Perform 1000 iterations using a for loop to update the stationary vector in the same manner as for the Power Method. Also, do not forget to normalize it.
for i in range(1000):
  final_init_I=np.dot(G, final_init_I)/np.linalg.norm(final_init_I, 1)

#Print out final_init_I
print(final_init_I)

# Code ends here


# a simple cubic spline example.
#
# generate some random data in 10 intervals -- note the data changes
# each time this is run.
#
# Our form of the spline polynomial comes from Pang, Ch. 2
#
# solve the matrix system for the splines
#
# plot the splines
#
# M. Zingale (2013-02-10)

import numpy
import pylab
import math
from scipy import linalg   # scipy modules need to be imported separately
from scipy import interpolate   # scipy modules need to be imported separately

# plot a spline
def plot_spline(x0, x1, f0, f1, ppp0, ppp1):

    # lots of points for a smooth plot
    x = numpy.linspace(x0, x1, 100)

    dx = x1-x0

    alpha = ppp1/(6.0*dx)
    beta = -ppp0/(6.0*dx)

    gamma = (-ppp1*dx*dx/6.0 + f1)/dx
    eta = (ppp0*dx*dx/6.0 - f0)/dx

    p = alpha*(x-x0)**3 + beta*(x-x1)**3 + gamma*(x-x0) + eta*(x-x1)

    pylab.plot(x, p)

# number of intervals
n = 20

xmin = 0.0
xmax = 1.0

# coordinates of the data locations
x = numpy.linspace(xmin, xmax, n+1)
dx = x[1] - x[0]

# random data
f = numpy.random.rand(n+1)

# we are solving for n-1 unknowns

# setup the righthand side of our matrix equation
b = numpy.zeros(n+1)

# b_i = (6/dx) * (f_{i-1} - 2 f_i + f_{i+1})
# here we do this with slice notation to fill the
# inner n-1 slots of b
b[1:n] = (6.0/dx)*(f[0:n-1] - 2.0*f[1:n] + f[2:n+1])

# we only care about the inner n-1 quantities
b = b[1:n]

# the matrix A is tridiagonal.  Create 3 arrays which will represent
# the diagonal (d), the upper diagonal (u), and the lower diagnonal
# (l).  l and u will have 1 less element.  For u, we will pad this at
# the beginning and for l we will pad at the end.
#
# see http://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.solve_banded.html#scipy.linalg.solve_banded
# for the description of a banded matrix

u = numpy.zeros(n-1)
d = numpy.zeros(n-1)
l = numpy.zeros(n-1)

d[:] = 4.0*dx

u[:] = dx
u[0] = 0.0

l[:] = dx
l[n-2] = 0.0

# create a banded matrix -- this doesn't store every element -- just
# the diagonal and one above and below
A = numpy.matrix([u,d,l])

# solve Ax = b using the scipy banded solver -- the (1,1) here means
# that there is one diagonal above the main diagonal, and one below.
xsol = linalg.solve_banded((1,1), A, b)

# x now hold all the second derivatives for points 1 to n-1.  Natural
# boundary conditions set p'' = 0 at i = 0 and n
# ppp will be our array of second derivatives
ppp = numpy.insert(xsol, 0, 0)  # insert before the first element
ppp = numpy.insert(ppp, n, 0)   # insert at the end

# now plot -- data points first
pylab.scatter(x, f, marker="x", color="r")

# plot the splines
i = 0
while i < n:

    # working on interval [i,i+1]
    ppp_i = ppp[i]
    ppp_ip1 = ppp[i+1]

    f_i = f[i]
    f_ip1 = f[i+1]

    x_i = x[i]
    x_ip1 = x[i+1]

    plot_spline(x_i, x_ip1, f_i, f_ip1, ppp_i, ppp_ip1)

    i += 1

pylab.savefig("spline.png")

# note: we could have done this all through scipy -- here is their
# spline, but it doesn't seem to support natural boundary conditions

#s = interpolate.InterpolatedUnivariateSpline(x, f, k=3)
#xx = numpy.linspace(xmin, xmax, 1000)
#pylab.plot(xx, s(xx), color="k", ls=":")

# old way from scipy -- this raises a NotImplementedError for natural
#spl1 = interpolate.splmake(x, f, order=3, kind="natural")
#xx = numpy.linspace(xmin, xmax, 1000)
#yy = interpolate.spleval(spl1, xx)
#pylab.plot(xx, yy, color="k", ls=":")

#pylab.savefig("spline-scipy.png")


import numpy as np

# ECDF
def ecdf(data):
    """Compute ECDF for a one-dimensional array of measurements."""
    # Number of data points: n
    n = len(data)

    # x-data for the ECDF: x
    x = np.sort(data)

    # y-data for the ECDF: y
    y = np.arange(1, n+1) / n

    return x, y

# Bootstrap Resampling

# bootstrap replicas
def bootstrap_replicate_1d(data, func):
     """Generate bootstrap replicate of 1D data."""
     bs_sample = np.random.choice(data, len(data))
     return func(bs_sample)

# many bootstraps replicas
def draw_bs_reps(data, func, size=1):
    """Draw bootstrap replicates."""

    # Initialize array of replicates: bs_replicates
    bs_replicates = np.empty(size)

    # Generate replicates
    for i in range(size):
        bs_replicates[i] = bootstrap_replicate_1d(data, func)

    return bs_replicates


import numpy as np
import math

def f(func_value, x):
    """
        Defining Function
    """
    if int(func_value) == 1 :
        return (x**3 - 3*x*x - x + 9)
    elif int(func_value) == 2 :
        return ((x**3 - 3*x*x - x + 9)*(math.exp(x)))
    else:
        print("Error Message")

def g(func_value, x):
    """
    Defining derivative of function
    """
    if int(func_value) == 1 :
        return (3*x*x - 6*x - 1)
    elif int(func_value) == 2 :
        return ((x**3 - 3*x*x - x + 9 + (3*x*x - 6*x - 1))*(math.exp(x)))
    else:
        print("Error Message")

def newtonRaphson(func_num,x0,e,N):
    """
        Implementing Newton Raphson Method
    """
    func_value_at_roots = []
    counter = 1
    flag = 1
    condition = True
    while condition:
        if g(func_num,x0) == 0.0:
            print('Divide by zero error!')
            break

        x1 = x0 - f(func_num,x0)/g(func_num,x0)
        #print('Iteration:{}, x1 = {} and f(x1) = {}'.format(counter, x1, f(func_num,x1)))
        func_value_at_roots.append((f(func_num,x1)))
        x0 = x1
        counter = counter + 1

        if counter > N:
            flag = 0
            break

        condition = abs(f(func_num,x1)) > e
    return func_value_at_roots
"""
    if flag==1:
        print('Required root is:{}'.format(x1))
        continue
    else:
        print('Not Convergent.')
"""

AS2101_Labwork/3.Trials-References/Task 3/Submission Codes/Newton-Raphson Method/nrm_plot/nrm.py


#!/usr/bin/python3

import numpy as np
from time import time

EPS = 1e-3

'''
    Sequential Implementation of https://doi.org/10.1007/978-3-319-11194-0_18
'''

def compute_next(mat, vec):
    sigma = np.diag(vec)
    sigma_inv = np.diag(1/vec)
    return np.matmul(np.matmul(sigma_inv, mat), sigma)

def sum_across_rows(mat):
    n = mat.shape[1]
    v = np.array([np.sum(mat[i]) for i in range(n)])
    return v

def stop(vec):
    return all(map(lambda e: e < EPS, [abs(vec[i] - vec[i-1])
                                       for i in range(1, len(vec))]))

def max_eigen_value_and_vector(mat):
    eigen_val = 0
    eigen_vec = np.ones(mat.shape[0])

    itr = 0
    while True:
        vec = sum_across_rows(mat)
        vec_max = np.max(vec)
        eigen_vec = np.array([j * (vec[i]/vec_max)
                              for i, j in enumerate(eigen_vec)])
        if stop(vec):
            eigen_val = vec[0]
            break

        mat = compute_next(mat, vec)
        itr += 1

    return eigen_val, eigen_vec, itr + 1

if __name__ == '__main__':
    # handwritten test begins
    mat = np.array([[1, 1, 2], [2, 1, 3], [2, 3, 5]])
    val, vec, _ = max_eigen_value_and_vector(mat)

    assert abs(val - 7.5311) < EPS
    assert abs(vec[0] - 0.3941) < EPS
    assert abs(vec[1] - 0.5788) < EPS
    assert abs(vec[2] - 0.9975) < EPS
    # handwritten test ends

    print('Sequential Similarity Transform, for finding maximum eigen value ( with vector )\n')
    for dim in range(5, 11):
        mat = np.random.random((1 << dim, 1 << dim))
        start = time() * 1000
        val, _, itr = max_eigen_value_and_vector(mat)
        end = time() * 1000

        assert val - np.max(np.linalg.eigvals(mat)) < EPS
        print(
            f'{1 << dim:<4} x {1 << dim:>4}\t\t{end - start:>6.2f} ms\t\t{itr:>8} round(s)')


import numpy as np
import math

def my_sphere(x):
    x1, x2 = x
    return x1 ** 2 + x2 ** 2

def my_rosenbrock(xy, a=2, b=5):
    x, y = xy
    return (a - x) ** 2 + b * ((y - x ** 2) ** 2)

def my_rastrigin(xy):
    x, y = xy
    return 10 * 2 + (x ** 2 - 10 * np.cos(2 * math.pi * x)) + (y ** 2 - 10 * np.cos(2 * math.pi * y))

def my_booth(xy):
    x, y = xy
    # min(1, 3) = 0
    z = (x + 2*y - 7)**2 + (2*x + y - 5)**2
    return z

def my_matyas(xy):
    x, y = xy
    # min(0, 0) = 0
    z = 0.26*(x**2 + y**2) - 0.48*x*y
    return z

def my_ackley(xy):
    x, y = xy
    # min(0, 0) = 0
    z = -20*np.exp(np.fabs(-0.2*np.sqrt(0.5*(x**2 + y**2)))) - np.exp(np.fabs(0.5*(np.cos(2*x*math.pi) + np.cos(2*y*math.pi)))) + math.e + 20
    return -z

def my_levi13(xy):
    x, y = xy
    # min(1, 1) = 0
    z = (np.sin(3*x*math.pi))**2 + ((x - 1)**2)*(1 + np.sin(3*y*math.pi)**2) + ((y - 1)**2)*(1 + np.sin(2*y*math.pi)**2)
    return z

def my_himmelblau(xy):
    x, y = xy
    # min(3, 2) = 0
    # min(-2.805118, 3.131312) = 0
    # min(-3.779310, -3.283186) = 0
    # min(3.584428, -1.848126) = 0
    z = (x**2 + y - 11)**2 + (x + y**2 - 7)**2
    return z

def my_beale(xy):
    x, y = xy
    # min(3, 0.5) = 0
    z = (1.5 - x + x*y)**2 + (2.25 - x + x*(y**2))**2 + (2.625 - x + x*(y**3))**2
    return z

def my_goldstein_price(xy):
    x, y = xy
    # min(0, -1) = 3
    z = ((1 + (x + y + 1)**2 * (19 - 14*x + 3*(x**2) - 14*y + 6*x*y + 3*(y**2))) * (30 + (2*x - 3*y)**2 * (18 - 32*x + 12*(x**2) + 48*y - 36*x*y + 27*(y**2))))
    return z


import numpy as np

"""[Recreates the adjacency matrix with which the steady state probabilities get multiplied iteratively
    The adjacency matrix A of a set of pages (nodes) defines the linking structure]

Returns:
    [numpy Matrix] -- [The matrix with which the steady state probabilities will get multipled]
"""
def recreate_adjacency_matrix(marchov_chain):
    # since the size of the matrix is n x n getting the size of the row is enough
    num_urls = marchov_chain.shape[0]

    # the probabilty of visiting a url, which is the 1 / total number of urls
    probability = 1 / num_urls

    probability_matrix = np.zeros((num_urls, num_urls))

    # probability_matrix will contain all similar values which is the probability of visiting a particular page
    probability_matrix[:] = probability

    '''
        In assigning a PageRank score to each node of the web graph, we use the teleport operation in two ways:
        (1) When at a node with no out-links, the surfer invokes the teleport operation.
        (2) At any node that has outgoing links, the surfer invokes the teleport operation with a probability of alpha.
        Typical value of alpha is 0.1
    '''
    alpha = 0.1

    adjacency_matrix = alpha * marchov_chain + ((1 - alpha) * probability_matrix)

    return adjacency_matrix

"""[Computes the page rank of a given marchov chain url]

Returns:
    [list] -- [A list of n page rank score where n is the total number of urls]
"""
def compute_page_rank(marchov_chain):
    # create the adjacency matrix with which the steady state probability will get multiplied iteratively
    adjacency_matrix = recreate_adjacency_matrix(marchov_chain)

    num_urls = adjacency_matrix.shape[0]

    # initial vector for the steady state probabilities will always be <1, 0, 0.....n>
    steady_state_probabilities = np.zeros((1, num_urls))
    # this creates a vector of <1, 0, 0...n>
    steady_state_probabilities[0][0] = 1

    # the steady_state probabilities always needs to be transposed
    steady_state_probabilities = np.transpose(steady_state_probabilities)

    previous_state_probabilities = steady_state_probabilities
    while True:
        steady_state_probabilities = adjacency_matrix * steady_state_probabilities

        # we stop when the values have converged and no longer change over the iterations
        if (previous_state_probabilities == steady_state_probabilities).all():
            # if the values converge then the steady_state_probabilities are returned which is the page rank score of the urls
            return steady_state_probabilities

        # otherwise the current steady state probabilities becomes the previous steady state probabilities as we are about to begin another iterations
        previous_state_probabilities = steady_state_probabilities

if __name__ == '__main__':

    '''
        NOTE - This marchov chain is transpose of the modified adjacency matrix of the graph.
        In an adjacency matrix the rows represent an individual url in the graph and columns
        represent the urls that the graph has already visited. This adjacency matrix will be
        transposed and modified to recreate the adjacency matrix. If a url has visited other urls then
        the columns will get replaced by 1 / n, where n is the total number of urls visited by that url.
        The matrix will essentially be a transpose of the adjacency matrix with the columns divided by total non zero entries.
    '''
    marchov_chain = np.matrix([[0, 0, 1],
            [1, 0.5, 0],
            [0, 0.5, 0]])

    page_rank = compute_page_rank(marchov_chain)

    print(page_rank)

    print(np.sum(page_rank))


from math import pi
from math import sqrt

import numpy as np

# Configure Parameters

XMIN = -20.0
XMAX = 20.0
RES = 256
STEP_SIZE = 0.05 * -1j  # imaginary time
TIMESTEPS = 100

# Init Variables

DX = 2 * XMAX / RES
X = np.arange(XMIN + (XMAX / RES), XMAX, DX)
DK = pi / XMAX
K_I = np.concatenate((np.arange(0, RES / 2), np.arange(-RES / 2, 0))) * DK

# Configure Operators

V = 0.5 * X ** 2
WFC = np.exp(-((X + 1) ** 2) / 2, dtype=complex)

# Init Operators

K = np.exp(-0.5 * (K_I ** 2) * STEP_SIZE * 1j, dtype=complex)
R = np.exp(-0.5 * V * STEP_SIZE * 1j, dtype=complex)

# Split-operator fourier method
for i in range(TIMESTEPS):
    # Half-step in real space
    WFC *= R

    # FFT to momentum space
    WFC = np.fft.fft(WFC)

    # Full step in momentum space
    WFC *= K

    # iFFT back to real space
    WFC = np.fft.ifft(WFC)

    # Half-step in real space
    WFC *= R

    # Density for plotting and potential
    density = np.abs(WFC) ** 2

    # Normalize for imaginary time
    if (np.iscomplex(STEP_SIZE)):
        factor = sum(density) * DX
        WFC /= sqrt(factor)

    # Outputting data to file. Plotting can also be done in a
    # similar way. This is set to output exactly 100 files, no
    # matter how many timesteps were specified.
    if (i % (TIMESTEPS // 100) == 0):
        filename = "output/output{}.dat".format(str(i).zfill(5))
        with open(filename, "w") as outfile:
            # Outputting for gnuplot. Any plotter will do.
            for j in range(len(density)):
                line = "{}\t{}\t{}\n".format(
                    X[j], density[j].real, V[j].real)
                outfile.write(line)
        print("Outputting step: ", i + 1)

# Calculate the energy < Psi | H | Psi >
# Creating real, momentum, and conjugate wavefunctions.
WFC_R = WFC
WFC_K = np.fft.fft(WFC_R)
WFC_C = np.conj(WFC_R)

# Finding the momentum and real-space energy terms
energy_k = 0.5 * WFC_C * np.fft.ifft((K_I ** 2) * WFC_K)
energy_r = WFC_C * V * WFC_R

# Integrating over all space
energy_final = sum(energy_k + energy_r).real

print('Final energy: ', energy_final * DX)


"""
This script contains routines for computation in Eulidean geometry.
"""

import numpy as np

def circle_approx_pts(centre, radius, num, from_to=[0,360]):
    """
    SUMMARY
        estimates a circle with evenly spaced points sampled on it
        sets current mode

    PARAMETERS
        centre: list of x coordinate and y coordinate
        radius: radius of circle
        num: number of samples
        from_to: with from_to we can set at what angle it starts and what angle it ends
            [0, 360] for full circle

    RETURNS
        [(float, float)]
    """
    centre = np.array(centre)
    if from_to[1] < from_to[0]:
        myrange = range(from_to[1], from_to[0]+360, (from_to[0]+360-from_to[1])//num)
    else:
        myrange = range(from_to[0], from_to[1], (from_to[1]-from_to[0])//num)

    return_pts = []
    for angle in myrange:
        return_pts.append(centre+radius*np.array([np.cos(np.radians(angle)), np.sin(np.radians(angle))]))

    return return_pts

def circumcentre(A,B,C):
    """
    SUMMARY
        computes the centre of the circumscribed circle

    PARAMETERS
        A: coordinates of vertex A
        B: coordinates of vertex B
        C: coordinates of vertex C

    RETURNS
        (float, float)
    """
    D = 2 * (A[0]*(B[1]-C[1]) + B[0]*(C[1]-A[1]) + C[0]*(A[1]-B[1]))
    K_x_A = (A[0]*A[0] + A[1]*A[1]) * (B[1]-C[1])
    K_x_B = (B[0]*B[0] + B[1]*B[1]) * (C[1]-A[1])
    K_x_C = (C[0]*C[0] + C[1]*C[1]) * (A[1]-B[1])
    K_x = (K_x_A + K_x_B + K_x_C) / D

    K_y_A = (A[0]*A[0] + A[1]*A[1]) * (C[0]-B[0])
    K_y_B = (B[0]*B[0] + B[1]*B[1]) * (A[0]-C[0])
    K_y_C = (C[0]*C[0] + C[1]*C[1]) * (B[0]-A[0])
    K_y = (K_y_A + K_y_B + K_y_C) / D

    return K_x, K_y

def circumradius(A, centre):
    """
    SUMMARY
        computes the radius of the circumscribed circle given a vertex

    PARAMETERS
        A: coordinates of vertex A
        centre: coordinates of the centre of the circle

    RETURNS
        float
    """
    return np.linalg.norm(np.array(A)-np.array(centre))

def circum_centre_and_radius(A,B,C):
    """
    SUMMARY
        combines circumcentre(A,B,C) and circumradius(A, centre) to compute both

    PARAMETERS
        A: coordinates of vertex A
        B: coordinates of vertex B
        C: coordinates of vertex C

    RETURNS
        ([float, float], float)
    """
    centre = circumcentre(A,B,C)
    radius = circumradius(A,centre)
    return centre, radius

def incentre(A,B,C):
    """
    SUMMARY
        computes the centre of the inscribed circle of a triangle

    PARAMETERS
        A: coordinates of vertex A
        B: coordinates of vertex B
        C: coordinates of vertex C

    RETURNS
        [float, float]
    """
    A = np.array(A)
    B = np.array(B)
    C = np.array(C)
    a = np.linalg.norm(C-B)
    b = np.linalg.norm(A-C)
    c = np.linalg.norm(B-A)

    I_x = (a*A[0]+b*B[0]+c*C[0]) / (a+b+c)
    I_y = (a*A[1]+b*B[1]+c*C[1]) / (a+b+c)
    return I_x, I_y

def inradius(A,B,C):
    """
    SUMMARY
        computes the radius of the circumscribed circle (uses Heron's formula)

    PARAMETERS
        A: coordinates of vertex A
        B: coordinates of vertex B
        C: coordinates of vertex C

    RETURNS
        float
    """
    a = np.linalg.norm(np.array(B)-np.array(C))
    b = np.linalg.norm(np.array(C)-np.array(A))
    c = np.linalg.norm(np.array(A)-np.array(B))
    s = (a+b+c)/2
    return np.sqrt(s*(s-a)*(s-b)*(s-c)) / s

def in_centre_and_radius(A,B,C):
    """
    SUMMARY
        combines circumcentre(A,B,C) and circumradius(A, centre) to compute both

    PARAMETERS
        A: coordinates of vertex A
        B: coordinates of vertex B
        C: coordinates of vertex C

    RETURNS
        ([float, float], float)
    """
    centre = incentre(A,B,C)
    radius = inradius(A,B,C)
    return centre, radius

def ll_intersection(A,B,P,Q):
    """
    SUMMARY
        computes the coordinates of the intersection of segment AB and segment PQ,
        (beware: when the segments are parallel the denominators are 0)

    PARAMETERS
        A: coordinates of vertex A
        B: coordinates of vertex B
        P: coordinates of vertex P
        Q: coordinates of vertex Q

    RETURNS
        ([float, float], float)
    """
    denominator = (A[0]-B[0]) * (P[1]-Q[1]) - (A[1]-B[1]) * (P[0]-Q[0])
    if denominator == 0:
        return (DEFAULT,DEFAULT)
    numerator_x = (A[0]*B[1]-B[0]*A[1]) * (P[0]-Q[0]) - (A[0]-B[0]) * (P[0]*Q[1]-Q[0]*P[1])
    numerator_y = (A[0]*B[1]-B[0]*A[1]) * (P[1]-Q[1]) - (A[1]-B[1]) * (P[0]*Q[1]-Q[0]*P[1])

    return numerator_x/denominator, numerator_y/denominator

# credit: https://stackoverflow.com/questions/55816902/finding-the-intersection-of-two-circles
def cc_intersection(O0, r0, O1, r1):
    """
    SUMMARY
        computes the intersection points of two circles
        (note that there can be two intersection points, or one, or none)

    PARAMETERS
        O0: centre of circle 0
        r0: radius of circle 0
        O1: centre of circle 1
        r1: radius of circle 1

    RETURNS
        [float, float]
    """
    # circle 1: (x0, y0), radius r0
    # circle 2: (x1, y1), radius r1
    x0, y0 = O0
    x1, y1 = O1

    d=np.sqrt((x1-x0)**2 + (y1-y0)**2)

    # non intersecting
    if d > r0 + r1 :
        return None
    # one circle within other
    if d < abs(r0-r1):
        return None
    # coincident circles
    if d == 0 and r0 == r1:
        return None
    else:
        a=(r0**2-r1**2+d**2)/(2*d)
        h=np.sqrt(r0**2-a**2)
        x2=x0+a*(x1-x0)/d
        y2=y0+a*(y1-y0)/d
        x3=x2+h*(y1-y0)/d
        y3=y2-h*(x1-x0)/d

        x4=x2-h*(y1-y0)/d
        y4=y2+h*(x1-x0)/d

        return (x3, y3, x4, y4)

def lc_intersection(O, r, A, B):
    """
    SUMMARY
        computes the intersection points of the circle (O,r) and segment AB
        (warning: the number of intersection points may be 0, 1, or 2)
    PARAMETERS
        O: centre of circle
        r: radius of circle
        A: endpoint of segment AB
        B: other endpoint of segment AB

    RETURNS
        [float, float]
    """
    sign = lambda x : 1 if x >= 0 else -1
    O_ = np.array(O)
    A_ = np.array(A)
    B_ = np.array(B)
    dx, dy = B_ - A_
    dr = np.sqrt(dx*dx + dy*dy)
    D = np.cross(A_-O_, B_-O_)
    discriminant = r*r*dr*dr-D*D

    if discriminant > 0:
        x1 = (D*dy+sign(dy)*dx*np.sqrt(discriminant)) / (dr*dr) + O_[0]
        y1 = (-D*dx+np.abs(dy)*np.sqrt(discriminant)) / (dr*dr) + O_[1]
        x2 = (D*dy-sign(dy)*dx*np.sqrt(discriminant)) / (dr*dr) + O_[0]
        y2 = (-D*dx-np.abs(dy)*np.sqrt(discriminant)) / (dr*dr) + O_[1]
        if np.cross(A_-O_,A_-B_) <= 0:
            if sign(dy) == 1:
                return [[x1,y1], [x2,y2]], True
            else:
                return [[x2,y2], [x1,y1]], True
        else:
            if sign(dy) == 1:
                return [[x1,y1], [x2,y2]], False
            else:
                return [[x2,y2], [x1,y1]], False
    elif discriminant == 0:
        x = D*dy / (dr*dr)
        y = -D*dx / (dr*dr)
        return [[x,y], [x,y]], False
    else:
        return [[0,0],[0,0]], False

# https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
def pt_segment_dist(A, B, P):
    """
    SUMMARY
        computes the distance of point P from segment AB
    PARAMETERS
        A: endpoint of segment AB
        B: other endpoint of segment AB
        P: point at some distance from AB

    RETURNS
        float
    """
    x1, y1 = A
    x2, y2 = B
    x3, y3 = P
    px = x2-x1
    py = y2-y1
    norm = px*px + py*py
    u =  ((x3 - x1) * px + (y3 - y1) * py) / float(norm)

    if u > 1:
        u = 1
    elif u < 0:
        u = 0

    x = x1 + u * px
    y = y1 + u * py
    dx = x - x3
    dy = y - y3

    dist = (dx*dx + dy*dy)**.5
    return dist

def pt_circle_dist(O, r, P):
    """
    SUMMARY
        computes the distance of point circle (O, r) from P

    PARAMETERS
        O: coordinates of the circle centre O
        r: radius of the circle
        P: point at some distance from the circle

    RETURNS
        float
    """
    return abs(np.linalg.norm(np.array(O)-np.array(P))-r)

def orthogonal_projection(A, B, P):
    """
    SUMMARY
        computes the orthogonal projection of P on AB

    PARAMETERS
        A: endpoint of segment AB
        B: other endpoint of segment AB
        P: point at some distance from AB

    RETURNS
        [float, float]
    """
    A_ = np.array(A)
    B_ = np.array(B)
    P_ = np.array(P)
    x = np.linalg.norm(P_ - A_) * (P_ - A_).dot(B_ - A_) / (np.linalg.norm(P_ - A_) * np.linalg.norm(B_ - A_))
    return A_ + (B_- A_) / np.linalg.norm(A_ - B_) * x

def bisector_point(A,B,C):
    """
    SUMMARY
        computes a point which lies on the bisector of the angle

        In order to get the exact distance from the angle point we follow the
        construction method of tkz-euclide.
        1. copy the first segment (A,B) on the second segment (B,C) to get (P)
        the result is the third coordinate of the equilateral triangle formed by AP.

    PARAMETERS
        A: point
        B: point where the angle is
        P: third point

    RETURNS
        [float, float]
    """
    A = np.array(A)
    B = np.array(B)
    C = np.array(C)
    P = B + np.linalg.norm(B-A) * (C - B) / np.linalg.norm(C-B)
    rotation_matrix = np.array([[np.cos(np.radians(60)), -np.sin(np.radians(60))],\
                               [np.sin(np.radians(60)), np.cos(np.radians(60))]])
    Q = A.reshape(2,1) + rotation_matrix @ (P-A).reshape(2,1)
    return Q.flatten()


from sympy import S, zeros, Matrix, nsimplify, factorial, maximum, lambdify, Interval, expand, symbols, pi, cos, eye
from ..genericas import matriz_inversa
from tqdm import tqdm
import numpy as np

def polinomio_lagrange(x_vals, y_vals, var=symbols('x')):
    """Genera el polinomio de interpolación de Lagrange.

    Args:
        x_vals (list): Valores x de interpolación
        y_vals (list): Valores f(x) de interpolación
        var (variable, optional): Variable sobre la que construir el polinomio. Defaults to symbols('x').

    Returns:
        p: polinomio de Lagrange.
        lista_L: Lista de coeficientes l_i de los polinomios.
    """
    lista_L, p, n = [], S(0), len(x_vals)
    for i in range(n):
        li = S(1)
        for k in range(n):
            if k != i:
                li *= (var - x_vals[k]) / (x_vals[i] - x_vals[k])

        lista_L.append(li)
        p += y_vals[i] * li

    return p, lista_L

def polinomio_newton(x_vals, y_vals, var=symbols('x'), evalf=None):
    """Genera el polinomio de interpolación de Newton.

    Args:
        x_vals (list): Valores x de interpolación
        y_vals (list): Valores f(x) de interpolación
        var (variable, optional): Variable sobre la que construir el polinomio. Defaults to symbols('x').
        evalf (int): Redondea la matriz de coeficiente y, por tanto, los coeficientes del polinomio a un número de cifras.

    Returns:
        p: polinomio de Newton.
        matriz_coeffs: Matriz de coeficientes con el método iterativo.
    """

    matriz_coeffs = zeros(len(y_vals), len(y_vals))
    for i in range(len(y_vals)):  # Asignamos la primera columna como f(x)
        matriz_coeffs[i, 0] = y_vals[i]

    for col in range(1, len(y_vals)):
        for row in range(len(y_vals) - col):
            num = matriz_coeffs[row + 1, col - 1] - matriz_coeffs[row, col - 1]
            den = x_vals[row + col] - x_vals[row]

            matriz_coeffs[row, col] = num / den

    matriz_coeffs = nsimplify(matriz_coeffs, tolerance=1e-10, rational=True)  # Esto es importante para quitar valores de redondeo
    if evalf is not None:  # si evalf es un numero redondea a ese numero de decimales
        matriz_coeffs = matriz_coeffs.evalf(evalf)

    # Aqui hacemos el polinomio
    p = matriz_coeffs[0, 0]
    for col in range(1, len(y_vals)):
        p_col = S(1)
        for i in range(0, col):
            p_col *= (var - x_vals[i])

        p += p_col * matriz_coeffs[0, col]

    return p, matriz_coeffs

def error_lagrange(f, x_vals, I=[0, 1], var=symbols('x')):
    """Calcula el error del polinomio de Lagrange, definido por PROD[(x-xi)]/(n+1)! max f^(n+1)(t)

    Args:
        f (función): Función interpolada (si se conoce).
        x_vals (list): Valores x de interpolación.
        I (list): Intervalo de interpolación.
        var (variable, optional): Variable de interpolación. Defaults to symbols('x').

    Returns:
        E_x (float): Error máximo de interpolación
    """
    E_x_fact = S(1)
    for x_val in x_vals:
        E_x_fact *= (var - x_val)

    E_x_fact /= factorial(len(x_vals))

    # Ahora derivamos la función n veces
    diff_f = f

    for _ in range(len(x_vals)):
        diff_f = diff_f.diff(var)

    max_diff_f = maximum(diff_f, var, Interval(I[0], I[1]))

    E_x = E_x_fact * abs(max_diff_f)

    return E_x

def error_maximo_estocastico(f, var=symbols('x'), grado=3, N=1000, I=[0, 1], list_x_vals=None):
    """Emplea un método estocástico para simular el error máximo de la interpolación. Para ello genera n (grado) nodos dentro del intervalo, calcula el error máximo para ese caso específico, y
    actualiza el error máximo para cada conjunto de nodos.

    Args:
        f (funcion): Función a interpolar.
        var (variable, optional): Variable de la función. Defaults to symbols('x').
        grado (int, optional): Grado de interpolación. Defaults to 3.
        N (int, optional): Número de generaciones aleatorias de nodos. Defaults to 1000.
        I (list, optional): Intervalo de interpolación. Defaults to [0, 1].
        list_x_vals (list, optional): Listado de nodos, definido por el usuario. Defaults to None.

    Returns:
        max_error (float): Error estocástico máximo.
    """
    max_error = 0

    # Hacemos la funcion por separado porque el cálculo de derivada es costoso y cada iteración de error_lagrange tarda mucho
    diff_f = f
    for _ in range(grado):
        diff_f = diff_f.diff(var)
    max_diff_f = maximum(diff_f, var, Interval(I[0], I[1]))

    for _ in tqdm(range(N)):
        if list_x_vals is None:
            x_list = np.sort(np.random.rand(grado) * (I[1] - I[0]) + I[0])
        else:
            x_list = list_x_vals

        E_x_fact = S(1) / S(factorial(grado))
        for x_val in x_list:
            E_x_fact *= (var - x_val)

        E_x = lambdify(var, expand(E_x_fact * max_diff_f))  # lambdify es para que la evaluación numérica sea mucho más ágil

        x_range = np.linspace(I[0], I[1], int(N / 10))
        y_f = [E_x(i) for i in x_range]
        max_y_f = np.max(abs(np.array(y_f)))

        if max_y_f >= max_error:
            max_error = max_y_f

        if list_x_vals is not None:
            break

    return max_error

def roots_chebyshev(n, I=[-1, 1]):
    """Calcula las raíces del polinomio de Chebyshev de grado n.

    Args:
        n (int): Grado del polinomio
        I (list, optional): Intervalo de generación del polinomio. Defaults to [-1, 1].

    Returns:
        roots (list): Lista con las raices, en forma trigonométrica.
    """
    roots = [0.5 * (I[1] + I[0]) + 0.5 * (I[1] - I[0]) * cos(S(2 * (i + 1) - 1) / S(2 * n) * pi) for i in range(n)][::-1]
    return roots

def aitken_neville(x_vals, y_vals, x0, modo='aitken'):
    """Aplica los algoritmos de Aitken y Nevile para calcular el valor de una función interpolada en un punto nuevo, dados los valores en nodos anteriores.

    Args:
        x_vals (list): Valores x de interpolación
        y_vals (list): Valores f(x) de interpolación
        x0 (float): Valor x del nodo a calcular su f(x)
        modo (str, optional): ['aitken', 'neville']. Defaults to 'aitken'.

    Returns:
        matriz_coeffs: Matriz de coeficientes de la interpolación. El elemento [0, -1] es el valor interpolado.
    """
    matriz_coeffs = zeros(len(y_vals), len(y_vals))
    for i in range(len(y_vals)):  # Asignamos la primera columna como f(x)
        matriz_coeffs[i, 0] = y_vals[i]

    for col in range(1, len(y_vals)):
        for row in range(len(y_vals) - col):
            xk = None
            if modo == 'aitken':
                xk = x_vals[0]
            elif modo == 'neville':
                xk = x_vals[row]

            num = (x0 - xk) * matriz_coeffs[row + 1, col - 1] - (x0 - x_vals[row + col]) * matriz_coeffs[row, col - 1]
            den = x_vals[row + col] - xk

            matriz_coeffs[row, col] = num / den

    matriz_coeffs = nsimplify(matriz_coeffs, tolerance=1e-10, rational=True)  # Esto es importante para quitar valores de redondeo

    return matriz_coeffs

def interpolacion_hermite(x_vals, y_vals, diff_vals, var=symbols('x'), evalf=None):
    """Aplica la interpolación de Hermite, que permite la introducción de valores de derivadas. En esta implementación solo
    se permiten valores de la primera derivada.

    Args:
        x_vals (list): Valores x de interpolación.
        y_vals (list): Valores f(x) de interpolación.
        diff_vals (list): Valores f'(x) de interpolación.
        var (variable, optional): Variable sobre la que construir el polinomio. Defaults to symbols('x').
        evalf (int): Redondea la matriz de coeficiente y, por tanto, los coeficientes del polinomio a un número de cifras.

    Returns:
        p: polinomio de Hermite.
        matriz_coeffs: Matriz de coeficientes con el método iterativo.
    """
    base_w = [S(1)]
    double_x_vals = []
    for val in x_vals:
        double_x_vals += [val, val]

    for x_val in x_vals:
        base_w += [base_w[-1] * (var - x_val), base_w[-1] * (var - x_val) ** 2]

    base_w = base_w[:-1]  # Eliminamos la componente w_2n+1, que no se emplea para el cálculo del polinomio
    matriz_coeffs = zeros(len(base_w), len(base_w))

    # Asignamos la primera columna como f(x)
    for i in range(2 * len(y_vals)):
        matriz_coeffs[i, 0] = y_vals[i // 2]

    # Asignamos la segunda columna para f'(x) o f[xa, xa+1]
    for i in range(0, 2 * len(y_vals), 2):
        matriz_coeffs[i, 1] = diff_vals[i // 2]

    for i in range(1, 2 * len(y_vals) - 1, 2):
        num = matriz_coeffs[i + 1, 0] - matriz_coeffs[i, 0]
        den = double_x_vals[i + 1] - double_x_vals[i]
        matriz_coeffs[i, 1] = num / den

    # Asignamos para el resto de columnas
    for col in range(2, 2 * len(y_vals)):
        for row in range(2 * len(y_vals) - col):
            num = matriz_coeffs[row + 1, col - 1] - matriz_coeffs[row, col - 1]
            den = double_x_vals[row + col] - double_x_vals[row]

            matriz_coeffs[row, col] = num / den

    matriz_coeffs = nsimplify(matriz_coeffs, tolerance=1e-10, rational=True)  # Esto es importante para quitar valores de redondeo
    if evalf is not None:  # si evalf es un numero redondea a ese numero de decimales
        matriz_coeffs = matriz_coeffs.evalf(evalf)

    # Aqui hacemos el polinomio
    p = S(0)
    for i in range(len(base_w)):
        p += base_w[i] * matriz_coeffs[0, i]

    return p, matriz_coeffs

def polinomio_generico(lista_condiciones, var=symbols('x'), evalf=None):
    """Calcula un polinomio que satisface las condiciones de interpolación.
       La lista de condiciones viene data por una lista de listas:
       [ [(a, b), (c, d)],  [],  [(e, f)]]
       Esto significaria: f(a) = b;  f(c) = d;  f''(e) = f
    Args:
        lista_condiciones (list): Lista de condiciones
        var (variable, optional): Variable sobre la que construir el polinomio. Defaults to symbols('x').
        evalf (int): Redondea la matriz de coeficiente y, por tanto, los coeficientes del polinomio a un número de cifras.

    Returns:
        p (polinomio): polinomio de interpolación.
        D (matriz): Matriz de coeficientes. Cada fila tiene tantas columnas como coeficientes a_i, y satisface la condición determinada: f''(3) -> 0  0 2 3·2·(3) 4·3·(3)^2 5·4·(3)^3 ...
        rhs: Matriz de rhs de la ecuación: f''(3) = 34 -> 34
        a_vals: Matrices de valores del polinomio que cumplen la ecuación D·a = rhs
    """
    # La lista de condiciones viene data por una lista de listas:
    # [ [(a, b), (c, d)],  [],  [(e, f)]]
    # Esto significaria: f(a) = b;  f(c) = d;  f''(e) = f

    # Primero contamos el número de elementos para generar el polinomio de grado n-1
    n = sum([len(i) for i in lista_condiciones])
    p_list = []
    for i in range(n):
        p_list.append(var ** i)

    # Ahora implementamos las condiciones. Cada elemento de la lista de condiciones será p, o sus derivadas, sustituyendo el elemento determinado
    D, rhs = zeros(n, n), []

    row = 0
    for diff_range in range(len(lista_condiciones)):
        # Primero aplicamos la n-derivada a ese polinomio
        for pair in lista_condiciones[diff_range]:
            for col in range(diff_range, n):
                if col == 0:
                    multiplicado_derivada = 1
                else:
                    multiplicado_derivada = factorial(col) / factorial(col - diff_range)
                D[row, col] = multiplicado_derivada * pair[0] ** (col - diff_range)  # Añadimos a la matriz su elemento, que es la derivada por x^n

            rhs.append(pair[1])
            row += 1

    try:
        a = matriz_inversa(D) * Matrix(rhs)
        a_vals = {symbols(f'a{i}'): a[i, 0] for i in range(D.shape[0])}

        p = S(0)
        for i in range(n):
            p += var**i * a_vals[symbols(f'a{i}')]
    except:
        print('La matriz de valores no tiene inversa. No existe un polinomio de interpolación que verifique las condiciones.')
        a_vals = None
        p = None

    return p, D, rhs, a_vals

def esplines(x_vals, y_vals, var=symbols('x')):
    """Genera un esplín natural cúbico.

    Args:
        x_vals (list): Valores x de interpolación.
        y_vals (list): Valores f(x) de interpolación.
        var (variable, optional): Variable sobre la que construir el esplín. Defaults to symbols('x').

    Returns:
        S_dict (dict): Diccionario con los polinomios S_i para cada par de nodos x_i x_i+1
        valores (dict): Para cada i, retorna los coeficientes a_i, b_i, c_i, d_i del polinomio S_i = a_i(x-x_i)^3 + b_i(x-x_i)^2 + c_i(x-x_i) + d_i
        D (matriz): Matriz de construcción del esplín.
        z (list): Lista de valores z_i para derivar a_i, b_i, c_i, d_i
        rhs (matriz): Matriz con los valores 6 * f[x_i, x_i+1, x_i+2]
    """
    # Primero creamos los polinomios de los esplines
    S_dict = {}
    valores = {}

    # Ahora hallamos h y r para resolver el sistema
    h_list = [x_vals[i + 1] - x_vals[i] for i in range(len(x_vals) - 1)]
    r_list = [h_list[i] / (h_list[i] + h_list[i + 1]) for i in range(len(x_vals) - 2)]

    D = 2 * eye(len(x_vals) - 2)
    for i in range(len(x_vals) - 3):
        D[i, i + 1] = 1 - r_list[i]
        D[i + 1, i] = r_list[i + 1]

    # Ahora hallamos el rhs, que está compuesto por f[a, b, c] = (f[b, c] - f[a, b])/(c-a) = ({(f(c) - f(b))/(c-b)} - {(f(b) - f(a))/(b-a)})/(c-a)
    rhs = 6 * Matrix([(((y_vals[i + 2] - y_vals[i + 1]) / (x_vals[i + 2] - x_vals[i + 1])) -
                       ((y_vals[i + 1] - y_vals[i]) / (x_vals[i + 1] - x_vals[i]))) /
                      (x_vals[i + 2] - x_vals[i])
                      for i in range(len(x_vals) - 2)])

    # Resolvemos el sistema D*z = rhs
    z = matriz_inversa(D) * rhs
    z_list = [0] + list(z) + [0]

    # Asignamos los valores de a, b, c, d, y con ello creamos los polinomios
    for i in range(len(x_vals) - 1):
        a = (z_list[i + 1] - z_list[i]) / (6 * h_list[0])
        b = z_list[i] / 2
        c = (y_vals[i + 1] - y_vals[i]) / (h_list[i]) - (2 * z_list[i] + z_list[i + 1]) / (6) * h_list[i]

        valores[i] = [a, b, c, y_vals[i]]
        S_dict[f'S_{i}'] = expand(y_vals[i] + c * (var - x_vals[i]) + b * (var - x_vals[i]) ** 2 + a * (var - x_vals[i]) ** 3)

    return S_dict, valores, D, z, rhs


from collections import Counter
from sklearn.neighbors import KernelDensity

import numpy as np

print("====================")
# Calculating Discrete Probability
print("Calculating Discrete Probability")

x = [0,1,0,0,0,1]
c = Counter(x)

def probability(a):
    # returns the probability of a given number a
    return float(c[a]) / len(x)

prob = probability(1)
print(prob)
print("====================")

# Calculating Continuous Probability
start = 5  # Start of the range
end = 6    # End of the range
N = 100    # Number of evaluation points

# Step size
step = (end - start) / (N - 1)
print("\nUniform linear distribution start=%s, end=%s, N=%s, Step size=%s" % (start, end, N,
                                                                             step))

# numpy.linspace - Return evenly spaced numbers over a specified interval i.e. uniformly distributed in linear space
# numpy.logspace - Return numbers spaced evenly on a log scale i.e. uniformly distributed in log space

# Generate values in the range i.e. we can generate a set of points equidistant from each
# other and estimate the kernel density at each point.
# np.newaxis might come in handy when you want to explicitly convert a 1D array to either a row vector or a column vector,
# make it as column vector by inserting an axis along second dimension

x_test_1d = np.linspace(start, end, N)

print("\nGenerated synthetic data from a uniform linear distribution:")
print(x_test_1d)

x_test = x_test_1d[:, np.newaxis] # make it as column vector by inserting an axis along second dimension

print("\nShape of synthetic data from a uniform linear distribution:")
print(x_test.shape)

# Get PDF values for each x
# Please note that kd.score_samples generates log-likelihood of the data samples.
# Therefore, np.exp is needed to obtain likelihood.
# When fitting a model your X needs to be 2D array. i.e (n_samples, n_features).
kde_model = KernelDensity(kernel='gaussian', bandwidth=0.75).fit(x_test)

kd_vals = np.exp(kde_model.score_samples(x_test))
print("\nLikelihood from uniform linear distribution synthetic data i.e. density from KDE:")
print(kd_vals)

# Approximate the integral of the PDF
probability = np.sum(kd_vals * step)
print("\nSumming the integrals of PDF from uniform linear distribution synthetic data  i.e. Probability:")
print(probability)

#Alternative using builtin SciPy integration methods
from scipy.integrate import quad

# Return the integration of a polynomial.
# The function quad is provided to integrate a function of one variable between two points

# Reshape your data using array.reshape(-1, 1) if your data has a single feature

# When you use .reshape(1, -1) it adds one dimension to the data.
# Reshape your data using array.reshape(1, -1) if it contains a single sample
# i.e. np.float64(x).reshape(1,-1) gives array([[ 0.]], dtype=float64) which is akin to [[x]]
fxn = lambda x: np.exp(kde_model.score_samples(np.float64(x).reshape(1,-1)))
probability = quad(fxn, start, end)[0]
# (quad returns a tuple where the first index is the result,# therefore the [0])
print("\nIntegral of PDF i.e. Probability ( using builtin and more accurate SciPy integration methods):")
print(probability)

print("====================")
# Generating Synthetic Data from 2 distributions - an asymmetric log-normal distribution and the other one is a Gaussian distribution
print("\nGenerating Synthetic Data from an asymmetric log-normal distribution and the other one is a Gaussian distribution")

def generate_data(mu1, sigma1, size1, mu2, sigma2, size2, seed):
    # Fix the seed to reproduce the results
    rand = np.random.RandomState(seed)

    # Draw samples from a log-normal distribution with specified mean, standard deviation, and array shape.
    # Note that the mean and standard deviation are not the values for the distribution itself,
    # but of the underlying normal distribution it is derived from.
    x = []
    # mean, standard deviation, size
    dat = rand.lognormal(mean=mu1, sigma=sigma1, size=size1)
    x = np.concatenate((x, dat))

    #loc - (Mean) where the peak of the bell exists.
    # scale - (Standard Deviation) how flat the graph distribution should be.
    # size - The shape of the returned array.
    # mean, standard deviation, size
    dat = rand.normal(loc=mu2, scale=sigma2, size=size2)
    x = np.concatenate((x, dat))
    return x

mu1, sigma1, size1 = 0, 0.3, 1000  # mean, standard deviation, size
mu2, sigma2, size2 = 3, 1, 1000  # mean, standard deviation, size
seed=17
x_train_1d = generate_data(mu1, sigma1, size1, mu2, sigma2, size2, seed) #one dimension i.e. 1d

print("\n Synthetic data from an asymmetric log-normal distribution (mean=%s, sigma=%s,size=%s) and a "
      "\n Gaussian distribution (mean=%s, standard deviation=%s, size=%s), with shape %s "
      "and length %s:" % (mu1, sigma1, size1,mu2, sigma2, size2, x_train_1d.shape, x_train_1d.size))
print(x_train_1d)
x_train = x_train_1d[:, np.newaxis] # make it as column vector by inserting an axis along second dimension

# ndarray.size - # Number of elements in the array. Caclulated as np.prod(a.shape), i.e., the product of the array’s dimensions.
print("====================")

# Generating Synthetic Data from two Gaussian distributions
print("\nGenerating Synthetic Data from two Gaussian distributions")

def generate_synthetic_data2(mu1, sigma1, size1, mu2, sigma2, size2, seed):
    # Fix the seed to reproduce the results
    rand = np.random.RandomState(seed)
    x = []
    dat = rand.normal(mu1, sigma1, size1)
    x = np.concatenate((x, dat))
    dat = rand.normal(mu2, sigma2, size2)
    x = np.concatenate((x, dat))
    return x

mu1, sigma1, size1 = 6, 1, 1000  # mean, standard deviation, size
mu2, sigma2, size2 = 3, 1, 1000  # mean, standard deviation, size
seed=17
x_train2_1d = generate_synthetic_data2(mu1, sigma1, size1, mu2, sigma2, size2, seed)
print("\n Synthetic data from two Gaussian distributions (mean=%s, sigma=%s,size=%s) and (mean=%s, sigma=%s,size=%s)\n"
      "with shape %s and length %s:" % (mu1, sigma1, size1,mu2, sigma2, size2, x_train2_1d.shape, x_train2_1d.size))
print(x_train2_1d)
x_train2 = x_train2_1d[:, np.newaxis]
print("====================")


import matplotlib.pyplot as plt
import numpy as np

# Euler Method
def euler_method(n0, decay_const, t_final, n_t_steps):
    iterations = n_t_steps
    delta_t = t_final/n_t_steps
    t1 = np.linspace(0, iterations*delta_t, iterations)
    n1 = np.zeros(t1.shape, float)
    n1[0]=n0
    for i in range(0,len(t1)-1):
        n1[i+1] = n1[i] * (1 - decay_const * delta_t )
    n1r = n1/n0
    return n1, n1r, t1

ne, ner, te = euler_method(n0=10000, decay_const=1.54e-1, t_final=20, n_t_steps=10)

#Analitical solution ...in the same points of the Euler method
def analytical_solution(n0, decay_const, t_final, n_t_steps):

    intermediate_points = n_t_steps
    delta_t = t_final/n_t_steps
    t2 = np.linspace(0, intermediate_points*delta_t, intermediate_points)
    n2 = n0 * np.exp(-decay_const * t2 )
    n2r = n2/n0
    return n2, n2r, t2

na, nar, ta = analytical_solution(n0=10000, decay_const=1.54e-1, t_final=20, n_t_steps=10)

euler_rel_error = 100*(ne-na)/na

fig = plt.figure()
ax1 = fig.add_subplot(1, 2, 1)
ax1.plot(te, ner, linestyle="-", linewidth=2, label='Euler method')
ax1.plot(ta, nar, linestyle="--", linewidth=2, label='Analytical Solution')
ax1.set_ylabel('Relative Number of $^{238}$U atoms')
ax1.set_xlabel('time in bilion years')
ax1.legend()

ax2 = fig.add_subplot(1, 2, 2)
ax2.plot(te, euler_rel_error, linestyle="-", linewidth=2, label='Deviation formthe \nexpected value')
ax2.set_ylabel('Relative Error, in %')
ax2.set_xlabel('time in bilion years')
ax2.legend()


import sympy as sy
import numpy as np
import random
from sympy import *

def threePointCubicApprox(x,y,xSlopePoint,yPrime):
	C3 = (y[2] - y[0])/((x[2] - x[1])*(x[2] - x[0])**2) - (y[1] - y[0])/((x[2] - x[1])*(x[1] - x[0])**2) + yPrime/((x[1]-x[0])*(x[2] - x[0]))
	C2 = (((y[1] - y[0])/(x[1] - x[0])) - yPrime)/(x[1] - x[0]) - C3*(2*x[0] + x[1])
	C1 = yPrime - 2*C2*x[0] - 3*C3*x[0]**2
	C0 = y[0] - C1*x[0] - C2*x[0]**2 - C3*x[0]**3

	return [C0,C1,C2,C3]

def threePointQuadraticApprox(x, y):
	#Inputs: Vector of x values and y values. These vectors must be equal length
	#Outputs: Coefficients for polynomial equation according to the form C0 + C1*x + C2*x^2...
	C2 = (((y[2]-y[0])/(x[2]-x[0])) - ((y[1]-y[0])/(x[1]-x[0])))/(x[2]-x[1])
	C1 = (y[1] - y[0])/(x[1]-x[0]) - C2*(x[0]+x[1])
	C0 = y[0] - C1*x[0] - C2*x[0]**2

	return [C0,C1,C2]

def twoPointLinearApprox(x, y):
	#Inputs: Vector of x values and y values. These vectors must be equal length
	#Outputs: Coefficients for polynomial equation according to the form C0 + C1*x + C2*x^2...
	C1 = (y[1] - y[0])/(x[1]-x[0])
	C0 = y[0] - C1*x[0]

	return [C0,C1]

def getValueOfPoly(c,x):
	#Inputs: Coefficients for polynomial equation according to the form C0 + C1*x + C2*x^2...
	#Inputs: x - value to get value at
	constantQuantity = len(c)

	if constantQuantity == 1:
		# Flat line
		y = c[0]
	elif constantQuantity == 2:
		# Linear
		y = c[0] + c[1] * x
	elif constantQuantity == 3:
		# Quadratic
		y = c[0] + c[1]*x + c[2]*x**2
	elif constantQuantity == 4:
		# Cubic
		y = c[0] + c[1]*x + c[2]*x**2 + c[3]*x**3
	else:
		print("Polynomial could not be calculated. Check getValueOfPoly function.")
		y = 99999999

	return y


'''
Data Characteristics
'''

import os
os.chdir("C:/Users/tprabhan/Documents/My_Books/RSADBE_2.0/R_and_Python_Programs/Python/Chapter_01")
import matplotlib.pyplot as plt
import numpy as np
import pylab

# Discrete Uniform Distribution
M = 10
mylabels = list(range(1,M+1))
prob_labels = [1/M]*M
plt.xlim([0.9,10.1])
plt.ylim([0,0.11])
plt.plot(mylabels,prob_labels,marker='o',linestyle='none')
plt.xlabel("Labels")
plt.ylabel("Probability")
plt.title("Probability of Discrete Uniform RV")
plt.show()

# Binomial Distribution
n = 10; p = 0.5
from scipy.stats import binom
p_x = binom.pmf(range(1,n+1),n,p)
plt.plot(range(1,n+1),p_x,marker='o',linestyle='none')
plt.show()

# Binomial Probabilities
n = 83; p = 0.01
from scipy.stats import binom
binom.pmf(10,n,p)
binom.pmf(20,n,p)
binom.pmf(30,n,p)
xr = range(0,84)
sum(binom.pmf(xr,n,p))

# Hypergeometric Distribution
N = 200; M = 20
n = 10
k = range(0,12)
from scipy.stats import hypergeom
np.round(hypergeom.pmf(k,N,M,n),3)

# Poisson Probabilities
from scipy.stats import poisson
poisson.pmf(0,3)
poisson.pmf(5,3)
poisson.pmf(20,3)

# Continuous Uniform Distribution
from scipy.stats import uniform
uniform.cdf(0.58)-uniform.cdf(0.35)

# Exponential Densities
from scipy.stats import expon
xr = np.arange(0,10.2,0.20)
f_x = expon.pdf(xr,scale=1)
pylab.plot(xr,f_x,'r',label='Rate=1')
f_x1 = expon.pdf(xr,scale=1/0.2)
pylab.plot(xr,f_x1,'g',label='Rate=0.2')
f_x2 = expon.pdf(xr,scale=1/0.5)
pylab.plot(xr,f_x2,'b',label='Rate=0.5')
f_x3 = expon.pdf(xr,scale=1/0.7)
pylab.plot(xr,f_x3,'y',label='Rate=0.7')
f_x4 = expon.pdf(xr,scale=1/0.85)
pylab.plot(xr,f_x4,'purple',label='Rate=0.85')
pylab.legend(loc='upper right')
plt.show()

# Exponential Densities Continued
from scipy.stats import expon
xr = np.arange(0,0.5,0.02)
f_x = expon.pdf(xr,scale=1/50)
pylab.plot(xr,f_x,'r',label='Rate=50')
f_x1 = expon.pdf(xr,scale=1/10)
pylab.plot(xr,f_x1,'g',label='Rate=10')
f_x2 = expon.pdf(xr,scale=1/20)
pylab.plot(xr,f_x2,'b',label='Rate=20')
f_x3 = expon.pdf(xr,scale=1/30)
pylab.plot(xr,f_x3,'y',label='Rate=30')
f_x4 = expon.pdf(xr,scale=1/40)
pylab.plot(xr,f_x4,'purple',label='Rate=40')
pylab.legend(loc='upper right')
pylab.show()

# Shady Normal Probabilities
from scipy.stats import norm
xr = np.arange(-4,4.1,0.1)
fx = norm.pdf(xr)
pylab.plot(xr,fx)
p1 = np.arange(0,4.02,0.02)
pylab.fill_between(p1,norm.pdf(p1))
pylab.show()
xr = np.arange(-4,4.1,0.1)
fx = norm.pdf(xr)
pylab.plot(xr,fx)
p2 = np.arange(-1.96,1.961,0.001)
pylab.fill_between(p2,norm.pdf(p2))
pylab.show()
xr = np.arange(-4,4.1,0.1)
fx = norm.pdf(xr)
pylab.plot(xr,fx)
p3 = np.arange(-2.58,2.581,0.001)
pylab.fill_between(p3,norm.pdf(p3))
pylab.show()


import numpy as np

'''
    In mathematics, Chebyshev distance (or Tchebychev distance), maximum metric,
    or L∞ metric is a metric defined on a vector space where
    the distance between two vectors is the greatest of their differences
    along any coordinate dimension.[2] It is named after Pafnuty Chebyshev.

    It is also known as chessboard distance, since in the game of chess the minimum number of
    moves needed by a king to go from one square on a chessboard to another equals the
    Chebyshev distance between the centers of the squares, if the squares have side length one,
    as represented in 2-D spatial coordinates with axes aligned to the edges of the board.
'''

objA = [22, 1, 42, 10]

objB = [20, 0, 36, 8]

npA = np.array(objA)

npB = np.array(objB)

chebyshev = np.abs(npA - npB).max()

# chebyshev = np.linalg.norm(npA -npB, ord=np.inf)

print(chebyshev)


"""Samples from normal distribution using Box-Muller transformation.

The transformation is applied to the bivariate normal distribution.
"""
import typing as t

import numpy as np

def sample_normal(num_inst: int = 1,
                  loc: float = 0.0,
                  scale: float = 1.0,
                  random_state: t.Optional[int] = None) -> np.ndarray:
    """Sample ``num_inst`` instances from normal distribution.

    Arguments
    ---------
    num_inst : :obj:`int`
        Number of samples to output.

    loc : :obj:`float`
       Mean of the normal distribution.

    scale : :obj:`float`
        Standard deviation (not the variance!) of the normal distribution.

    random_state : :obj:`int`, optional
        If not None, set numpy random seed before the first sampling.

    Returns
    -------
    :obj:`np.ndarray`
        Samples of the normal distribution with ``loc`` mean and ``scale``
        standard deviation.

    Notes
    -----
    Uses the Box-Muller bivariate transformation, which maps two samples
    from the Uniform Distribution U(0, 1) into two samples of the Normal
    Distribution N(0, 1).
    """
    if random_state is not None:
        np.random.seed(random_state)

    remove_extra_inst = False

    if num_inst % 2:
        num_inst += 1
        remove_extra_inst = True

    uniform_samples = np.random.uniform(0, 1, size=(2, num_inst // 2))

    aux_1 = np.sqrt(-2 * np.log(uniform_samples[0, :]))
    aux_2 = 2 * np.pi * uniform_samples[1, :]

    samples = np.concatenate((aux_1 * np.cos(aux_2), aux_1 * np.sin(aux_2)))

    samples = loc + scale * samples

    if remove_extra_inst:
        return samples[1:]

    return samples

def _test():
    import matplotlib.pyplot as plt
    import scipy.stats

    plt.subplot(1, 2, 1)
    vals = np.linspace(-4, 4, 100)
    plt.plot(vals, scipy.stats.norm(loc=0, scale=1).pdf(vals))
    samples = sample_normal(num_inst=1000, random_state=16)
    plt.hist(samples, bins=64, density=True)
    plt.title("N(0, 1)")

    plt.subplot(1, 2, 2)
    vals = np.linspace(-20, 20, 100)
    plt.plot(vals, scipy.stats.norm(loc=6, scale=3).pdf(vals))
    samples = sample_normal(loc=6, scale=3, num_inst=1000, random_state=32)
    plt.hist(samples, bins=64, density=True)
    plt.title("N(6, 3)")

    plt.show()

if __name__ == "__main__":
    _test()


from typing import Callable, List, Optional
import numpy as np

import lab1.src.grad.grad_step_strategy as st
import lab1.src.grad.stop_criteria as sc

DEFAULT_EPSILON = 1e-9
DEFAULT_MAX_ITERATIONS = 1e5

def gradient_descent(f: Callable[[np.ndarray], float],
                     f_grad: Callable[[np.ndarray], np.ndarray],
                     start: np.ndarray,
                     step_strategy: st.StepStrategy,
                     stop_criteria: sc.StopCriteria,
                     eps_strategy: float = DEFAULT_EPSILON,
                     eps_stop_criteria: float = DEFAULT_EPSILON,
                     max_iterations_strategy=DEFAULT_MAX_ITERATIONS,
                     max_iterations_criteria=DEFAULT_MAX_ITERATIONS,
                     trajectory: Optional[List] = None):
    strategy = st.get_step_strategy(step_strategy, f, f_grad, eps_strategy, max_iterations_strategy)
    criteria = sc.get_stop_criteria(stop_criteria, f, f_grad, eps_stop_criteria, max_iterations_criteria)
    cur_x = start
    iters = 0

    if trajectory is not None:
        trajectory.append(cur_x)

    while True:
        iters += 1
        cur_grad = f_grad(cur_x)
        step = strategy.next_step(cur_x)
        next_x = cur_x - step * cur_grad

        if criteria.should_stop(cur_x, next_x):
            return cur_x, iters

        cur_x = next_x
        if trajectory is not None:
            trajectory.append(cur_x)

        if iters == max_iterations_criteria:
            return cur_x, iters

if __name__ == '__main__':
    def foo(p):
        return p[0] ** 2 + p[1] ** 2

    def foo_grad(p):
        x, y = p[0], p[1]
        return np.array([2 * x, 2 * y])

    res, _ = gradient_descent(foo,
                              foo_grad,
                              start=np.array([3, 4]),
                              step_strategy=st.StepStrategy.DIVIDE_STEP,
                              stop_criteria=sc.StopCriteria.BY_GRAD)
    print(res)


import numpy as np
import cv2
import math

def medianFilter(image, size=3):
    return np.uint8(cv2.medianBlur(src=image, ksize=size))

def discreteFourierTransform(image):
    gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)

    dft = cv2.dft(np.float32(gray), flags = cv2.DFT_COMPLEX_OUTPUT)
    dft_shift = np.fft.fftshift(dft)

    magnitude_spectrum = 20*np.log(cv2.magnitude(dft_shift[:,:,0],dft_shift[:,:,1]))

    return magnitude_spectrum

def butterworthLowPassMask(width,height,dinit = 20,n = 1):
    width = width / 2
    height = height / 2

    hw = np.arange(-width, width)
    hh = np.arange(-height, height)

    x, y = np.meshgrid(hh, hw)
    mg = np.sqrt(x**2 + y**2)
    return 1 / (1 + (mg/dinit)**(2*n))

def butterworthHighPassMask(width,height,dinit = 20,n = 1):
    width = width / 2
    height = height / 2

    hw = np.arange(-width, width)
    hh = np.arange(-height, height)

    x, y = np.meshgrid(hh, hw)
    mg = np.sqrt(x**2 + y**2)
    return 1 / (1 + (dinit/mg)**(2*n))

def butterworthFilter(image, d=20, n=1, filter_type="low"):
    gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
    width, height = gray.shape

    mask = ""
    if filter_type == "low":
        mask = butterworthLowPassMask(width, height, d, n)
    elif filter_type == "high":
        mask = butterworthHighPassMask(width, height, d, n)

    fft = np.fft.fftshift(np.fft.fft2(np.float32(gray)))

    img_pass = mask * fft
    result = np.abs(np.fft.ifft2(np.fft.ifftshift(img_pass)))

    return result

def imsave(num, algo, img):
    imname = "radiograph_{}_{}.jpg".format(num, algo)
    cv2.imwrite("./output/" + imname, img)

if __name__ == "__main__":
    img_1 = cv2.imread("./samples/radiograph_1.jpg")
    img_2 = cv2.imread("./samples/radiograph_2.jpg")

    # Apply Median Filter with Adjusted Kernel's Size
    imsave(1, "median_filter", medianFilter(img_1, 15))
    imsave(2, "median_filter", medianFilter(img_2, 11))

    # Apply Discrete Fourier Transform
    imsave(1, "dft", discreteFourierTransform(img_1))
    imsave(2, "dft", discreteFourierTransform(img_2))

    # Apply Butterworth Lowpass Filter
    imsave(1, "butterworth_lowpass", butterworthFilter(img_1, 20, 2, "low"))
    imsave(2, "butterworth_lowpass", butterworthFilter(img_2, 20, 3, "low"))

    # Apply Butterworth Highpass Filter
    imsave(1, "butterworth_highpass", butterworthFilter(img_1, 100, 1, "high"))
    imsave(2, "butterworth_highpass", butterworthFilter(img_2, 40, 1, "high"))


# Necessary Packages
import numpy as np

#%% AR(1) Generation.
'''
X_t = coef * X_t-1 + n
n ~ N(0, sigma^2)
sigma = np.sqrt(margin*(1-coef*coef))
Therefore, Marginal distribution is N(0, margin)

Inputs
- n: Number of samples
- p: Number of features
- phi: Autoregressiveness
- margin: Std of the normal distribution
'''
def AR_Gauss_X1 (n, d, t, phi, sigma):

    # Initialization
    Output_X = list()

    # For each sample
    for i in range(n):

        Temp_Output_X = np.zeros([t,d])

        # For each feature
        for j in range(d):

            for k in range(t):

                # Starting feature
                if (k == 0):
                    Temp_Output_X[k,j] = np.random.normal(0,sigma)

                # AR(1) Generation
                else:
                    Temp_Output_X[k,j] = phi[j] * Temp_Output_X[k-1,j] + (1-phi[j])*np.random.normal(0,sigma)

        Output_X.append(Temp_Output_X)

    return Output_X

#%%
def AR_Gauss_X2 (n, d, t, phi, sigma, gamma):

    # Initialization
    Output_X = list()

    # For each sample
    for i in range(n):

        Temp_Output_X = np.zeros([t,2*d])

        # For each feature
        for j in range(d):

            for k in range(t):

                # Starting feature
                if (k == 0):
                    Temp_Output_X[k,j] = np.random.normal(0,sigma)

                # AR(1) Generation
                else:
                    Temp_Output_X[k,j] = phi[j] * Temp_Output_X[k-1,j] + (1-phi[j])*np.random.normal(0,sigma)

                Temp_Output_X[k,d+j] = Temp_Output_X[k,j] + np.random.normal(0,gamma)

        Output_X.append(Temp_Output_X)

    return Output_X


#!/usr/bin/env python3

import sys
import numpy as np
from math import pi, sqrt
import cvxopt
from cvxopt import matrix, solvers

def gen_eq_bloch_states(n, C=3.6, return_angles=True):
    '''
    Generates equally spaced points on the bloch sphere through a spiral.
    See here: https://www.intlpress.com/site/pub/files/_fulltext/journals/mrl/1994/0001/0006/MRL-1994-0001-0006-a003.pdf

    ==========
    Parameters
    ----------
    n (type=int): the number of points to be generated evenly on the sphere
    C (type=float, default=3.6): constant that makes sure that succesive points
                                 on S^2 will be the same Euclidean distance apart
    return_angles (type=boolean, default=True): whether to also return the angles
                                                associated with each state
    ==========

    ======
    Return
    ------
    (type=numpy.array): an array for n evenly spaced coordinates on the block sphere,
                         with their associated angles if needed
    ======
    '''
    states = np.zeros((n, 2), dtype=complex)
    h = np.zeros(n)
    theta = np.zeros(n)
    phi = np.zeros(n)

    # initialize
    h[0] = -1
    h[n-1] = -1 + 2*(n-1)/(n-1)
    theta[0] = np.arccos(h[0])
    theta[n-1] = np.arccos(h[n-1])
    phi[0] = 0
    phi[n-1] = 0
    states[0] = [np.cos(theta[0]/2), np.sin(theta[0]/2)*np.exp(1j*phi[0])]
    states[n-1] = [np.cos(theta[n-1]/2), np.sin(theta[n-1]/2)*np.exp(1j*phi[n-1])]

    # the rest of the states
    for k in range(1, n-1):
        h[k] = -1 + 2*(k)/(n-1)
        theta[k] = np.arccos(h[k])
        phi[k] = np.mod(phi[k] + C/(sqrt(n)*sqrt(1 - h[k]**2)), 2*pi)
        states[k] = [np.cos(theta[k]/2), np.sin(theta[k]/2)*np.exp(1j*phi[k])]

    if return_angles==True:
        return [states, theta, phi]
    else:
        return states

def vec(M):
    '''
    Converts an n x m matrix into a column vector, where the first n entries
    corresponds to the first column of the matrix, the next n entries to the
    second column, and so on.

    https://stackoverflow.com/a/25248378

    ==========
    Parameters
    ----------
    M (type=numpy.array): the matrix to use for the column vector
    ==========

    ======
    Return
    ------
    (type=numpy.array): the column vector
    ======
    '''
    return M.reshape((-1, 1), order="F")

def mat(v, n, m, dty=float):
    '''
    Converts a vector of length n*m to an n x m matrix where the first n entries
    corresponds to the first column of the matrix, the next n entries to the
    second column, and so on.

    ==========
    Parameters
    ----------
    v (type=numpy.array): the array to use for the matrix
    n (type=int): number of rows for the matrix
    m (type=int): number of columns for the matrix
    dty (type=type): type of the matrix
    ==========

    ======
    Return
    ------
    (type=numpy.array): the matrix
    ======
    '''
    M = np.zeros((n, m), dtype=dty)
    for i in range(n):
        for j in range(m):
            M[i][j] = v[n*j + i]
    return M

def s_min(E, W, F, n, m):
    '''
    Function that minimizes the estimated state space, S, with a fixed effect space
    according to the following quadratic program:

    minimize_{S} vec(S)^T @ (E x I_n) @ W (E^T x I_n) vec(S) - 2 vec(S)^T @ (E x I_n) @ W @ vec(F)

    subject to 0 <= (E^T x I_n) @ vec(S) <= 1 (element wise inequality)

    ==========
    Parameters
    ----------
    E (type=numpy.array): fixed effect space matrix; dim = k x m
    W (type=numpy.array): matrix that encodes the uncertainties along the diagonal
                          for each preparation/measurement pair; dim = n*m x n*m
    F (type=numpy.array): data matrix; dim = n x m
    n (type=int): number of preparations
    m (type=int): number of measurements
    ==========

    ======
    Return
    ------
    (type=numpy.array): the solution to the quadratic program
    ======
    '''
    I_n = np.identity(n, dtype=float)
    P = 2 * (np.kron(E, I_n)) @ W @ (np.kron(np.transpose(E), I_n))
    q = -2 * np.kron(E, I_n) @ W @ vec(F)
    G_0 = -np.kron(np.transpose(E), I_n)
    G_1 = np.kron(np.transpose(E), I_n)
    h_0 = (np.zeros(n*m))
    h_1 = (np.ones(n*m))

    P = matrix(P)
    q = matrix(q)
    G = matrix(np.concatenate([G_0, G_1]))
    h = matrix(np.concatenate([h_0, h_1]))

    return solvers.qp(P, q, G, h, kktsolver="chol")

def e_min(S, W, F, n, m):
    '''
    Function that minimizes the estimated effect space, E, with a fixed state space
    according to the following quadratic program:

    minimize_{E} vec(E)^T @ (I_m x S)^T @ W (I_m x S) vec(S) - 2 vec(S)^T @ (I_m x S)^T @ W @ vec(F)

    subject to 0 <= (I_m x S) @ vec(E) <= 1 (element wise inequality)

    ==========
    Parameters
    ----------
    S (type=numpy.array): fixed state space matrix; dim = k x m
    W (type=numpy.array): matrix that encodes the uncertainties along the diagonal
                          for each preparation/measurement pair; dim = n*m x n*m
    F (type=numpy.array): data matrix; dim = n x m
    n (type=int): number of preparations
    m (type=int): number of measurements
    ==========

    ======
    Return
    ------
    (type=numpy.array): the solution to the quadratic program
    ======
    '''
    I_m = np.identity(m, dtype=float)
    P = 2 * (np.transpose(np.kron(I_m, S))) @ W @ (np.kron(I_m, S))
    q = -2 * np.transpose(np.kron(I_m, S)) @ W @ vec(F)
    G_0 = -np.kron(I_m, S)
    G_1 = np.kron(I_m, S)
    h_0 = (np.zeros(n*m))
    h_1 = (np.ones(n*m))

    P = matrix(P)
    q = matrix(q)
    G = matrix(np.concatenate([G_0, G_1]))
    h = matrix(np.concatenate([h_0, h_1]))

    return solvers.qp(P, q, G, h, kktsolver="chol")

def chi_squared(S, E, F, W, n, m):
    '''
    Calculates the weighted chi^2 value.

    ==========
    Parameters
    ----------
    S (type=numpy.array): state space matrix; dim = n x k
    E (type=numpy.array): effect space matrix; dim = k x m
    F (type=numpy.array): data matrix; dim = n x m
    W (type=numpy.array): matrix that encodes the uncertainties along the diagonal
                          for each preparation/measurement pair; dim = n*m x n*m
    n (type=int): number of preparations
    m (type=int): number of measurements
    ==========

    ======
    Return
    ------
    (type=float): the chi^2 value
    ======
    '''
    sum = 0
    w_index = 0
    D = S @ E
    for i in range(n):
        for j in range(m):
            sum += ((F[i][j] - D[i][j]))**2 * W[w_index][w_index]
            w_index += 1
    return sum

def bfp(k, E_0, F, W, n, m, max_iterations=5000, convergence_threshold=10E-6):
    '''
    Finds the low-rank matrix that best fits the data matrix of frequencies.

    ==========
    Parameters
    ----------
    k (type=int): rank of best-fit matrix
    E_0 (type=numpy.array): initial estimate of effect space matrix; dim = k x m
    F (type=numpy.array): data matrix; dim = n x m
    W (type=numpy.array): matrix that encodes the uncertainties along the diagonal
                          for each preparation/measurement pair; dim = n*m x n*m
    n (type=int): number of preparations
    m (type=int): number of measurements
    max_iterations (type=int, default=5000): the max number of iterations to go through
                                             for the optimization
    convergence_threshold (type=float, default=10E-16): the convergence threshold for
                                                        the optimization
    ==========

    ======
    Return
    ------
    (type=numpy.array): an array that contains the estimated state space and effect space matrices
    ======
    '''
    S = np.zeros((n, k), dtype=float)
    E = E_0
    chi_squared_prev = 0
    chi_squared_curr = 0
    iteration = 1
    while (True):
        chi_squared_prev = chi_squared_curr
        S = mat(s_min(E, W, F, n, m)['x'], n, k)
        E = mat(e_min(S, W, F, n, m)['x'], k, m)
        chi_squared_curr = chi_squared(S, E, F, W, n, m)
        if (iteration == max_iterations or (chi_squared_curr -  chi_squared_prev) < convergence_threshold):
            break
        iteration += 1
    return [S, E]

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

def aic(k, chi_squared_k, m, n):
    r_k = k*(m + n - k)
    return chi_squared_k + r_k

def is_pos_semi_def(A, tol=1e-8):
    E = np.linalg.eigvalsh(A)
    return np.all(E > -tol)

def S_min(S, E, F, W, m):
    I_m = np.identity(m)
    A_1 = np.transpose(vec(S)) @ (np.kron(E, I_m)) @ W @ (np.kron(np.transpose(E), I_m)) @ vec(S)
    A_2 = 2 * np.transpose(vec(S)) @ (np.kron(E, I_m)) @ W @ vec(F)
    A = A_1 - A_2
    return A[0][0]

def E_min(E, S, F, W, n):
    I_n = np.identity(n)
    A_1 = np.transpose(vec(E)) @ np.transpose(np.kron(I_n, S)) @ W @ (np.kron(I_n, S)) @ vec(E)
    A_2 = 2 * np.transpose(vec(E)) @ np.transpose(np.kron(I_n, S)) @ W @ vec(F)
    A = A_1 - A_2
    return A[0][0]

def cvxopt_solve_qp(P, q, G=None, h=None, A=None, b=None):
    P = .5 * (P + P.T)  # make sure P is symmetric
    args = [cvxopt.matrix(P), cvxopt.matrix(q)]
    if G is not None:
        args.extend([cvxopt.matrix(G), cvxopt.matrix(h)])
        if A is not None:
            args.extend([cvxopt.matrix(A), cvxopt.matrix(b)])
    sol = cvxopt.solvers.qp(*args)
    if 'optimal' not in sol['status']:
        return None
    return np.array(sol['x']).reshape((P.shape[1],))

def quadprog_solve_qp(P, q, G=None, h=None, A=None, b=None):
    qp_G = .5 * (P + P.T)   # make sure P is symmetric
    qp_a = -q
    if A is not None:
        qp_C = -numpy.vstack([A, G]).T
        qp_b = -numpy.hstack([b, h])
        meq = A.shape[0]
    else:  # no equality constraint
        qp_C = -G.T
        qp_b = -h
        meq = 0
    return quadprog.solve_qp(qp_G, qp_a, qp_C, qp_b, meq)[0]

def wrla_1(U, V, A, W):
    A_tilda = U @ V
    summation = 0
    w = 0
    for i in range(len(A)):
        for j in range(len(A[0])):
            summation += W[w][w] * ((A[i][j] - A_tilda[i][j])**2)
    return summation

def wrla_2(V, U, A, W):
    A_tilda = U @ V
    summation = 0
    w = 0
    for i in range(len(A)):
        for j in range(len(A[0])):
            summation += W[w][w] * ((A[i][j] - A_tilda[i][j])**2)
    return summation


'''
------------------------------------
Assignment 9 - EE2703 (Jan-May 2020)
Done by Akilesh Kannan (EE18B122)
Created on 20/03/20
Last Modified on 27/04/20
------------------------------------
'''

# Imports
import cmath
import numpy as np
import numpy.fft as fft
import matplotlib.pyplot as plt
from scipy.linalg import lstsq
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm

# Global Variables
plotsDir = 'plots/'
PI = np.pi
figNum = 0
showAll = True

# Functions used
def hammingWindow(n):
    '''
                    0.54 + 0.46*cos(2πn/(N−1)), |n| <= (N-1)/2
        w[n] =
                    0, otherwise
    '''

    N = n.size
    window = np.zeros(N)
    window = 0.54 + 0.46*np.cos((2*PI*n)/(N-1))
    return fft.fftshift(window)

def plotSignal(t, x, figTitle, style='b-', blockFig=False, showFig=True, saveFig=False):
    global figNum
    plt.figure(figNum)
    plt.title(figTitle)
    plt.grid()
    plt.plot(t, x, style)
    if(saveFig):
        plt.savefig(plotsDir + "Fig"+str(figNum)+".png")
    if(showFig):
        plt.show(block=blockFig)
    figNum+=1

def plotSpectrum(figTitle, w, Y, magStyle='b-', phaseStyle='ro', xLimit=None, yLimit=None, showFig=False, saveFig=True, blockFig=False):
    global figNum
    plt.figure(figNum)
    plt.suptitle(figTitle)
    plt.subplot(211)
    plt.grid()
    plt.plot(w, abs(Y), magStyle, lw=2)
    plt.ylabel(r"$\|Y\|$")
    if (xLimit):
        plt.xlim(xLimit)
    if (yLimit):
        plt.ylim(yLimit)
    plt.subplot(212)
    plt.grid()
    plt.plot(w, np.angle(Y), phaseStyle, lw=2)
    plt.xlim(xLimit)
    plt.ylabel(r"$\angle Y$")
    plt.xlabel(r"$\omega\ \to$")

    if(saveFig):
        plt.savefig(plotsDir + "Fig"+str(figNum)+".png")
    if(showFig):
        plt.show(block=blockFig)
    figNum+=1

# Example 1 - sin(sqrt(2)t)

    ## Without windowing

t = np.linspace(-PI, PI, 65)[:-1]
dt = t[1]-t[0]
fmax = 1/dt
y = np.sin(cmath.sqrt(2)*t)
y[0] = 0
y = fft.fftshift(y)
Y = fft.fftshift(fft.fft(y))/64.0
w = np.linspace(-PI*fmax, PI*fmax, 65)[:-1]
plotSpectrum(r"Spectrum of $sin(\sqrt{2}t)$", w, Y, xLimit=[-10, 10], showFig=showAll)

    ## Windowing with Hamming Window

t = np.linspace(-PI, PI, 65)[:-1]
dt = t[1]-t[0]
fmax = 1/dt
n = np.arange(64)
y = np.sin(cmath.sqrt(2)*t) * hammingWindow(n)
y[0] = 0
y = fft.fftshift(y)
Y = fft.fftshift(fft.fft(y))/64.0
w = np.linspace(-PI*fmax, PI*fmax, 65)[:-1]
plotSpectrum(r"Spectrum of $sin(\sqrt{2}t) * w(t)$", w, Y, xLimit=[-8, 8], showFig=showAll)

# Question 2 - spectrum of (cos(0.86 t))**3

    ## Without windowing

t = np.linspace(-4*PI, 4*PI, 257)[:-1]
dt = t[1]-t[0]
fmax = 1/dt
y = np.cos(0.86*t)**3
y[0] = 0
y = fft.fftshift(y)
Y = fft.fftshift(fft.fft(y))/256.0
w = np.linspace(-PI*fmax, PI*fmax, 257)[:-1]
plotSpectrum(r"Spectrum of $cos^3(0.86t)$", w, Y, xLimit=[-8, 8], showFig=showAll)

    ## Windowing with Hamming Window

t = np.linspace(-4*PI, 4*PI, 257)[:-1]
dt = t[1]-t[0]
fmax = 1/dt
n = np.arange(256)
y = (np.cos(0.86*t))**3 * hammingWindow(n)
y[0] = 0
y = fft.fftshift(y)
Y = fft.fftshift(fft.fft(y))/256.0
w = np.linspace(-PI*fmax, PI*fmax, 257)[:-1]
plotSpectrum(r"Spectrum of $cos^3(0.86t) * w(t)$", w, Y, xLimit=[-8, 8], showFig=showAll)

def estimateWandD(w, wo, Y, do, pow=2):
    wEstimate = np.sum(abs(Y)**pow * abs(w))/np.sum(abs(Y)**pow) # weighted average
    print("wo = {:.03f}\t\two (Estimated) = {:.03f}".format(wo, wEstimate))

    t = np.linspace(-PI, PI, 129)[:-1]
    y = np.cos(wo*t + do)

    c1 = np.cos(wEstimate*t)
    c2 = np.sin(wEstimate*t)
    A = np.c_[c1, c2]
    vals = lstsq(A, y)[0]
    dEstimate = np.arctan2(-vals[1], vals[0])
    print("do = {:.03f}\t\tdo (Estimated) = {:.03f}".format(do, dEstimate))

# Question 3 - Estimation of w, d in cos(wt + d)
wo = 1.35
d = PI/2

print("Question 3:")
t = np.linspace(-PI, PI, 129)[:-1]
trueCos = np.cos(wo*t + d)
fmax = 1.0/(t[1]-t[0])
n = np.arange(128)
y = trueCos.copy()*hammingWindow(n)
y = fft.fftshift(y)
Y = fft.fftshift(fft.fft(y))/128.0
w = np.linspace(-PI*fmax, PI*fmax, 129)[:-1]
plotSpectrum(r"Spectrum of $cos(\omega_o t + \delta) \cdot w(t)$", w, Y, xLimit=[-4, 4], showFig=showAll, saveFig=False)
estimateWandD(w, wo, Y, d, pow=1.75)

# Question 4 - Estimation of w, d in noisy cos(wt + d)

print("\nQuestion 4:")
trueCos = np.cos(wo*t + d)
noise = 0.1*np.random.randn(128)
n = np.arange(128)
y = (trueCos + noise)*hammingWindow(n)
fmax = 1.0/(t[1]-t[0])
y = fft.fftshift(y)
Y = fft.fftshift(fft.fft(y))/128.0
w = np.linspace(-PI*fmax, PI*fmax, 129)[:-1]
plotSpectrum(r"Spectrum of $(cos(\omega_o t + \delta) + noise) \cdot w(t)$", w, Y, xLimit=[-4, 4], showFig=showAll, saveFig=False)
estimateWandD(w, wo, Y, d, pow=2.5)

# Question 5 - DFT of chirp

# chirp function used
def chirp(t):
    return np.cos(16*(1.5*t + (t**2)/(2*PI)))

t = np.linspace(-PI, PI, 1025)[:-1]
x = chirp(t)
plotSignal(t, x, r"$cos(16(1.5 + \frac{t}{2\pi})t)$", saveFig=True)
fmax = 1.0/(t[1]-t[0])
X = fft.fftshift(fft.fft(x))/1024.0
w = np.linspace(-PI*fmax, PI*fmax, 1025)[:-1]
plotSpectrum(r"DFT of $cos(16(1.5 + \frac{t}{2\pi})t)$", w, X, 'b-', 'r.-', [-75, 75], showFig=showAll, saveFig=True)

n = np.arange(1024)
x = chirp(t)*hammingWindow(n)
plotSignal(t, x, r" $cos(16(1.5 + \frac{t}{2\pi})t) \cdot w(t)$", saveFig=True)
X = fft.fftshift(fft.fft(x))/1024.0
plotSpectrum(r"DFT of $cos(16(1.5 + \frac{t}{2\pi})t) \cdot w(t)$", w, X, 'b-', 'r.-', [-75, 75], showFig=showAll, saveFig=True)

# Question 6 - Time evolution of DFT of chirp signal

# calculates DFT of x, taking every batchSize samples
def STFT(x, t, batchSize=64):
    t_batch = np.split(t, 1024//batchSize)
    x_batch = np.split(x, 1024//batchSize)
    X = np.zeros((1024//batchSize, batchSize), dtype=complex)
    for i in range(1024//batchSize):
        X[i] = fft.fftshift(fft.fft(x_batch[i]))/batchSize
    return X

# plots the STFT
def plot3DSTFT(t, w, X, colorMap=cm.viridis, showFig=showAll, saveFig=True, blockFig=False):
    global figNum

    t = t[::64]
    w = np.linspace(-fmax*PI,fmax*PI,65)[:-1]
    t, w = np.meshgrid(t, w)

    fig = plt.figure(figNum)
    ax = fig.add_subplot(211, projection='3d')
    surf = ax.plot_surface(w, t, abs(X).T, cmap=colorMap)
    fig.colorbar(surf)
    plt.xlabel(r"Frequency $\to$")
    plt.ylabel(r"Time $\to$")
    plt.title(r"Magnitude $\|Y\|$")

    ax = fig.add_subplot(212, projection='3d')
    surf = ax.plot_surface(w, t, np.angle(X).T, cmap=colorMap)
    fig.colorbar(surf)
    plt.xlabel(r"Frequency $\to$")
    plt.ylabel(r"Time $\to$")
    plt.title(r"Angle $\angle Y$")
    if saveFig:
        plt.savefig(plotsDir+"Fig"+str(figNum)+".png")
    if showFig:
        plt.show(block=blockFig)

    figNum+=1

x = chirp(t)
X = STFT(x, t)
plot3DSTFT(t, w, X, colorMap=cm.plasma)

x = chirp(t)*hammingWindow(np.arange(1024))
X = STFT(x, t)
plot3DSTFT(t, w, X, blockFig=True)


# Create a 2D Numpy Array

# Import the libraries
import numpy as np
import matplotlib.pyplot as plt

# Consider the list a, the list contains three nested lists each of equal size.

# Create a list
a = [[11, 12, 13], [21, 22, 23], [31, 32, 33]]
print(a)

# We can cast the list to a Numpy Array as follow
# Convert list to Numpy Array
# Every element is the same type
A = np.array(a)
print(A)

# Show the numpy array dimensions
print(A.ndim)

# Attribute shape returns a tuple corresponding to the size or number of each dimension.
# Show the numpy array shape
print(A.shape)

# The total number of elements in the array is given by the attribute size.
# Show the numpy array size
print(A.size)

# We simply use the square brackets and the indices corresponding to the element we would like:
# Access the element on the second row and third column
print(A[1, 2])

# We can also use the following notation to obtain the elements:
# Access the element on the second row and third column
print(A[1][2])

# We can access the element as follows
# Access the element on the first row and first column
print(A[0][0])

# This can be done with the following syntax
# Access the element on the first row and first and second columns
print(A[0][0:2])

# Similarly, we can obtain the first two rows of the 3rd column as follows:
# Access the element on the first and second rows and third column
print(A[0:2, 2])

# The numpy array is given by X and Y
# Create a numpy array X
X = np.array([[1, 0], [0, 1]])
print(X)

# Create a numpy array Y
Y = np.array([[2, 1], [1, 2]])
print(Y)

# We can add the numpy arrays as follows.
# Add X and Y
Z = X + Y
print(Z)

# We can perform the same operation in numpy as follows
# Create a numpy array Y
Y = np.array([[2, 1], [1, 2]])
print(Y)

# Multiply Y with 2
Z = 2 * Y
print(Z)

# We can perform element-wise product of the array X and Y as follows:
# Create a numpy array Y
Y = np.array([[2, 1], [1, 2]])
print(Y)

# Create a numpy array X
X = np.array([[1, 0], [0, 1]])
print(X)

# Multiply X with Y
Z = X * Y
print(Z)

# We can also perform matrix multiplication with the numpy arrays A and B as follows:
# First, we define matrix A and B:
# Create a matrix A
A = np.array([[0, 1, 1], [1, 0, 1]])
print(A)

# Create a matrix B
B = np.array([[1, 1], [1, 1], [-1, 1]])
print(B)

# Calculate the dot product
Z = np.dot(A,B)
print(Z)

# Calculate the sine of Z
print(np.sin(Z))

# We use the numpy attribute T to calculate the transposed matrix
# Create a matrix C
C = np.array([[1,1],[2,2],[3,3]])
print(C)

# Get the transposed of C
print(C.T)

# Quiz

# Consider the following list a, convert it to Numpy Array.
a = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
A = np.array(a)
print(A)

# Calculate the numpy array size.
print(A.size)

# Access the element on the first row and first and second columns.
print(A[0][0:2])

# Perform matrix multiplication with the numpy arrays A and B.
B = np.array([[0, 1], [1, 0], [1, 1], [-1, 0]])
X = np.dot(A,B)
print(X)