import numpy as np
import pymc3 as pm
import matplotlib.pyplot as plt
from scipy.stats import binom
from scipy.stats import mode

import seaborn as sns

p_positive_when_vampire = 0.95
p_positive_when_mortal = 0.01
p_is_vampire = 0.001
p_positive = p_positive_when_vampire*p_is_vampire+p_positive_when_mortal*(1. - p_is_vampire)

p_vampire_positive = p_positive_when_vampire*p_is_vampire / p_positive

print('the probability that you are a vampire given the vampire blood test was positive:%f'%p_vampire_positive)

def grid_aprox(grid_points=100,success=6,tosses=9):
    p_grid = np.linspace(0.,1.,grid_points)
    prior = np.repeat(5,grid_points)
    likehood = binom.pmf(success,tosses,p_grid)
    posterior = likehood*prior
    posterior = posterior/posterior.sum()

    return p_grid, posterior

print('--------------6/9-----------')
p_grid,posterior = grid_aprox(1000,6,9)

# plt.plot(p_grid,posterior)
# plt.show()

samples = np.random.choice(p_grid,p=posterior,size=int(1e4),replace=True)
_,(ax0,ax1,ax2) = plt.subplots(1,3)
ax0.plot(p_grid,posterior)
ax1.plot(samples,'o')
sns.kdeplot(samples,ax=ax2)
plt.show()

p_less_then_0_5 = sum(posterior[p_grid < 0.5])
print('posterior < 0.5 = %f'%p_less_then_0_5)
samples_less_0_5 = sum(samples < 0.5)/1e4
print('samples < 0.5 = %f'%samples_less_0_5)

print('between 0.5 and 0.75 posterior:%f'%(sum(posterior[(p_grid > 0.5) & (p_grid < 0.75)])))
print('between 0.5 and 0.75 samples:%f'%(sum((samples > 0.5) & (samples < 0.75))/1e4))

print('80 percentile:'+str(np.percentile(samples,80)))
print('middle 80 percentile(between 10 and 90 percent):'+str(np.percentile(samples,[10,90])))
print('middle 50 percentile(between 25 and 75 percent):'+str(np.percentile(samples,[25,75])))
print('high posterior density percentile interval 50:'+str(pm.hpd(samples,alpha=0.5)))
print('middle 50 percentile(between 80 and 90 percent):'+str(np.percentile(samples,[80,90])))
print('high posterior density percentile interval 80:'+str(pm.hpd(samples,alpha=0.8)))
print('high posterior density percentile interval 90:'+str(pm.hpd(samples,alpha=0.9)))
print('high posterior density percentile interval 95:'+str(pm.hpd(samples,alpha=0.95)))
print('maximum posteriori at prob =(%f,%f)'%(max(posterior),p_grid[posterior == max(posterior)]))
print('maximum posteriori at prob from samples: %f'%mode(samples)[0])
print('mean and median : %f,%f' %(np.mean(samples),np.median(samples)))
print('expected loss:%f'%(sum(posterior*abs(0.5-p_grid))))
loss = [sum(posterior*abs(p-p_grid)) for p in p_grid]
plt.plot(p_grid,loss)
plt.show()
print('min loss at probability:%f'%p_grid[loss == min(loss)])

print('expected loss:%f'%(sum(posterior*np.power(0.5-p_grid,2))))
loss = [sum(posterior*np.power(p-p_grid,2)) for p in p_grid]
plt.plot(p_grid,loss)
plt.show()
print('min loss at probability:%f'%p_grid[loss == min(loss)])

print('--------------3/3-----------')
p_grid,posterior = grid_aprox(1000,3,3)
samples = np.random.choice(p_grid,p=posterior,size=int(1e4),replace=True)
_,(ax0,ax1,ax2) = plt.subplots(1,3)
ax0.plot(p_grid,posterior)
ax1.plot(samples,'o')
sns.kdeplot(samples,ax=ax2)
plt.show()
print('middle 50 percentile(between 25 and 75 percent):'+str(np.percentile(samples,[25,75])))
print('high posterior density percentile interval 50:'+str(pm.hpd(samples,alpha=0.5)))
print('middle 50 percentile(between 80 and 90 percent):'+str(np.percentile(samples,[80,90])))
print('high posterior density percentile interval 80:'+str(pm.hpd(samples,alpha=0.8)))
print('high posterior density percentile interval 90:'+str(pm.hpd(samples,alpha=0.9)))
print('high posterior density percentile interval 95:'+str(pm.hpd(samples,alpha=0.95)))
print('maximum posteriori at prob =(%f,%f)'%(max(posterior),p_grid[posterior == max(posterior)]))
print('maximum posteriori at prob from samples: %f'%mode(samples)[0])
print('mean and median : %f,%f' %(np.mean(samples),np.median(samples)))

print('expected loss:%f'%(sum(posterior*abs(0.5-p_grid))))
loss = [sum(posterior*abs(p-p_grid)) for p in p_grid]
plt.plot(p_grid,loss)
plt.show()
print('min loss at probability:%f'%p_grid[loss == min(loss)])

print('expected loss:%f'%(sum(posterior*np.power(0.5-p_grid,2))))
loss = [sum(posterior*np.power(p-p_grid,2)) for p in p_grid]
plt.plot(p_grid,loss)
plt.show()
print('min loss at probability:%f'%p_grid[loss == min(loss)])

print('-----------------simulating---------')
# simulate globe tossing for water with 0.7 prob. in 2 throws
data = binom.pmf(range(3),2,0.7)
# 0 water 0.09, 1 water 0.42, 2 water 0.49
print(data)
# sample from distribution of simulation data - it is binom, so sample from binom distribution
# we are generating observations...that mean how many times did we see water when we tossed the globe 2x
samples = binom.rvs(n=2, p=0.7, size=1)
print(samples)
samples = binom.rvs(n=2, p=0.7, size=10)
print(samples)
dummy_w = binom.rvs(n=2, p=0.7, size=100000)
means = [(dummy_w == i).mean() for i in range(3)]
print(means)

dummy_w = binom.rvs(n=9, p=0.7, size=100000)
means = [(dummy_w == i).mean() for i in range(9)]
print(means)
plt.hist(dummy_w,bins=50)
plt.show()

dummy_w = binom.rvs(n=9, p=0.6, size=100000)
means = [(dummy_w == i).mean() for i in range(9)]
print(means)
plt.hist(dummy_w,bins=50)
plt.show()

dummy_w = binom.rvs(n=9, p=0.5, size=100000)
means = [(dummy_w == i).mean() for i in range(9)]
print(means)
plt.hist(dummy_w,bins=50)
plt.show()

dummy_w = binom.rvs(n=9, p=0.4, size=100000)
means = [(dummy_w == i).mean() for i in range(9)]
print(means)
plt.hist(dummy_w,bins=50)
plt.show()

dummy_w = binom.rvs(n=9, p=0.3, size=100000)
means = [(dummy_w == i).mean() for i in range(9)]
print(means)
plt.hist(dummy_w,bins=50)
plt.show()

#generate samples from posterior
p_grid, posterior = grid_aprox(grid_points=1000, success=6, tosses=9)
np.random.seed(100)
samples = np.random.choice(p_grid, p=posterior, size=int(1e4), replace=True)
dummy_w = binom.rvs(n=9, p=samples)
_,(ax0,ax1) = plt.subplots(1,2)
ax0.plot(posterior)
ax1.hist(dummy_w,bins=50)
plt.show()


import math
import numpy as np
from Jacobi import jacobi
from Printer import print_results
from Gauss_Seidel import gauss_seidel
from SOR import sor
'''
MATH 5336, Homework #6

Main function area, run program from here.

A and b matrices were created from HW #6 description.

We will use x = [0,0,0,0,0,0,0,0] as starting value

Prints number of iterations that each method took along with their X values

Printing will be done with the help of a helper printer file
'''

A = np.array([[-1, 0, 0, math.sqrt(2)/2, 1, 0, 0, 0],
    [0, -1, 0, math.sqrt(2)/2, 0, 0, 0, 0],
   [0, 0, -1, 0, 0, 0, 1/2, 0],
   [0, 0, 0, -math.sqrt(2)/2, 0, -1, -1/2, 0],
   [0, 0, 0, 0, -1, 0, 0, 1],
   [0, 0, 0, 0, 0, 1, 0, 0],
   [0, 0, 0, -math.sqrt(2)/2, 0, 0, math.sqrt(3)/2, 0],
   [0, 0, 0, 0, 0, 0, math.sqrt(3)/2, -1]
   ])
b = np.array([0,0,0,0,0,10000,0,0])
x = np.array([0,0,0,0,0,0,0,0])

# Uses built-in linalg solver to find X
x_solved = np.dot(np.linalg.inv(A), b)

# Print actual solution for comparison
print("The actual solution using the built-in matrix solver is: ")
print_results(x_solved)

# Runs Jacobi's method and prints its results
x_jacobi = jacobi(A, b, x, error=10**-8, actual_soln = x_solved)
print()
print("Jacobi's method took {} iterations: " .format(x_jacobi[1]))
print_results(x_jacobi[0])

# Runs Gauss Seidel method and prints its results
x_gauss_seidel = gauss_seidel(A, b, x, error=10**-8, actual_soln=x_solved)
print()
print("Gauss Seidel method took {} iterations: ".format(x_gauss_seidel[1]))
print_results(x_gauss_seidel[0])

# Runs SOR with w = 1.25 and prints its results
x_sor = sor(A, b, x, error=10**-8, actual_soln=x_solved, w=1.25)
print()
print("SOR method took {} iterations: ".format(x_sor[1]))
print_results(x_sor[0])


# -*- coding: utf-8 -*-
"""
Created on Wed Nov 17 12:38:15 2021

@author: Oscar
"""

# Golden Search, mimics the secant method, but for finding the Global Max and min (optimization of a function)
# Strategy in selecting the bounds of the interval:
    # l0 = distance between estimate,
    # l0 = l1+l2 ; l1/l0 = l2/l1
    # R = (l2/l1)**-1 (reciprocal)
    # From substitution : 1 +R = 1/R -> R**2 + R - 1 = 0
    # R = [sqrt(5)-1]/2 <- GOLDEN RATIO
        # d = R(x_u - x_l)
        #x1 = x_l + d ; x2 = x_u - d

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

"""
Interval Selection
"""
# Parameters
xu = 20 #int(input("Please choose a upper bound: "))
xl = -20 #int(input("Please choose a lower bound: "))
N = 100 #int(input("Please choose Maxt number of iterations: "))
# Golden Ratio
R = (math.sqrt(5) - 1)/2

"""
Evaluation of the Function
"""
# Evaluated function
f = lambda x: 2*np.sin(x) - x**2/10

def GoldenSearchMax(xu, xl, f, N):

    for i in range(0, N-1):
        # Intermediate points
        d = R*(xu - xl)

        x1 = xl + d
        x2 = xu - d

        fx1, fx2 = f(x1), f(x2)

        if fx1 > fx2 :
            xl = x2

        elif fx1 < fx2:
            xu = x1

        else:
            #print("The local maxima is located at:", x1, fx1)
            break
    return x1, fx1

def GoldenSearchMin(xu, xl, f, N):

    for i in range(0, N-1):
        # Intermediate points
        d = R*(xu - xl)

        x1 = xl + d
        x2 = xu - d

        fx1, fx2 = f(x1), f(x2)

        if fx1 < fx2 :
            xl = x2

        elif fx1 > fx2:
            xu = x1

        else:
            #print("The local minima is located at:", x1, fx1)
            break
    return x1, fx1

# Arrays to store the numbers
Max = GoldenSearchMax(xu, xl, f, N)
Min = GoldenSearchMin(xu, xl, f, N)
print('The local max and min of the interval is:', Max, Min)

# Initializing Arrays
x_value = np.linspace(xl, xu, N-1)
y_value = np.zeros(N-1)

# Populating y_array
for k in range(N-1):
    y_value[k] = f(x_value[k])

# Plotting the function f
plt.plot(x_value ,y_value)
plt.scatter(Max[0], Max[1], label = 'Maxima', color = 'r')
plt.scatter(Min[0], Min[1], label = 'Maxima', color = 'g')
plt.legend(['Function', 'Maxima', 'Minima'])
plt.xlabel('x')
plt.ylabel('y')
plt.show()


# -*- coding: utf-8 -*-
"""
Created on Wed Oct 20 13:37:44 2021

@author: Oscar
"""

# Newton Raph Method is anohter open root method to finding the roots  of a function
    # The function in question is evaluated at an initially choosen point
    # A tangent line wrt the point is then drawn and intersects the x-axis
    # Repeats until the root is found
        # x_new =  x - f(x)/f'(x)
import sympy as sp

# Define The required Parameters
x0 = 1 #int(input("Initial Estimate:"))
N = 100 #int(input("Max Iterations:"))
e = 0.000001#float(input("Tolerance:"))
# Equation that needs to be evaluated

f = lambda x: x**2 +4*x -12
df = lambda x: 2*x +4

def NewtonRapshon(f, df, e, N):

    xn = x0

    for i in range(0,N):
        # If the value of the function at x is less than the tolerance, we print xn as it is close to the root
        if abs(f(xn)) < e:
            abs_error=(xn-2)/2
            print('Found solution after',i,'iterations.')
            return xn, abs_error
        # Find the derivative at the value of xn, if none exists
        if df(xn) == 0:
            print('Zero derivative. No solution found.')
            return None
        # Calculate the new value of xn and repeat the process
        xn = xn - f(xn)/df(xn)

    # If the Max number of iterations are reached, stop the program
    print('Exceeded maximum iterations. No solution found.')

    return

solution = NewtonRapshon(f, df, e, N)
print(solution)


import numpy as np
import warnings

def quadratic1(a,b,c):
    '''
    Solve an quadratic equation
    '''
    if b**2-4*a*c < 0: x = np.nan
    elif b**2-4*a*c == 0: x = -b/(2*a)
    else: x = np.array(((-b+np.sqrt(b**2-4*a*c))/(2*a), (-b-np.sqrt(b**2-4*a*c))/(2*a)))
    return x

def demean(d):
    return d - d.mean(axis=0)[np.newaxis, :]

def is_outliers(data):
    '''
    Check outliers.
    Checking if the giving number's zscore is above 2.5.
    '''
    with warnings.catch_warnings():
        warnings.simplefilter("ignore", category=RuntimeWarning)
        mean = np.nanmean(data, axis=0)
        sd = np.sqrt(np.nanmean((data - mean)**2, axis=0))
    zdata = (data - mean) / sd
    return abs(zdata) > 2.5

def imputedata(data, strategy='mean'):
    '''
    impute outliers and missing data
    impute outlier with mean or mean+-2sd
    missing data (np.nan) will impute as mean
    '''
    with warnings.catch_warnings():
        warnings.simplefilter("ignore", category=RuntimeWarning)
        mean = np.nanmean(data, axis=0)
        std = np.sqrt(np.nanmean((data - mean)**2, axis=0))

    data_sign = np.sign(data - mean)
    data_sign[np.isnan(data_sign)] = 0 # missing data will be imputed as mean

    is_out = is_outliers(data)
    data[is_out] = np.nan

    for i in range(data.shape[1]):
        ind_nan = np.where(np.isnan(data[:, i]))
        if strategy == '2sd':
            data[ind_nan, i] = mean[i] + (std[i] * 2 * data_sign[ind_nan, i])
        if strategy == 'mean':
            data[ind_nan, i] = mean[i]
    return data

def mean_nonzero(data, axis):
    '''
    calculate the non zero elements in a 2-D matrix
    '''
    with warnings.catch_warnings():
        warnings.simplefilter("ignore", category=RuntimeWarning)
        temp_sum = np.sum(data != 0, axis=axis)
        temp_sum[temp_sum == 0] = 1
        return np.sum(data, axis=axis)/temp_sum

import pickle
def load_pkl(file):
    '''
    load pickled file
    '''
    with open(file, 'rb') as handle:
        return pickle.load(handle)

def flatten(corrmat):
    '''
    flatten the correlation coefficient matrix in to a flat vector
    '''

    triu_inds = np.triu_indices(corrmat.shape[0], 1)
    corrmat_vect = corrmat[triu_inds]
    return corrmat_vect

def unflatten(corrmat_vect):
    '''
    Transform the flattened correlation matrice back to matrices
    for visualisation
    '''

    # figure out the size
    y = corrmat_vect.shape[0]
    x = quadratic1(0.5, -0.5, -y)
    x = int(np.max(x))

    idx = np.triu_indices(x, 1)
    corr_mat = np.zeros((x, x))
    corr_mat[idx] = corrmat_vect
    corr_mat = corr_mat + corr_mat.T

    return corr_mat


from math import e, factorial

import numpy as np

fac = np.vectorize(factorial)

def e_x(x, terms=10):
    """Approximates e^x using a given number of terms of
    the Maclaurin series
    """
    n = np.arange(terms)
    return np.sum((x ** n) / fac(n))

if __name__ == "__main__":
    print("Actual:", e ** 3)  # Using e from the standard library

    print("N (terms)\tMaclaurin\tError")

    for n in range(1, 14):
        maclaurin = e_x(3, terms=n)
        print(f"{n}\t\t{maclaurin:.03f}\t\t{e**3 - maclaurin:.03f}")


import numpy as np

def brownian_motion(t, kappa=1.0):
    '''
    Compute the 1D Brownian motion at some given time instants. The Brownian
    motion is such that each increment B(t[i]) - B(t[i-1]) are independent
    random variables normally distributed with zero mean an kappa*(t[i]-t[i-1])
    variance.

    Parameters
    ----------
    t : 1d ndarray of floats
        Time instants where the Brownian motion is to be evaluated.

    kappa : float, optional
        Diffusion constant. Default is 1.0.

    Returns
    -------
    u : 1d ndarray of floats
        1D Brownian motion evaluated. Same shape as t.
    '''
    dt = np.ediff1d(t, to_begin=t[0])
    du = np.random.randn(len(t)) * np.sqrt(dt * kappa)
    u = np.add.accumulate(du)
    return u

def fractional_brownian_motion(n, h, b=1.0, tf=1.0):
    '''
    Compute the 1D fractional Brownian motion. These are stochastic processes
    with long range power-law correlations. The mean square displacement
    behaves as

    <B(t)^2> = b t^(2h)

    where b is the diffusion constant and h is the Hurst exponent.

    It uses the Davies-Harte algorithm.

    Reference
    ---------
    Davies, Robert B., and D. S. Harte. "Tests for Hurst effect."
    Biometrika 74.1 (1987): 95-101.

    Parameters
    ----------
    n : int
        Number of point the function will produce.

    h : float
        Hurst exponent.

    b : float, optional
        Diffusion constant. Default is 1.0.

    tf : float, optional
        Endind time, so the brownian motion is evaluated at
        n time instants uniformly spaced in the interval [0, tf].
        Default if 1.0.

    Returns
    -------
    t : ndarray of floats
        Time instants where the fractional Brownian motion was evaluated.

    X : ndarray of floats
        Values of fractional Brownian motion.
    '''
    n2 = 2 * n
    h2 = 2 * h

    i = np.arange(0, n+1)
    s = np.zeros(n2)
    s[:n+1] = np.abs(i+1) ** h2 + np.abs(i-1) ** h2 - 2 * np.abs(i) ** h2
    s[:n+1] *= b * 0.5
    s[n+1:] = s[n-1:0:-1]

    A = np.fft.fft(s).real
    assert (A >= 0).all()

    Y = np.empty(n2, dtype=np.complex128)
    Y[0] = 2**0.5 * np.random.randn()
    Y[n] = 2**0.5 * np.random.randn()
    Y[1:n] = np.random.randn(n-1) + 1j * np.random.randn(n-1)
    Y[n+1:n2] = Y[n-1:0:-1].conj()
    Y *= np.sqrt(A*n)

    X = np.fft.ifft(Y).real[:n]
    X = np.cumsum(X)
    X -= X[0]

    t = np.linspace(0.0, tf, n)
    X *= (tf / n) ** h

    return t, X


from __future__ import division  # take the division operator from future versions
import numpy as np
import math
import toolboxutilities as util

def least_squares(matrix_a, vector_y):
    '''
    This function computes the estimate :math:`\\hat x` given by the least squares method
    :math:`\\hat x = {\\rm arg}\\min_x\\,\\lVert \\mathbf{y - Ax} \\rVert_2^2`.
    This is the simplest algorithm to solve a linear inverse problem of the form :math:`\\mathbf{y = Ax + n}`, where
    :math:`\\mathbf{y}` (vector) and :math:`\\mathbf{A}` (matrix) are known and :math:`\\mathbf{x}`  (vector)
     and :math:`\\mathbf{n}`  (vector) are unknown.

    :param matrix_a: (np.matrix) matrix :math:`\\mathbf{A}`
    :param vector_y: (array) vector :math:`\\mathbf{y}`

    :return vector_x: estimate :math:`\\hat x` given by least squares

    Example : compute the least squares solution of a system :math:`\\mathbf{y = Ax}`

    .. code-block:: python

        import numpy as np
        import linvpy as lp

        A = np.matrix([[1,3],[3,4],[4,5]])
        y = [-6,1,-2]

        # Returns x_hat, the least squares solution of y = Ax
        lp.least_squares(A,y)

        # [ 3.86666667 -3.18666667]

    '''

    # Ensures np.matrix type
    matrix_a = np.matrix(matrix_a)

    # x = (A' A)^-1 A' y
    vector_x = np.dot(
        np.dot(
            np.linalg.inv(
                np.dot(
                    matrix_a.T, # A.T returns the transpose of A
                    matrix_a
                    )
            ),
            matrix_a.T
        ),
        vector_y
    )

    # Flattens result into an array
    vector_x = np.squeeze(np.asarray(vector_x))

    return vector_x

def tikhonov_regularization(matrix_a, vector_y, lambda_parameter=0):
    '''
    The standard approach to solve the problem :math:`y = Ax + n' explained above is to use the  ordinary least squares
    method. However if your matrix :math:`A` is a fat matrix (it has more columns than rows) or it has a large condition number,
    then you should use a regularization to your problem in order to get a meaningful estimation of :math:`x`.

    The Tikhonov regularization is a tradeoff between the least squares
    solution and the minimization of the L2-norm of the output :math:`x` (L2-norm =
    sum of squared values of the vector :math:`x`),
    :math:`\\hat x = {\\rm arg}\\min_x\\,\\lVert \\mathbf{y - Ax} \\rVert_2^2 + \\lambda\\lVert x \\rVert_2^2  `

    The parameter lambda tells how close to the least squares solution the
    output :math:`x` will be; a large lambda will make :math:`x` close to L2-norm(x)=0, while
    a small lambda will approach the least squares solution (Running
    the function with lambda=0 will behave like the ordinary leat_squares()
    method).

    The solution is given by :math:`\\hat{\\mathbf{x}} = (A^{T}A+ \\lambda^{2} I)^{-1}A^{T}\\mathbf{y}`, where :math:`I` is the identity matrix.

    Raises a ValueError if lambda < 0.

    :param matrix_a: (np.matrix) matrix A in :math:`y = Ax + n'
    :param vector_y: (array) vector y in :math:`y = Ax + n'
    :param lambda: (int) lambda non-negative parameter to regulate the tradeoff.

    :return array: vector_x solution of Tikhonov regularization

    :raises ValueError: raises an exception if lambda_parameter < 0

    Example : compute the solution of a system y = Ax (knowing y, A) which is a
    tradeoff between the least squares solution and the minimization of x's
    L2-norm. The greater lambda, the smaller the norm of the given solution.
    We take a matrix A which is ill-conditionned.

    .. code-block:: python

        import numpy as np
        import linvpy as lp

        A = np.matrix([[7142.80730214, 6050.32000196],
                       [6734.4239248, 5703.48709251],
                       [4663.22591408, 3949.23319264]])

        y = [0.83175086, 0.60012918, 0.89405644]

        # Returns x_hat, the tradeoff solution of y = Ax
        print lp.tikhonov_regularization(A, y, 50)

        # [8.25871731e-05   4.39467106e-05]

    '''

    if lambda_parameter < 0:
        raise ValueError('lambda_parameter must be zero or positive.')
    if lambda_parameter == 0 :
        return np.linalg.lstsq(matrix_a, vector_y)[0].reshape(-1)

    # Ensures np.matrix type
    matrix_a = np.matrix(matrix_a)

    # Generates an identity matrix of the same shape as A'A.
    # matrix_a.shape() returns a tuple (#row,#columns) so with [1] with take the
    # number of columns to build the identity because A'A yields a square
    # matrix of the same size as the number of columns of A and rows of A'.
    identity_matrix = np.identity(matrix_a.shape[1])

    try:
        # x = (A' A + lambda^2 I)^-1 A' y
        vector_x = np.dot(
            np.dot(
                np.linalg.inv(
                    np.add(
                        np.dot(matrix_a.T, matrix_a), # A.T transpose of A
                        np.dot(math.pow(lambda_parameter,2), identity_matrix)
                    ),
                ),
                matrix_a.T
            ),
            vector_y
        )

        # Flattens result into an array
        vector_x = np.squeeze(np.asarray(vector_x))
        return vector_x

    # catches Singular matrix error (or any other error), prints the trace
    except :
        print("Lambda parameter may be too small.")
        raise

def rho_huber(input, clipping=1.345):
    '''
    The regular huber loss function; the "rho" version.

    :math:`\\rho(x)=\\begin{cases}
    \\frac{1}{2}{x^2}& \\text{if |x| <=} clipping, \\\\
    clipping (|x| - \\dfrac{1}{2} clipping)& \\text{otherwise}.
    \\end{cases}`

    This function is quadratic for small inputs, and linear for large
    inputs, with equal values and slopes of the different sections at the two
    points where |input|= clipping. The variable a often refers to the residuals,
    that is to the difference between the observed and predicted values
    a=y-f(x)

    :param input: (float) residual to be evaluated
    :param clipping: (optional)(float) clipping parameter

    :return float: penalty incurred by the estimation

    Example : run huber loss on a vector

    .. code-block:: python

        import linvpy as lp

        x = [1,2,3,4,5,6,7,8,9]

        loss = [lp.rho_huber(e, 4) for e in x]

        # [0.5, 2.0, 4.5, 8.0, 12, 16, 20, 24, 28]
    '''
    # Casting input to float to avoid divisions rounding
    input = float(input)

    if clipping <= 0 :
        raise ValueError('clipping must be positive.')

    if (np.absolute(input) <= clipping):
        return math.pow(input, 2)/2.0
    else :
        return clipping * (np.subtract(np.absolute(input),clipping/2.0))

def psi_huber(input, clipping=1.345):
    '''
    Derivative of the Huber loss function; the "psi" version. Used in the weight
    function of the M-estimator.

    :math:`\\psi(x)=\\begin{cases}
    x& \\text{if |x| <=} clipping, \\\\
    clipping * sign(x) & \\text{otherwise}.
    \\end{cases}`

    :param input: (float) residual to be evaluated
    :param clipping: (optional)(float) clipping parameter

    :return float: penalty incurred by the estimation

    Example : run huber loss derivative on a vector

    .. code-block:: python

        import linvpy as lp

        x = [1,2,3,4,5,6,7,8,9]

        derivative = [lp.psi_huber(e, 4) for e in x]

        # [1, 2, 3, 4, 4, 4, 4, 4, 4]

    '''
    # Casting input to float to avoid divisions rounding
    input = float(input)

    if clipping <= 0 :
        raise ValueError('clipping must be positive.')

    if (np.absolute(input) >= clipping):
        return clipping * np.sign(input)
    else :
        return input

def rho_bisquare(input, clipping=4.685):
    '''
    The regular bisquare loss (or Tukey's loss), "rho" version.

    :math:`\\rho(x)=\\begin{cases}
    (c^2 / 6)(1-(1-(x/c)^2)^3)& \\text{if |x|} \\leq 0, \\\\
    c^2 / 6& \\text{if |x| > 0}.
    \\end{cases}`

    :param input: (float) residual to be evaluated
    :param clipping: (optional)(float) clipping parameter

    :return float: result of bisquare function

    Example : run huber loss on a vector

    .. code-block:: python

        import linvpy as lp

        x = [1,2,3,4,5,6,7,8,9]

        result = [lp.rho_bisquare(e, 4) for e in x]

        # [0.46940104166666663, 1.5416666666666665, 2.443359375, 2.6666666666666665, 2.6666666666666665, 2.6666666666666665, 2.6666666666666665, 2.6666666666666665, 2.6666666666666665]
    '''
    # Casting input to float to avoid divisions rounding
    input = float(input)

    if clipping <= 0 :
        raise ValueError('clipping must be positive.')

    if (np.absolute(input) <= clipping):
        return (
            (clipping**2.0)/6.0)*(
                1-(
                    (1-(
                        input/clipping)**2)
                    **3)
                )
    else :
        return (clipping**2)/6.0

def psi_bisquare(input, clipping=4.685):
    '''
    The derivative of bisquare loss (or Tukey's loss), "psi" version.

    :math:`\\psi(x)=\\begin{cases}
    x((1-(x/c)^2)^2)& \\text{if |x|} \\leq 0, \\\\
    0& \\text{if |x| > 0}.
    \\end{cases}`

    :param input: (float) residual to be evaluated
    :param clipping: (optional)(float) clipping parameter

    :return float: result of bisquare function

    Example : run huber loss on a vector

    .. code-block:: python

        import linvpy as lp

        x = [1,2,3,4,5,6,7,8,9]

        result = [lp.psi_bisquare(e, 4) for e in x]

        # [0.87890625, 1.125, 0.57421875, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
    '''
    # Casting input to float to avoid divisions rounding
    input = float(input)

    if clipping <= 0 :
        raise ValueError('clipping must be positive.')

    if (np.absolute(input) <= clipping):
        return input*((1-(input/clipping)**2)**2)
    else :
        return 0.0

def rho_cauchy(input, clipping=2.3849):
    '''
    Cauchy loss function; the "rho" version.

    :math:`\\rho(x)=(c^2/2)log(1+(x/c)^2)`

    :param input: (float) residual to be evaluated
    :param clipping: (optional)(float) clipping parameter

    :return float: result of the cauchy function

    Example : run huber loss on a vector

    .. code-block:: python

        import linvpy as lp

        x = [1,2,3,4,5,6,7,8,9]

        result = [lp.rho_cauchy(e, 4) for e in x]

        # [0.4849969745314787, 1.7851484105136781, 3.5702968210273562, 5.545177444479562, 7.527866755716213, 9.42923997073317, 11.214388381246847, 12.875503299472802, 14.416978050108813]
    '''
  # Casting input to float to avoid divisions rounding
    input = float(input)

    if clipping <= 0 :
        raise ValueError('clipping must be positive.')

    return (
        (clipping**2)/2
        )*math.log(
        1+(
            input/clipping)**2)

def psi_cauchy(input, clipping=2.3849):
    '''
    Derivative of Cauchy loss function; the "psi" version.

    :math:`\\psi(x)=\\frac{x}{1+(x/c)^2}`

    :param input: (float) residual to be evaluated
    :param clipping: (optional)(float) clipping parameter

    :return float: result of the cauchy's derivative function

    Example : run huber loss on a vector

    .. code-block:: python

        import linvpy as lp

        x = [1,2,3,4,5,6,7,8,9]

        result = [lp.psi_cauchy(e, 4) for e in x]

        # [0.9411764705882353, 1.6, 1.92, 2.0, 1.951219512195122, 1.8461538461538463, 1.7230769230769232, 1.6, 1.4845360824742269]
    '''

    # Casting input to float to avoid divisions rounding
    input = float(input)

    if clipping <= 0 :
        raise ValueError('clipping must be positive.')

    return input/(
        1+(
            input/clipping
            )**2)

def rho_optimal(input, clipping=3.270):
    '''
    The so-called optimal 'rho' function is given by
    :math:`\\rho(x)=\\begin{cases}
    1.38(x/c)^2 & \\text{if |x/c|} \\leq 2/3, \\\\
    0.55 - 2.69(x/c)^2 + 10.76(x/c)^4 - 11.66(x/c)^6 + 4.04(x/c)^8 & \\text{if 2/3 }<|x/c| \\leq 1, \\\\
    1 &  \\text{if |x/c| > 1}.
    \\end{cases}`

    :param input: (float) residual to be evaluated
    :param clipping: (optional)(float) clipping parameter

    :return float: result of the optimal function
    '''

    # Casting input to float to avoid divisions rounding
    input = float(input)

    if clipping <= 0 :
        raise ValueError('clipping must be positive.')
    if abs(input/clipping) <= 2.0 / 3.0 :
        return 1.38 * (input/clipping)**2
    elif abs(input/clipping) <= 1.0 :
        return 0.55 - (2.69 * (input/clipping)**2) + (
            10.76 * (input/clipping)**4) - (
            11.66 * (input/clipping)**6) + (
            4.04 * (input/clipping)**8)
    elif abs(input/clipping) > 1 :
        return 1.0

def psi_optimal(input, clipping=3.270):
    '''
    The derivative of the optimal 'rho' function is given by
    :math:`\\rho(x)=\\begin{cases}
    2*1.38 x / c^2 & \\text{if |x/c|} \\leq 2/3, \\\\
    2*2.69x / c^2 + 4*10.76x^3 / c^4 - 6*11.66x^5/ c^6 + 8*4.04x^7 /c^8 & \\text{if 2/3 }<|x/c| \\leq 1, \\\\
    0 &  \\text{if |x/c| > 1}.
    \\end{cases}`

    :param input: (float) residual to be evaluated
    :param clipping: (optional)(float) clipping parameter

    :return float: result of the optimal function
    '''

    if clipping <= 0:
        raise ValueError('clipping must be positive.')

    if abs(input / clipping) <= 2.0 / 3.0:
        return 2 * 1.38 * (input / clipping ** 2)
    elif abs(input / clipping) <= 1.0:
        return (- 2 * 2.69 * (input / clipping ** 2)) + (
            4 * 10.76 * (input ** 3 / clipping ** 4)) - (
                   6 * 11.66 * (input ** 5 / clipping ** 6)) + (
                   8 * 4.04 * (input ** 7 / clipping ** 8))
    elif abs(input / clipping) > 1:
        return 0

def weights(input, loss_function, clipping=None, nmeasurements=None):
    '''
    Returns an array of :

    :math:`\\begin{cases}
    \\frac{loss\\_function(x_i)}{x_i}& \\text{if } x_i \\neq 0, \\\\
    0& \\text{otherwise}.
    \\end{cases}`

    Weights function designed to be used with loss functions like rho_huber,
    psi_huber, rho_cauchy... Note that the loss_function passed in argument must
    support two inputs.

    :param input: (array or float) vector or float to be processed, x_i's
    :param loss_function: (loss_function) f(x) in f(x)/x.
    :param clipping: (optional) clipping parameter of the huber loss function.

    :return array or float: element-wise result of f(x)/x if x!=0, 0 otherwise

    Example : run the weight function with the psi_huber with default
    clipping or with another function like rho_cauchy and another clipping.

    .. code-block:: python

        import linvpy as lp

        x = [1,2,3,4,5]

        # psi_huber, default clipping
        lp.weights(x, lp.psi_huber)

        # [1.0, 0.67249999999999999, 0.44833333333333331, 0.33624999999999999, 0.26900000000000002]

        # rho_cauchy, clipping=2.5
        lp.weights(x, lp.rho_cauchy, 2.5)

        # [0.46381251599460444, 0.7729628778689174, 0.9291646242761568, 0.9920004256749526, 1.0058986952713127]

    '''

    # kwargs = keyword arguments : if clipping is not specified, kwargs=None
    # and we use the default loss function's clipping, otherwise we use the one
    # passed in weights() with **kwargs
    kwargs = {}
    if clipping != None:
        kwargs['clipping'] = clipping

    if isinstance(input, (int, float)):
        if (input == 0) :
            return 0.0
        return loss_function(input, **kwargs)/float(input)

    # only used for the tau estimator
    elif (nmeasurements!=None) :
        z = util.scorefunction(input, 'tau', **kwargs)
        w = np.zeros(input.shape)

        # only for the non zero u elements
        i = np.nonzero(input)
        w[i] = z[i] / (2 * nmeasurements * input[i])
        return w

    else :
        # Ensures the input is an array and not a matrix.
        # Turns [[a b c]] into [a b c].

        input = np.squeeze(
                    np.asarray(
                        input
                        )
                    ).flatten()

        output = [0 if (i == 0) else 0.5*loss_function(i, **kwargs)/float(i) for i in input]
        return np.array(output)

# scale = sigma that divides; if sigma if given in parameter => preliminary scale
# lmb = lambda for tikhonov => if lambda is given : regularized m-estimator
# if lamb and scale are given : regularized m-estimator with preliminary scale
def irls(matrix_a, vector_y, loss_function, clipping=None, scale=None, lamb=0, initial_x=None, regularization=tikhonov_regularization, kind=None, b=0.5, tolerance=1e-5, max_iterations=100):
    '''
    The method of iteratively reweighted least squares (IRLS) is used to solve
    certain optimization problems with objective functions of the form:

    :math:`\underset{ \\boldsymbol x } {\operatorname{arg\,min}} \sum_{i=1}^n | y_i - loss\\_function_i (\\boldsymbol x)|^p`

    by an iterative method in which each step involves solving a weighted least
    squares problem of the form:

    :math:`\\boldsymbol x^{(t+1)} = \underset{\\boldsymbol x} {\operatorname{arg\,min}} \sum_{i=1}^n w_i (\\boldsymbol x^{(t)})\\big| y_i - loss\\_function_i (\\boldsymbol x) \\big|^2.`

    IRLS is used to find the maximum likelihood estimates of a generalized
    linear model, and in robust regression to find an M-estimator, as a way of
    mitigating the influence of outliers in an otherwise normally-distributed
    data set. For example, by minimizing the least absolute error rather than
    the least square error.

    :param matrix_a: (np.matrix) matrix A in y - Ax
    :param vector_y: (array) vector y in y - Ax
    :param loss_function: the loss function to be used in the M estimator
    :param clipping: clipping parameter for the loss function

    :return array: vector x solution of IRLS

    '''

    # If a scale parameter is given, m-estimator runs with preliminary scale.
    # This checks that scale is int or float and nonzero.
    # If no scale is given, scale = 1.0
    if (scale == None) or (scale == 0):
        scale = 1.0

    # kwargs = keyword arguments : if clipping is not specified, kwargs=None
    # and we use the default loss function's clipping, otherwise we use the one
    # passed in weights() with **kwargs
    # If the tau option is used, the function needs a clipping as tuple,
    # otherwise only one clipping is given.
    kwargs = {}
    if clipping != None :
        kwargs['clipping'] = clipping

    # if an initial value for x is specified, use it, otherwise generate a
    # vector of ones
    if initial_x != None:
        vector_x = initial_x
    else :
        # Generates a ones vector_x with length = matrix_a.columns
        vector_x = np.ones(matrix_a.shape[1])
        initial_x = np.ones(matrix_a.shape[1])

    # number of measurements and unknowns; by default None, if tau is used it
    # takes value m,n = matrix_a.shape
    m = None

    # Ensures numpy types
    matrix_a = np.matrix(matrix_a)
    vector_y = vector_y.reshape(-1,1)

    # Residuals = y - Ax, difference between measured values and model
    residuals = vector_y - np.dot(matrix_a, initial_x).reshape(-1,1)

    for i in range(1,max_iterations):

        # if we are computing the tau estimator, we need to upgrade the estimation of the scale in each iteration
        if kind == 'tau':

            m,n = matrix_a.shape

            residuals = np.asarray(residuals.reshape(-1)).flatten()

            # scale = scale * (mean(loss_function(residuals/scale))/b)^1/2
            if (scale != 0) :
                scale  *= np.sqrt(
                    np.mean(
                        array_loss(residuals / scale, loss_function, clipping[0])
                        ) / b
                    )

        # normalize residuals ((y - Ax)/ scale)
        if (scale != 0) :
            rhat = np.array(residuals / scale).flatten()
        else :
            rhat = np.array(residuals).flatten()

        # weights(y-Ax, loss_function, clipping)
        weights_vector = weights(rhat, loss_function, nmeasurements=m, **kwargs)

        # Makes a diagonal matrix with values of w(y-Ax)
        # np.squeeze(np.asarray()) is there to flatten the matrix into a vector
        weights_matrix = np.diag(
            np.squeeze(
                np.asarray(weights_vector)
                )
            )

        #print "weights matrix = ", weights_matrix

        # Square root of the weights matrix, sqwm = W^1/2
        #sqwm = np.sqrt(weights_vector.reshape(-1,1))

        sqwm = np.sqrt(weights_matrix)

        # A_weighted = W^1/2 A
        a_weighted = np.dot(sqwm,matrix_a)

        # y_weighted = diagonal of W^1/2 y
        #sqwm = sqwm.reshape(-1)

        y_weighted = np.dot(sqwm, vector_y)

        # vector_x_new is there to keep the previous value to compare
        vector_x_new = regularization(a_weighted, y_weighted, lamb)

        # Normalized distance between previous and current iteration
        xdis = np.linalg.norm(vector_x - vector_x_new)

        # New residuals
        residuals = vector_y.reshape(-1) - np.dot(matrix_a, vector_x_new).reshape(-1)

        # Divided by the specified optional scale, otherwise scale = 1
        #residuals = np.array(residuals / scale).flatten()

        vector_x = vector_x_new

        # if the difference between iteration n and iteration n+1 is smaller
        # than tolerance, return vector_x
        if (xdis < tolerance):
            return vector_x

    return vector_x

def basictau(a, y, loss_function, clipping, ninitialx, maxiter=100, nbest=1, initialx=None, b=0.5, regularization=tikhonov_regularization, lamb=0):
    '''
    This routine minimizes the objective function associated with the tau-estimator.
    This function is hard to minimize because it is non-convex. This means that it has several local minima. Depending on
    the initial x that we use for our minimization, we will end up in a different local minimum (for the m-estimator is
    not like this; the function in that case is convex and we always arrive to it, independently of the initial solution)

    In this algorithm we take the 'brute force' approach: let's try many different initial solutions, and let's pick the
    minimum with smallest value. The output of basictau are the best nbest minima (we will need them later)

    :param a: matrix A in y - Ax
    :param y: vector y in y - Ax
    :param loss_function: type of the rho function we are using
    :param clipping: clipping parameters. In this case we need two, because the rho function for the tau is composed two rho functions.
    :param ninitialx: how many different solutions do we want to use to find the global minimum (this function is not convex!)
                      if ninitialx=0, means the user introduced a predefined initial solution
    :param maxiter: maximum number of iteration for the irls algorithm
    :param nbest: we return the best nbest solutions. This will be necessary for the fast algorithm
    :param initialx: the user can define here the initial x he wants
    :param b: this is a parameter to estimate the scale

    :return xhat: contains the best nmin estimations of x
    :return mintauscale: value of the objective function when x = xhat
    '''

    # to store the minimum values of the objective function (in this case is
    # the scale)
    mintauscale = np.empty((nbest, 1))

    # initializing objective function with infinite. When we have a x that gives a smaller value for the obj. function,
    # we store the value of the objective function here
    mintauscale[:] = float("inf")

    # count how many initial solutions are we trying
    k = 0

    # store here the best xhat (nbest of them)
    xhat = np.zeros((a.shape[1], nbest))  # to store the best nmin minima

    # auxiliary variable to check if the user introduced a predefined initial solution.
    # = 0 if we do not have initial x. =1 if we have a given initial x
    givenx = 0

    if (initialx == None) :
        initialx = np.ones(a.shape[1])

    if ninitialx == 0:
        # we have a predefined initial x
        ninitialx = initialx.shape[1]
        # set givenx to 1
        givenx = 1

    while k < ninitialx:
        # if still we did not reach the number of initial solutions that we want to try,
        # get a new initial solution initx (randomly)
        initx = util.getinitialsolution(y.reshape(-1, 1), a)

        if givenx == 1:
            # if we have a given initial solution initx, we take it
            initx = np.expand_dims(initialx[:, k], axis=1)

        # compute the residual y - Ainitx
        initialres = y.reshape(-1, 1) - np.dot(a, initx)

        # estimate the scale using initialres
        initials = np.median(np.abs(initialres)) / .6745

        # solve irls using y, a, the tau weights, initx and initals. We get an
        # estimation of x, xhattmp
        xhattmp = irls(
            a,
            y,
            loss_function,
            clipping,
            scale=initials,
            initial_x=initx,
            kind='tau',
            b=0.5,
            max_iterations=maxiter,
            regularization=regularization,
            lamb=lamb)

        # last working version
        # xhattmp, scaletmp, ni, w, steps = inv.irls(y, a, 'tau', 'optimal', 'none', 0, initx, initials, clipping, maxiter)

        # compute the value of the objective function using xhattmp
        # we compute the res first
        res = y.reshape(-1, 1) - np.dot(a, xhattmp).reshape(-1, 1)

        # Value of the objective function using xhattmp
        #tscalesquare = util.tauscale(res, lossfunction, clipping, b)

        tscalesquare = util.tauscale(res, 'optimal', clipping[0], b)

        # update counter
        k += 1

        # we checks if the objective function has a smaller value that then
        # ones we found before
        if tscalesquare < np.amax(mintauscale):
            # it is smaller, so we keep it!
            # store value for the objective function
            mintauscale[np.argmax(mintauscale)] = tscalesquare

            # store value of xhat
            xhat[:, np.argmax(mintauscale)] = np.squeeze(xhattmp)

    # we return the best solutions we found, with the value of the objective
    # function associated with the xhats
    return xhat, mintauscale

# TODO : need doc here
def fasttau(y, a, loss_function, clipping, ninitialx, nmin=5, initialiter=5):
  xhat, mintauscale = basictau(a=a, y=y, loss_function=loss_function, clipping=clipping, ninitialx=ninitialx, maxiter=initialiter, nbest=nmin)  # first round: only initialiter iterations. We keep the nmin best solutions

  xfinal, tauscalefinal = basictau(a=a, y=y, loss_function=loss_function, clipping=clipping, ninitialx=0, maxiter=100, nbest=1, initialx=xhat)  # iterate the best solutions
  # until convergence

  return xfinal, tauscalefinal

# function to apply a loss function to any data structure; scalar, array or matrix
# returns the exact same data structure with the loss function applied element-wise
def array_loss(values, loss_function, clipping=None):

    # if we input incorrect values (due to division by too tiny numbers in irls)
    # inside array_loss, it returns zeroes

    # checks if there is a 'nan' value in the input
    if np.any(np.isnan(values)) :
        print "incorrect input in array loss !"
        return np.zeros(values.shape)

    kwargs = {}
    if clipping != None:
        kwargs['clipping'] = clipping

    vfunc = np.vectorize(loss_function)
    return vfunc(values, **kwargs)

def tau_weights_new(input, clipping, loss_function=rho_optimal):

    weights = 2.0 * array_loss(input, loss_function, clipping[1])
    weights -= (util.scoreoptimal(input, clipping[1]) * input)
    scaling = np.sum(util.scoreoptimal(input, clipping[0]) * input)

    if (scaling == 0):
        scaling = 1.0

    return weights / scaling

#===============================================================================
#===================================TESTING AREA================================
#===============================================================================


import numpy as np

def mape(y_true, y_pred, version=0) -> float:
    r"""
    .. math::
        mape = mean(|Y - Y_hat| / |Y|))

    Args:
        y_true (np.array): Ground truths
        y_pred (np.array): Forecasts
        version (int, optional): Version 0 is as-is implementation of formula. Version 1 & 2 deal with div-by-0, but
            version=1 ignores those with y=0 from calculation. Defaults to 0.

    Returns:
        array of floats: mape values
    """

    if version == 0:
        return np.mean(np.abs((y_true - y_pred) / y_true))
    elif version == 1:
        # This version takes care of div-by-0, and ignore 0-y in nominator.
        # See: https://github.com/awslabs/gluon-ts/pull/725
        denominator = np.abs(y_true)
        flag = denominator == 0
        return np.mean((np.abs(y_true - y_pred) * (1 - flag)) / (denominator + flag))
    elif version == 2:
        # This version takes care of div-by-0, and include 0-y in nominator.
        denominator = np.abs(y_true)
        flag = denominator == 0
        return np.mean(np.abs(y_true - y_pred) / (denominator + flag))

    raise ValueError(f"Unknown mape version: {version}")

def wmape(actual, forecast, version=0) -> float:
    r"""
    .. math::
        wmape = mape * (actual / sum(actual))

    This implementation assumes actual are positives -- FIXME: would it be
    better to enforce this assumption by using sum(abs(actual))?

    Args:
        actual (np.array): Ground truths
        forecast (np.array): Forecasts
        version (int, optional): mape version to use. See mape(). Defaults to 0 (which
            may perform division-by-zero).

    Returns:
        array of floats: wmape values
    """
    # we take two series and calculate an output a wmape from it.
    # - NOTE: as-is implementation from Logbooks/Med_Low/Weekly_AutoArima.ipynb

    # make a series called mape
    se_mape = mape(actual, forecast, version=version)

    # get a float of the sum of the actual
    # - NOTE: this as-is implementation assumes actual are positives. Would it
    # be better to enforce this assumption by using np.sum(np.abs(actual))?
    ft_actual_sum = actual.sum()

    # get a series of the multiple of the actual & the mape
    se_actual_prod_mape = actual * se_mape

    # summate the prod of the actual and the mape
    ft_actual_prod_mape_sum = se_actual_prod_mape.sum()

    # float: wmape of forecast
    ft_wmape_forecast = ft_actual_prod_mape_sum / ft_actual_sum

    # return a float
    return ft_wmape_forecast


import numpy as np

# TRANSPOSE OF MATRIX --> convert from rows to columns and columns to rows

matrix = np.random.randint(10,100,(4,2))
""" array([[19, 11],
       [25, 83],
       [30, 63],
       [51, 76]])
 """

transpose_matrix = matrix.T
"""
array([[19, 25, 30, 51],
       [11, 83, 63, 76]]) """

# DETERMINANT OF MATRIX --> volume of the matrix

matrix = np.random.randint(10,100,(4,4))
"""
array([[59, 68, 57, 92],
       [48, 50, 36, 63],
       [30, 52, 45, 32],
       [62, 66, 97, 65]])
       """

from numpy.linalg import det

det_matrix = det(matrix)
# -912991.9999999997

# RANK OF THE MATRIX --> estimate of linearly independent rows or columns

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

np.linalg.matrix_rank(vector)
# 1

matrix = np.random.randint(10,100,(4,4))
np.linalg.matrix_rank(matrix)
# 4


from sympy import *
from sympy.solvers import solve
from sympy import Symbol
import matplotlib.pyplot as plt
import numpy as np

x=Symbol('x')
a1=Symbol('a1')
a2=Symbol('a2')
a3=Symbol('a3')
a4=Symbol('a4')
a5=Symbol('a5')
eqn_gal2 =[]
eqn_gal3 =[]
eqn_gal4 =[]

u2 = a1 + a2*x + a3*x*x
z2=u2.subs(a1,solve(u2.subs(x,1)-2 , a1)[0])
z2=z2.subs(a2,solve((x*diff(u2,x)+0.5).subs(x,2),a2)[0])
# finding residual
r_gal2=diff(x*(diff(z2,x)),x)-(2/(x*x))
eqn_gal2.append(integrate(r_gal2*x*x, (x, 1, 2)))
z_gal2 = solve(eqn_gal2, a3)
print("Equation for Quadratic",eqn_gal2)
print(z_gal2)

u3 = a1 + a2*x + a3*x*x + a4*x*x*x
z3=u3.subs(a1,solve(u3.subs(x,1)-2 , a1)[0])
z3=z3.subs(a2,solve((x*diff(u3,x)+0.5).subs(x,2),a2)[0])
# finding residual
r_gal3=diff(x*(diff(z3,x)),x)-(2/(x*x))
# galerkian integral wrt weighting functions
eqn_gal3.append(integrate(r_gal3*x*x, (x, 1, 2)))
eqn_gal3.append(integrate(r_gal3*x*x*x, (x, 1, 2)))
z_gal3 = solve(eqn_gal3, [a3,a4])
print("Equation for Qubic",eqn_gal3)
print(z_gal3)

u4 = a1 + a2*x + a3*x*x + a4*x*x*x + a5*x*x*x*x
z4=u4.subs(a1,solve(u4.subs(x,1)-2 , a1)[0])
z4=z4.subs(a2,solve((x*diff(u4,x)+0.5).subs(x,2),a2)[0])
# finding residual
r_gal4=diff(x*(diff(z4,x)),x)-(2/(x*x))
# galerkian integral wrt weighting functions
eqn_gal4.append(integrate(r_gal4*x*x, (x, 1, 2)))
eqn_gal4.append(integrate(r_gal4*x*x*x, (x, 1, 2)))
eqn_gal4.append(integrate(r_gal4*x*x*x*x, (x, 1, 2)))
z_gal4 = solve(eqn_gal4, [a3,a4,a5])
print("Equation for Biquadratic",eqn_gal4)
print(z_gal4)

# plotting the curve
u_gal2=[]
u_gal3=[]
u_gal4=[]
u_exact=[]
y = []
i=1
while i<=2:
    y.append(i)
    u_gal2.append((z2.subs(a3,z_gal2.get(a3))).subs(x,i))
    u_gal3.append(((z3.subs(a3,z_gal3.get(a3))).subs(a4,z_gal3.get(a4))).subs(x,i))
    u_gal4.append((((z4.subs(a3,z_gal4.get(a3))).subs(a4,z_gal4.get(a4))).subs(a5,z_gal4.get(a5))).subs(x,i))
    u_exact.append((2/i)+0.5*np.log(i))
    i+=0.1

plt.plot(y, u_gal2, label="Quadratic")
plt.plot(y, u_gal3, label="Qubic")
plt.plot(y, u_gal4, label="Biquadratic")
plt.plot(y, u_exact, label="Exact")
plt.legend()
plt.xlabel('x', fontsize=16)
plt.ylabel('u', fontsize=16)
plt.suptitle('Solutions with different Trial functions')
print("It can be observed from the graph that Biquadratic trial function \n has the least error and almost coincides with the exact soluton")
plt.show()


"""
Check out https://www.youtube.com/watch?v=gMlf1ELvRzc ; Veritasium is a good addiction to have.
We are using an area sampling method. Instead of doing a perimeter method like Archimedes's method of polygon ratios,
we are just taking ratios of areas in this code. We can do that only using a computer using randomness.
A similar method might be using integration to find out the area of the circle and the area of the square.

- Side note, Issac Newton as a motherfucking genius. It took me 28 years to appreciate his contributions to the world.

"""
import numpy as np

# Setting the seed for reproducibility.
SEED = 1

def sample_a_point(random_generator):
    """Sample a 2D point within a square of length 1.

    Returns:
        point: Dict of form {'x': x-coordinate, 'y': y-coordinate}

    """
    # Uniform distribution is enough.
    point = dict(x=random_generator.rand(1)[0], y=random_generator.rand(1)[0])
    return point

def check_within_a_unit_circle(point):
    radius = 1.0 # Radius of a unit circle
    cartesian_distance = np.sqrt(point['x']**2 + point['y']**2)
    return cartesian_distance <= radius

def sample_and_estimate_pi(num_iterations=10000, seed=SEED):
    """Find pi by the ratio of points within a unit circle and a square tangential and enclosing it.

    - To enclose a unit circle, the square's length is 2*r = 2, hence the area being 4.0
    - Area of the unit circle is pi * r**2 = pi
    - Hence ratio of areas = pi / 4.0
    - pi = 4.0 * ratio of areas
    - Ratio of areas = 4.0 * (points in circle) / (points in square)

    """
    rng = _get_random_generator(seed)
    estimated_pi = 0
    diff_from_prev_iteration = np.inf

    total_circle_canditates = 0

    for total_square_candidates in range(num_iterations):
        current_point = sample_a_point(random_generator=rng)
        if(check_within_a_unit_circle(current_point)):
            total_circle_canditates += 1

        if total_square_candidates == 0:
            new_estimated_pi = 1.0
        else:
            new_estimated_pi = 4.0 * float(total_circle_canditates) / (total_square_candidates)

        diff_from_prev_iteration = np.abs(new_estimated_pi - estimated_pi)
        estimated_pi = new_estimated_pi

    return estimated_pi

def _get_random_generator(seed):
    return np.random.RandomState(seed=seed)

def integration_method(num_of_rectangles):
    """Following the method here: https://www.youtube.com/watch?v=uK2OQMUAUDQ

    Basically `num_of_rectangles` is the number of rectangles representing a quarter circle.

    """
    estimated_area_of_quarter = 0.
    width = 1. / num_of_rectangles
    for rectangle_num in range(1 , num_of_rectangles):
        x_k = (rectangle_num  - 1) * width  # first x starts from 0, hence the -1
        # Applying the circle equation to find "y"
        y_k = np.sqrt(1 - x_k**2)

        # y_k is the height of the rectangle, i.e. length
        current_area = y_k * width
        estimated_area_of_quarter += current_area

    # pi * r^2 / 4 = total_area_of_of_quarter; r=1 in this case.
    pi = estimated_area_of_quarter * 4
    return pi

if __name__ == "__main__":
    apple_pie = sample_and_estimate_pi(num_iterations=100000)
    print(f"Estimated pi using random area sampling as {apple_pie}")
    assert np.isclose(apple_pie, np.pi, rtol=1e-3)

    pecan_pie = integration_method(10000)
    print(f"Estimated pi using summation as {pecan_pie}")
    assert np.isclose(pecan_pie, np.pi, rtol=1e-3)


'''
numpy 패키지를 사용한 벡터 연산
'''
import numpy as np
print('numpy version:',np.__version__)
import math

# 파이썬 list 데이터 타입의 연산
v =[1,2] # type : class list
print(type(v))
print('v=',v)

w =[2,3]
print('w=',w)

print(v + w)
# list는 + 연산을 사용할 수 있음
# + 연산자는 extend 함수와 비슷한 기능
# + 연산자는 v나 w를 변경하지 않고, 새로운 리스트를 리턴
# v.extend(w) 함수는 v를 변경함
# print(v- w) # list는 - 연산을 사용할 수 없음!
v.extend(w)
print(v)

# numpy 패키지의 ndarray 타입을 사용
# n-dimensional array(n차원 배열)
v = np.array([1,2])
print('type :', type(v))
print(v)
print('dimension:', v.ndim)
print('shape:', v.shape) # 1차원 배열의 경우의 원소의 갯수를 나타냄

v = np.array([
    [1,2],
    [2,3],
    [3,4]
])
print('type:', type(v))
print('dimension:',v.ndim)
print('shape:', v.shape) # 2차원 에서 부터는 행의 갯수가 x위치 컬럼의 갯수가 y위치로 출력됨.
print(v)

#ndarray 타입을 사용한 벡터 연산
v = np.array([1,2,3])
w = np.array([3,4,5])
vector_add = v + w
print('vector add =', vector_add)

vector_sub = v - w
print('vector subtract =', vector_sub)

vectors = np.array([
    [1,2],
    [3,4],
    [5,6]
])

np_sum = np.sum(vectors) # 2차원 배열의 모든 원소들의 합
print('np_sum =',np_sum)

#axis=0: 2차원 배열에서 각 컬럼들의 합으로 이루어진 배열
np_sum_by_col = np.sum(vectors,axis=0)
print('np_sum_by_col =', np_sum_by_col)

#axis=1: 2차원 배열에서 각 행들의 합으로 이루어진 배열
np_sum_by_row = np.sum(vectors, axis=1)
print('np_sum_by_row =', np_sum_by_row)

np_mean = np.mean(vectors)
print('np_mean =',np_mean)

np_mean_by_col = np.mean(vectors, axis=0)
print('np_mean_by_col =',np_mean_by_col)

np_mean_by_row = np.mean(vectors, axis=1)
print('np_mean_by_row =',np_mean_by_row)

v = np.array([1,2,3])
scalar_mul = v * 3
print('scalar multiplication =', scalar_mul)
scalar_div = v / 3
print('scalar division =', scalar_div)

v = np.array([1,2])
w = np.array([3,4])
print('dot =', v.dot(w))

#numpy를 사용한 벡터의 크기
def norm(v):
    return math.sqrt(v.dot(v))

v = np.array([1,1])
print('norm =', norm(v))

# numpy를 사용한 두 벡터의 거리
def dist(v,w):
    return norm(v - w)


import scipy.optimize as optimize
import numpy as np

# **************** problem #1 ******************
def func(x):
    return np.cos(x)**2 + 6 - x

def f(x):
    return np.power(x, 2) + 6.0 * x + 4

def f_prime(x):
    return 2.0 * x + 6.0

# 0<=cos(x)**2<=1, so the root has to be between x=6 and x=7
print(optimize.bisect(func, 6, 7))
6.77609231632

a = optimize.bisect(f, -1, 0)

print("This is the bisection method:{:.16e}".format(a))
# -7.6393202249983005e-01
print("This is the bisection method:{0}".format(a))
# -0.7639320225

# ********************* problem #2 ******************

def g(x):
    return (-4.0) / x + 6.0

def h(x):
    return -1.0 * np.sqrt(-6.0 * x - 4.0)

# This finds the value of x such that func(x) = x, that is, where
# -x**3 + 1 = x
print(optimize.fixed_point(g, -5.2))
# 5.2360679775

print(optimize.fixed_point(h, -0.77))

# ***************** problem #4 ********************
# use the secant method.
# well scipy.optimize.newton() uses the secant method if we don't give it the functions derivative.

a = 0

a = optimize.newton(f, 0, tol=1e-16, maxiter=8)

def convergence(n_plus_one, n):  # using equation (1) from the assignment. where -7.6 is the fixed point "s"
    numerator = np.log(np.abs(n_plus_one - -7.6393202250021031e-01))
    denominator = np.log(np.abs(n - -7.6393202250021031e-01))
    return numerator / denominator

print("This is the convergence:", convergence(-0.74996874883, -0.66665555574))
print("This is the convergence:", convergence(-0.763636264454, -0.74996874883))
print("This is the convergence:", convergence(-0.763931103843, -0.763636264454))

print("This is secant method answer {:.16e}".format(a))

# **************** problem #5 ***********************

# newton() will use the newton raphson method because we gave it the derivative.
a = optimize.newton(f, 0, fprime=f_prime, tol=1e-16, maxiter=14)

print"This is Newton method answer{:.16e}".format(a)

print("This is the convergence:", convergence(-0.761904761905, -0.666666666667))
print("This is the convergence:", convergence(-0.763931104357, -0.761904761905))
print("This is the convergence:", convergence(-0.7639320225, -0.763931104357))

# ************* problem #6 *********************

def k(x):
    return np.power(x, 20) - 10.0

def k_prime(x):
    return 20.0 * np.power(x, 19)

a = optimize.newton(k, 8, fprime=k_prime, tol=1e-16, maxiter=44)

print"This is Newton method answer: {:.16e}".format(a)

# **************** problem #7 ************

def f(x):
    return np.power(x,2) - 6.0*x + 9.0

def f_prime(x):
    return 2.0 * x - 6.0

a = optimize.newton(f, 4, fprime=f_prime, tol=1e-16, maxiter=29)

print"This is Newton method answer: {:.16e}".format(a)

def convergence(n_plus_one, n):  # using equation (1) from the assignment. where -7.6 is the fixed point "s"
    numerator = np.log(np.abs(n_plus_one - 3.0000000298023224e+00))
    denominator = np.log(np.abs(n - 3.0000000298023224e+00))
    return numerator / denominator

print("This is the convergence:", convergence(3.25, 3.5))
print("This is the convergence:", convergence(3.125, 3.25))
print("This is the convergence:", convergence(3.0625, 3.125))

print("This is the convergence:", convergence(3.00000011921, 3.00000023842 ))


#From http://scipy.github.io/old-wiki/pages/Cookbook/LoktaVolterraTutorial
import numpy as np
import matplotlib.pyplot as plt
# Definition of parameters
a = 1.
b = 0.1
c = 1.5
d = 0.75
def dX_dt(X, t=0):
    """ Return the growth rate of fox and rabbit populations.
        It appears that it needs to take a numpy array in, just one,
        and returns the same. """
    return np.array([ a*X[0] -   b*X[0]*X[1] ,
                  -c*X[1] + d*b*X[0]*X[1] ])
#Population equilibrium points when dX_dt = 0.
X_f0 = np.array([     0. ,  0.])
X_f1 = np.array([ c/(d*b), a/b])

all(dX_dt(X_f0) == np.zeros(2) ) and all(dX_dt(X_f1) == np.zeros(2)) # => True

#Create the Jacobian for the purpose of evaluating eigenvalues etc.
def d2X_dt2(X, t=0):
    """ Return the Jacobian matrix evaluated in X. """
    return np.array([[a -b*X[1],   -b*X[0]     ],
                  [b*d*X[1] ,   -c +b*d*X[0]] ])
#So near X_f0, which represents the extinction of both species, we have:
A_f0 = d2X_dt2(X_f0)        # >>> array([[ 1. , -0. ],
                                        #            [ 0. , -1.5]])

#Near X_f0, the number of rabbits increase and the population of foxes decrease.
#The origin is therefore a saddle point.
#Near X_f1, we have:
A_f1 = d2X_dt2(X_f1)                    # >>> array([[ 0.  , -2.  ],
                                        #            [ 0.75,  0.  ]])
# whose eigenvalues are +/- sqrt(c*a).j:
lambda1, lambda2 = np.linalg.eigvals(A_f1) # >>> (1.22474j, -1.22474j)
# They are imaginary numbers. The fox and rabbit populations are periodic as follows from further
# analysis. Their period is given by:
T_f1 = 2*np.pi/abs(lambda1)                # >>> 5.130199

#Integrate the functions in terms of a starting point.
from scipy import integrate
t = np.linspace(0, 15,  1000)              # time
X0 = np.array([10, 5])                     # initials conditions: 10 rabbits and 5 foxes
X, infodict = integrate.odeint(dX_dt, X0, t, full_output=True)
infodict['message']                     # >>> 'Integration successful.'
#Plot the evolution of both populations.
rabbits, foxes = X.T
fig, (ax0,ax1) = plt.subplots(2,1,figsize=(8,8))
ax0.plot(t, rabbits, 'r-', label='Rabbits')
ax0.plot(t, foxes  , 'b-', label='Foxes')
ax0.grid()
ax0.legend(loc='best')
ax0.set_xlabel('time')
ax0.set_ylabel('population')
ax0.set_title('Evolution of fox and rabbit populations')
#f1.savefig('rabbits_and_foxes_1.png')
#Plot the phase space.
values  = np.linspace(0.3, 0.9, 5)                          # position of X0 between X_f0 and X_f1
vcolors = plt.cm.autumn_r(np.linspace(0.3, 1., len(values)))  # colors for each trajectory
#-------------------------------------------------------
# plot trajectories
for v, col in zip(values, vcolors):
    X0 = v * X_f1                               # starting point
    X = integrate.odeint( dX_dt, X0, t)         # we don't need infodict here
    ax1.plot( X[:,0], X[:,1], lw=3.5*v, color=col, label='X0=(%.f, %.f)' % ( X0[0], X0[1]) )

#-------------------------------------------------------
# define a grid and compute direction at each point
ymax = ax1.set_ylim(ymin=0)[1]                        # get axis limits
xmax = ax1.set_xlim(xmin=0)[1]
nb_points   = 20

x = np.linspace(0, xmax, nb_points)
y = np.linspace(0, ymax, nb_points)

X1 , Y1  = np.meshgrid(x, y)                       # create a grid
DX1, DY1 = dX_dt([X1, Y1])                      # compute growth rate on the gridt
M = (np.hypot(DX1, DY1))                           # Norm of the growth rate
M[ M == 0] = 1.                                 # Avoid zero division errors
DX1 /= M                                        # Normalize each arrows
DY1 /= M

#-------------------------------------------------------
# Drow direction fields, using matplotlib 's quiver function
# I choose to plot normalized arrows and to use colors to give information on
# the growth speed
ax1.set_title('Trajectories and direction fields')
Q = ax1.quiver(X1, Y1, DX1, DY1, M, pivot='mid', cmap=plt.cm.jet)
ax1.set_xlabel('Number of rabbits')
ax1.set_ylabel('Number of foxes')
ax1.legend()
ax1.grid()
ax1.set_xlim(0, xmax)
ax1.set_ylim(0, ymax)
plt.show()
#f2.savefig('rabbits_and_foxes_2.png')


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

def newtonMethod(func, derFunc, tol, guess,root):
    cur = guess
    if derFunc(root) == 0:
        return None
    else:
        cur = guess
        while abs(cur - root) > tol:
            try:                                        #Check if the derivate function at x_n is 0
                cur = cur - (func(cur)/derFunc(cur))
            except:
                return None
    if cur != cur:                                        #Check cur ends up at nan
        return None
    return cur

def hwHelper(showVisuals):
    func = lambda x: 1/(np.exp(x)+1) - 1/2                  #Function Set Up
    dFunc = lambda x: -np.exp(x)/math.pow(np.exp(x)+1,2)    #First Derivative
    tol = 10**(-9)                                          #Tolerance
    root = 0                                                #Root of Funciton

    start = -5.0                                            #Variables for the interval [-5,5]
    end = 5.0
    points = 50000
    potentialGuess = np.linspace(start,end,num=points)      #Created 5000 points between -5 and 5

    valids = []
    for x in potentialGuess:                                #Iterating through each point in potentialGuess and saving all the points that coverged into an array
        if newtonMethod(func,dFunc,tol,x,root) is not None:
            valids.append((x, newtonMethod(func,dFunc,tol,x,root)))

    # Visuals - Printing out all the results
    if showVisuals:
        print("\n\n\n\n\n")

        print("Grid: \n Start: {}\n End: {} \n # of Points: {}\n".format(start,end,points))

        print("(Approx.) Lowest intial Guess that's valid is {}:\n With a Final Guess of: {}\n".format(valids[0][0],valids[0][1]))
        print("(Approx.) Highest intial Guess that's valid is {}:\n With a Final Guess of: {}\n".format(valids[len(valids)-1][0],valids[len(valids)-1][1]))
    return (valids[0][0],valids[len(valids)-1][0])

def extraCreditOne():
    orignalFunc = lambda x: 1/(np.exp(x)+1) - 1/2
    intevals = hwHelper(True)
    x = np.linspace(intevals[0],intevals[1],num=1000)
    y = orignalFunc(x)

    xw = np.arange(-5,6)
    yw = orignalFunc(xw)
    line2, = plt.plot(xw,yw,color = "blue",label="Original Function")
    line1, = plt.plot(x,y,color = "red",label="Original Function -  With Intervals")
    plt.xlabel("X-Values")
    plt.ylabel("Y-Values (Function Output)")
    plt.title("Function: \"1/(e^x + 1) - 0.5\" for the intervals \nwhere Newton's Method Converges")
    plt.legend(handles=[line1,line2])
    plt.show()

# hwHelper(True)
extraCreditOne()


from scipy.stats import norm
import matplotlib.pyplot as plt
import numpy as np

# I'm going to define my normal PDF...
def normal_pdf(x, mean, std):
    return 1/(np.sqrt(2*np.pi*std**2))*np.exp(-0.5*((x - mean)**2)/(std**2))

x = np.arange(-12, 12, .001)

pdf1 = normal_pdf(x, mean=0, std=2)

#the built-in norm PDF in scipy.stats
pdf2 = norm.pdf(x, loc=0, scale=2)

fig = plt.figure(figsize=(7,9))
ax1 = fig.add_subplot(3, 1, 1)
ax1.plot(x,pdf1, color='#84b4e8', linestyle="-", linewidth=6, label="My normal PDF")
ax1.plot(x,pdf2, color='#ff464a', linestyle="--", label="norm.pdf() in scipy.stats ")
ax1.set_xlabel("x")
ax1.set_ylabel("PDF(x)")
ax1.legend(title = r"Normal PDF with $\mu$=0 and 1$\sigma$=2")

ax2 = fig.add_subplot(3, 1, 2)
for i in [1, 2, 3]:
     y = normal_pdf(x,0,i)
     ax2.plot(x, y, label=r"$\mu$ = 0, 1$\sigma$ = " + str(i))
ax2.set_xlabel("x")
ax2.set_ylabel("PDF(x)")
ax2.legend()

ax3 = fig.add_subplot(3, 1, 3)
for i in [-3, 0, 3]:
     y = normal_pdf(x, i, 1)
     ax3.plot(x, y, label=r"$\mu$ = " + str(i) + ", 1$\sigma$ = 1")
ax3.set_xlabel("x")
ax3.set_ylabel("PDF(x)")
ax3.legend()

fig.tight_layout()


from __future__ import division
import numpy as np
import pylab as pl

def S(f,a,b):
    # Implement's the Simpson's method
    h = abs(b-a)
    c = (a+b)/2.0
    return (h/6.0) * (f(a) + 4*f(c) + f(b))

def adaptive_simpson(f,a,b,e,level_max):
    retVal = None
    c = (a+b)/2.0
    s1 = S(f,a,b)
    s2 = S(f,a,c) + S(f,c,b)
    err = (1/15.0)*(abs(s2 - s1))
    print "c={}".format(c)
    inpts.append(a)
    inpts.append(b)
    inpts.append(c)
    if level_max == 0 or err < e :
        # base case
        # print " a={}, b={}, c={}, s1={}, s2={}, level={}".format(a,b,c,s1,s2,level_max)
        retVal = s2
    elif  err > e:
        # sub divide into left and right
        left = adaptive_simpson(f,a,c,e/2.0,level_max -1)
        right = adaptive_simpson(f,c,b,e/2.0,level_max -1)
        retVal = left + right
    return retVal

if __name__ == '__main__':
    # call adaptive simpson's for
    e = 0.5 * 10**(-4)
    level_max = 30
    # function 1
    f = lambda x: 4.0/(1 + x**2)
    a = 0
    b = 1
    inpts = [] # intervals
    result = adaptive_simpson(f,a,b,e,level_max)
    print("Numerical Integral by adaptive simpson's method: {}".format(result))

    xmin,xmax = int(a-2),int(b+2)+1 # for plots
    ymin,ymax = -5,5 # for plots
    x = np.linspace(xmin,xmax,1000)#floor and ceil used incase a and b are not ints
    x_interest = np.linspace(a,b,1000)
    pl.plot(x,f(x))
    pl.plot(x_interest, f(x_interest),color='red')
    # plotting axes
    pl.plot(range(xmin,xmax+1), 0*np.arange(xmin,xmax+1), color='black')
    pl.plot(0*np.arange(ymin,ymax+1), range(ymin,ymax+1), color='black')
    pl.scatter(inpts, [0 for _ in inpts], color="green")
    pl.plot()
    pl.xlabel("x")
    pl.ylabel("f(x)")
    pl.xlim(xmin,xmax)
    pl.ylim(ymin,ymax)
    pl.grid()
    pl.savefig("ex5_fig1.pdf")
    pl.show()

    # function 2
    f = lambda x: np.cos(2.0*x)/(np.e ** x)
    a = 0
    b = 2*np.pi
    inpts = [] # intervals
    result = adaptive_simpson(f,a,b,e,level_max)
    print("Numerical Integral by adaptive simpson's method: {}".format(result))

    xmin,xmax = int(a-2),int(b+2)+1 # for plots
    ymin,ymax = -0.3,1.3 # for plots
    x = np.linspace(xmin,xmax,1000)#floor and ceil used incase a and b are not ints
    x_interest = np.linspace(a,b,1000)
    pl.xlabel("x")
    pl.xlim(xmin,xmax)
    pl.ylim(ymin,ymax)
    pl.grid()
    pl.ylabel("f(x)")
    pl.plot(x,f(x))
    pl.plot(x_interest, f(x_interest),color='red')
    # plotting axes
    pl.plot(range(xmin,xmax+1), 0*np.arange(xmin,xmax+1), color='black')
    pl.plot(0*np.arange(int(ymin)-1,int(ymax)+1), range(int(ymin)-1,int(ymax)+1), color='black')
    pl.scatter(inpts, [0 for _ in inpts], color="green")
    pl.savefig("ex5_fig2.pdf")
    pl.show()


import numpy as np
from matplotlib import pyplot as plt

# A function to compute the first n terms of the given sequence
def computeSequence(n):
	diff_seq = np.zeros(n)

	# Saving the first two terms of the difference equation
	diff_seq[0] = 1.0/3.0
	diff_seq[1] = 1.0/12.0

	# Getting the remaining terms
	for i in range(2,n):
		diff_seq[i] = 2.25 * diff_seq[i-1] - 0.5 * diff_seq[i-2]
	return diff_seq

# A function to generate the graph for the same
def get_plot(n):

	# Get the sequence corresponding to the difference equation
	diff_seq = computeSequence(n+1)

	# Get the sequence corresponding to the recurrence relation solution
	rec_seq = np.array([(pow(4.0,-1.0 * k)/3.0) for k in range(n+1)])

	# Get the sequence for the values of k
	k_seq = np.array([k for k in range(n+1)])

	# Generating the plots
	plt.plot(k_seq,rec_seq,'r',k_seq,diff_seq,'b')
	plt.xlabel('The values of K')
	plt.ylabel('The solution of the difference equation')
	plt.legend(['Exact solution','Sequence computed'])
	plt.yscale('log')
	plt.title('Semi-log plot of difference equation values v/s Point indices K')
	plt.savefig('q6_plot.png')

# Calling the function as asked in the assignment statement
if __name__ == '__main__':
	get_plot(80)


# -*- coding: utf-8 -*-
"""
@author: vladimirnesterov
Ten Little Algorithms by Jason Sachs from here https://www.embeddedrelated.com/showarticle/760.php
"""

def euclidean_gcd(a,b):
    """Euclidean Algorithm to find greatest common divisor.

    Euclidean algorithm is an efficient method for computing
    the greatest common divisor (GCD) of two integers (numbers),
    the largest number that divides them both without a remainder [Wiki].

    Args:
        a (int): The first integer, > 0,
        b (int): The second integer, > 0.

    Returns:
        int: the greatest common divisor.

    """
    if a < b:
        a,b = b,a

    while a > b:
        a = a - b

    if (a != b):
        #print("a =", a, "b =", b)
        a = euclidean_gcd(b, a)

    return a

def euclidean_ext_gcd(a,b):
    """Extended Euclidean Algorithm to find GCD and Bézout's identity.

    Extended Euclidean algorithm is an extension to the Euclidean algorithm,
    and computes, in addition to the greatest common divisor (GCD) of integers
    a and b, also the coefficients of Bézout's identity, which are integers
    x and y such that ax+by = gcd(a,b) [Wiki].

    Args:
        a (int): The first integer, > 0,
        b (int): The second integer, > 0.

    Returns:
        tuple(int,int,int): the gcd and coefficients x and y.

    """
    def calc_next_step(a,b,s,spv,t,tpv):

        if a < b:
            a,b = b,a

        r = a
        qs = 0
        qt = 0
        while r >= b:
            r = r - b
            qs += s
            qt += t

        spv, s = s, spv - qs
        tpv, t = t, tpv - qt

        return (b, r, s, spv, t, tpv )

    spv = 1
    tpv = 0
    s = 0
    t = 1

    flip = 0
    if a < b:
        flip = 1

    while (b != 0):
        #print("a =", a, "b =", b, "s =", s, "t =", t)
        a,b,s,spv,t,tpv = calc_next_step(a,b,s,spv,t,tpv)

    return (a,tpv,spv) if flip else (a,spv,tpv)

def newton(f, f_derivative, x0, eps, kmax):
    """Newton's method for finding roots.

    The Newton's method (Newton–Raphson method) is a root-finding algorithm
    which produces approximations to the roots (or zeroes)
    of a real-valued function. [Wiki].

    Args:
        f (function): single-variable function f ,
        f_derivative (function):  the function's derivative f ′,
        x0 (float): initial guess,
        eps (float): precision wanted,
        kmax (int): maximum number of iterations.

    Returns:
        x (float): root of f(x) = 0.

    """
    x = x0
    x_prev = x0 + 2 * eps
    i = 0

    while (abs(x - x_prev) >= eps) and (i < kmax):
        #print("Step", i, ":", int(x), int(x_prev), ", x - f(x) = ", int(x - f(x)), ", f_derivative(x) = ", int(f_derivative(x)), "f/f'=",int(f(x)/f_derivative(x)))
        x, x_prev =  x - ( f(x) / f_derivative(x) ), x
        i += 1

    return x

def rpmul(a,b):
    """Russian peasant multiplication.

    Simple multiplication on shifters, taken from "Ten Little Algorithms" by
    Jason Sachs.

    Args:
        a (int): the first variable,
        b (int): the second vairable.

    Returns:
        x (int): result of multiplication a*b.

    """
    result = 0
    while b != 0:
        if b & 1:
            result += a
        b >>= 1
        a <<= 1

    return result

def rpexp(a,b):
    """Russian peasant exponention.

    Exponention based on Russian peasant multiplication algorithm,
    taken from "Ten Little Algorithms" by Jason Sachs.

    Args:
        a (int): the base,
        b (int): the exponent.

    Returns:
        x (int): the b power of a, a**b.

    """
    result = 1
    while b != 0:
        if b & 1:
            result *= a
        b >>= 1
        a *= a
    return result

def sp_iir_lpf(cutoff = 0.25, smpl_f = 1):
    """Single-pole IIR low-pass filter.

    A single-pole IIR filter design y += alpha * (x-y),
    taken from "Ten Little Algorithms" by Jason Sachs.

    Args:
        cutoff (float): the cutoff frequency, can bi in proportion of sampling
                        frequency or in hertz if sampling frequency is given as
                        second argument
        smpl_f (float): sampling frequency in hertz (optional).

    Returns:
        alpha (float):  filter coefficient alpha,
        h (ndarray):    the frequency response as complex numbers,
        w (ndarray):    the frequencies at which h was computed
                        in proportion of pi

    """
    import numpy as np
    from scipy import signal

    # calculate coefficient
    dt = 1/smpl_f
    tau = 1 / cutoff
    alpha = dt/tau

    # do filtring and get impulse response to estimate parameters
    '''
    def do_filter(x, alpha, x0 = None):
        y = np.zeros_like(x)
        yk = x[0] if x0 is None else x0
        for k in range(len(x)):
            yk += alpha * (x[k]-yk)
            y[k] = yk
        return y
    # make test impulse signal
    smpls = np.zeros(1000)
    smpls[0] = 1
    # filter and get impulse response
    filter_result = do_filter(smpls, alpha)
    # get the filter parameters
    w, h = signal.freqz(filter_result)
    '''

    # get the frequency and phase response with help of scipy
    '''
    The function of the filter: y[n]=αx[n]+(1−α)y[n−1]
    The transfer function is H(z)=α / 1-(1−α)z−1
    '''
    b = alpha
    a = [1,-(1-alpha)]
    w, h = signal.freqz(b,a)
    # change radians to proportions of pi
    for i in range(len(w)):
        w[i] = w[i]/np.pi

    return alpha, h, w

def welford(x_array):
    """Welford's method.

    Mean and variance calculation using Welford's method,
    taken from part 3 of "Ten Little Algorithms" by Jason Sachs.

    Args:
        x_array (array): sample sequence.

    Returns:
        M, S: mean and variance of x_array.

    """
    k = 0
    M = 0
    S = 0
    for x in x_array:
        k += 1
        Mnext = M + (x - M) / k
        S = S + (x - M)*(x - Mnext)
        M = Mnext
    return (M, S/(k-1))


# M. Yaşar Polatlı
# 250201075

import numpy as np
import random
import matplotlib.pyplot as plt
import scipy.stats as stats
from scipy.stats import semicircular

# Experiment 1:

s1 = np.random.uniform(0, 1, 50000)  # 50000 values from standard uniform distribution (0,1) independently.

samples1 = [np.sum(random.choices(s1, k=2)) for i in range(50000)]  # 50000 sum samples from randomly chosen 2 values.

print("Experiment 1: ")
print("Sample mean is ", np.mean(samples1))
print("Sample standard deviation is ", np.std(samples1))
plt.figure()
plt.title("Experiment 1: Histogram for generated random variables")
plt.hist(s1, 100, density=True)
plt.show()
plt.title("Experiment 1: Histogram for sums of generated random variables")
plt.hist(samples1, 100, density=True)
# initializations for theoretical normal distribution curve
mu = np.mean(samples1)
sigma = np.std(samples1)
x = np.linspace(mu - 6*sigma, mu + 6*sigma, 200)
plt.plot(x, stats.norm.pdf(x, mu, sigma))
plt.show()

# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

# Experiment 2:

s2 = np.random.uniform(0, 1, 50000)  # 50000 values from standard uniform distribution (0,1) independently.

samples2 = [np.sum(random.choices(s2, k=10)) for i in range(50000)]  # sum samples of randomly chosen 10 values.

print("Experiment 2: ")
print("Sample mean is ", np.mean(samples2))
print("Sample standard deviation is ", np.std(samples2))
plt.figure()
plt.title("Experiment 2: Histogram for sums of generated random variables")
plt.hist(samples2, 100, density=True)
# initializations for theoretical normal distribution curve
mu = np.mean(samples2)
sigma = np.std(samples2)
x = np.linspace(mu - 6*sigma, mu + 6*sigma, 200)
plt.plot(x, stats.norm.pdf(x, mu, sigma))
plt.show()

# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

# Experiment 3:

s3 = np.random.uniform(0, 1, 50000)  # 50000 values from standard uniform distribution (0,1) independently.

samples3 = [np.sum(random.choices(s3, k=50)) for i in range(50000)]  # sum samples of randomly chosen 50 values.

print("Experiment 3: ")
print("Sample mean is ", np.mean(samples3))
print("Sample standard deviation is ", np.std(samples3))
plt.figure()
plt.title("Experiment 3: Histogram for sums of generated random variables")
plt.hist(samples3, 100, density=True)
# initializations for theoretical normal distribution curve
mu = np.mean(samples3)
sigma = np.std(samples3)
x = np.linspace(mu - 6*sigma, mu + 6*sigma, 200)
plt.plot(x, stats.norm.pdf(x, mu, sigma))
plt.show()

# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

# Experiment 4:

samples4 = np.array([])   # Array for sums of generated random variables.
samplesG = []   # Array for generated random variables.

i = 0
while i < 10000:    # Simulation continues 10000 times.

    sSum = 0  # Holds total value to check the conditions.
    sCount = 0  # Holds number of values which requested 100.

    while sCount < 101:  # Number of values is 100.
        if sSum < 40:
            a1 = np.random.uniform(0.5, 1.5)
            sSum += a1
            samplesG.append(a1)  # Generated random variables added corresponding array to observe via histogram.
        else:
            a2 = np.random.uniform(-0.5, 0.5)
            sSum += a2
            samplesG.append(a2)  # Generated random variables added corresponding array to observe via histogram.
        sCount += 1

    samples4 = np.append(samples4, sSum)  # Appends sums to corresponding array to observe whether it converges to normal or not.

    i += 1

print("Experiment 4: ")
print("Sample mean is ", np.mean(samples4))
print("Sample standard deviation is ", np.std(samples4))

plt.figure()
plt.title("Experiment 4: Histogram for generated random variables")
plt.hist(samplesG, 100, density=True)
plt.show()

plt.title("Experiment 4: Histogram for sums of generated random variables")
plt.hist(samples4, 100, density=True)
# initializations for theoretical normal distribution curve
mu = np.mean(samples4)
sigma = np.std(samples4)
x = np.linspace(mu - 6*sigma, mu + 6*sigma, 200)
plt.plot(x, stats.norm.pdf(x, mu, sigma))
plt.show()

# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

# Experiment 5:

s = semicircular.rvs(2, 1, size=50000)  # 50000 randomly generated values from a distribution of semi-circle of radius 1 centered at 2.

samples5 = [np.sum(random.choices(s, k=2)) for i in range(50000)]  # Number of values is 2.

print("Experiment 5: ")
print("Sample mean is ", np.mean(samples5))
print("Sample standard deviation is ", np.std(samples5))
plt.figure()
plt.title("Experiment 5: Histogram for generated random variables")
plt.hist(s, 100, density=True)
plt.show()
plt.title("Experiment 5: Histogram for sums of generated random variables")
plt.hist(samples5, 100, density=True)
# initializations for theoretical normal distribution curve
mu = np.mean(samples5)
sigma = np.std(samples5)
x = np.linspace(mu - 6*sigma, mu + 6*sigma, 200)
plt.plot(x, stats.norm.pdf(x, mu, sigma))
plt.show()

# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

# Experiment 6:

samples6 = [np.sum(random.choices(s, k=10)) for i in range(50000)]  # Number of values is 10.

print("Experiment 6: ")
print("Sample mean is ", np.mean(samples6))
print("Sample standard deviation is ", np.std(samples6))
plt.figure()
plt.title("Experiment 6: Histogram for sums of generated random variables")
plt.hist(samples6, 100, density=True)
# initializations for theoretical normal distribution curve
mu = np.mean(samples6)
sigma = np.std(samples6)
x = np.linspace(mu - 6*sigma, mu + 6*sigma, 200)
plt.plot(x, stats.norm.pdf(x, mu, sigma))
plt.show()

# * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

# Experiment 7:

samples7 = [np.sum(random.choices(s, k=50)) for i in range(50000)]  # Number of values is 50.

print("Experiment 7: ")
print("Sample mean is ", np.mean(samples7))
print("Sample standard deviation is ", np.std(samples7))
plt.figure()
plt.title("Experiment 7: Histogram for sums of generated random variables")
plt.hist(samples7, 100, density=True)
# initializations for theoretical normal distribution curve
mu = np.mean(samples7)
sigma = np.std(samples7)
x = np.linspace(mu - 6*sigma, mu + 6*sigma, 200)
plt.plot(x, stats.norm.pdf(x, mu, sigma))
plt.show()


"""
demo_1d_unconstrained_optimization.py.py

Minimize a function of one variable:
Minimize f(X)

Golden section algorithm
    Only special requirements.

Fibonacci algorithm
    Only special requirements.

Newton algorithm
    Requires 1st and 2nd derivatives.

Secant algorithm
    Requires only 1st derivative (2nd derivative estimated using 1st derivative).
"""

import numpy as np
import matplotlib.pyplot as plt
from bracketing import bracketing
from golden_section_algorithm import golden_section_algorithm, golden_section_algorithm_calc_N_iter
from fibonacci_algorithm import fibonacci_algorithm, fibonacci_algorithm_calc_N_iter
from newton_algorithm import newton_algorithm
from secant_algorithm import secant_algorithm
from print_report import print_report
from plot_progress_y import plot_progress_y
import time

reg_coeff = 0.01; # regularization coefficient
# Minimize the following objective function:
# f(X) = X^4 - 14 * X^3 + 60 * X^2 - 70 * X + reg_coeff * X^2;
func = lambda X : np.power(X, 4) - 14 * np.power(X, 3) + 60 * np.power(X, 2) -70 * X + reg_coeff * np.power(X, 2);
func_1st_der = lambda X : 4 * np.power(X, 3) - 3 * 14 * np.power(X, 2) + 2 * 60 * np.power(X, 1) -70 + 2 * reg_coeff * X;
func_2nd_der = lambda X : 3 * 4 * np.power(X, 2) - 2 * 3 * 14 * np.power(X, 1) + 2 * 60 + 2 * reg_coeff;

# Plot the curve
fig = plt.figure();
X = np.arange(-5, 12, 0.5);
Y = np.empty(shape = X.size);
for ii in range(X.size):
    Y[ii] = func(X[ii]);

plt.plot(X, Y)
plt.show()

# Bracketing
print('***********************************************************************')
print('Bracketing')
X0 = -20;
start = time.time();
interval0 = bracketing(X0, func, func_1st_der);
end = time.time();
print("Minimizer lies in interval [%0.3f %0.3f]" % (interval0[0], interval0[1]));
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n')

# Golden section algorithm
print('***********************************************************************')
print('Golden section algorithm')
uncertainty_range_desired = 0.01;
start = time.time();
N_iter = golden_section_algorithm_calc_N_iter(interval0, uncertainty_range_desired);
interval = golden_section_algorithm(func, interval0, N_iter);
end = time.time();
print("Desired uncertainty range: %0.2f" % (uncertainty_range_desired));
print("Algorithm converged in %d iteration" % (N_iter));
print("Initial uncertainty interval: [%0.3f %0.3f]" % (interval0[0], interval0[1]));
print("Reduced uncertainty interval: [%0.3f %0.3f]" % (interval[0], interval[1]));
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n')

# Fibonacci algorithm
print('***********************************************************************')
print('Fibonacci algorithm')
uncertainty_range_desired = 0.01;
start = time.time();
N_iter, F = fibonacci_algorithm_calc_N_iter(interval0, uncertainty_range_desired);
interval = fibonacci_algorithm(func, interval0, N_iter, F);
end = time.time();
print("Desired uncertainty range: %0.2f" % (uncertainty_range_desired));
print("Algorithm converged in %d iteration" % (N_iter));
print("Initial uncertainty interval: [%0.3f %0.3f]" % (interval0[0], interval0[1]));
print("Reduced uncertainty interval: [%0.3f %0.3f]" % (interval[0], interval[1]));
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n')

# Newton's algorithm
print('***********************************************************************')
print('Newton\'s algorithm')
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-6;
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max};
X0 = 0;
start = time.time();
X, report = newton_algorithm(X0, func, func_1st_der, func_2nd_der, options);
end = time.time();
print_report(report);
# Plot path to X* for Y
algorithm_name = 'Newton\'s algorithm';
plot_progress_y(algorithm_name, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n')

# Secant algorithm
print('***********************************************************************')
print('Secant algorithm')
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-6;
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max};
X0 = 0;
start = time.time();
X, report = secant_algorithm(X0, func, func_1st_der, options);
end = time.time();
print_report(report);
# Plot path to X* for Y
algorithm_name = 'Secant algorithm';
plot_progress_y(algorithm_name, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n')


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

import math
import numpy

PI2 = math.pi * 2

def dft2d(nums):
    """ 2D DFT

    args:
        nums = [ [ 1, 2, 3 ], [ ... ], ... ]
    """
    M = len(nums)
    N = len(nums[0])
    x = [ [ 0.0 + 0.0j for y in range(N) ] for x in range(M) ]

    for m in range(M):
        for n in range(N):
            for k in range(M):
                for l in range(N):
                    re = nums[m][n] * math.cos(PI2 * (k * m / M + l * n / N))
                    im = -nums[m][n] * math.sin(PI2 * (k * m / M + l * n / N))
                    x[k][l] += complex(re, im)
    return x

def idft2d(F):
    """ 2D IDFT

    args:
        F: 复数，2D傅里叶变换的结果

    args:
        二维复数
    """
    M = len(F)
    N = len(F[0])
    f = [ [ 0.0 + 0.0j for y in range(N) ] for x in range(M) ]
    MN1 = 1 / (M * N)
    for m in range(M):
        for n in range(N):
            for k in range(M):
                for l in range(N):
                    re = F[k][l].real * math.cos(PI2 * (k * m / M + l * n / N)) - F[k][l].imag * math.sin(PI2 * (k * m / M + l * n / N))
                    im = F[k][l].real * math.sin(PI2 * (k * m / M + l * n / N)) + F[k][l].imag * math.cos(PI2 * (k * m / M + l * n / N))
                    re *= MN1
                    im *= MN1
                    f[m][n] += complex(re, im)
    return f

if __name__ == '__main__':
    M = 2
    N = 2
    nums = [ [ x + 1 for y in range(N) ] for x in range(M) ]

    dft2d_result = dft2d(nums)

    # numpy_result = numpy.fft.fft2(nums)

    idft2d_result = idft2d(dft2d_result)

    # numpy_ifft = numpy.fft.ifft2(dft2d_result)

    # print("------nums--------")
    # print(nums)

    print("------dft2d--------")
    print(dft2d_result)

    # print("------numpy fft2--------")
    # print(numpy_result)

    print("------idft2d--------")
    print(idft2d_result)

    # print("------numpy ifft2--------")
    # print(numpy_ifft)

    success = True
    for m in range(M):
        for n in range(N):
            if math.fabs(nums[m][n].real - idft2d_result[m][n].real) > 1e-05:
                print("[%d,%d]: %f != %f" % (m, n, nums[m][n], idft2d_result[m][n]))
                success = False
    print("success: %s\n" % str(success))