import numpy as np
from numpy import linalg
from abc import abstractmethod
import pandas as pd
import math

pd.options.display.float_format = '{:,.10f}'.format
np.set_printoptions(suppress=True, precision=10)

TOR = pow(10.0, -5)

class NewtonMethod(object):

    def __init__(self):
        return

    @abstractmethod
    def f(self, x):
        return NotImplementedError('Implement f()!')

    @abstractmethod
    def jacobian(self, x):
        return NotImplementedError('Implement jacobian()!')

    @abstractmethod
    def run(self, x):
        return NotImplementedError('Implement run()!')

class Newton(NewtonMethod):

    def __init__(self):
        super(NewtonMethod, self).__init__()

    def f(self, x):
        sol = np.zeros(len(x))
        sol[0] = 3 * x[0] - math.cos(x[1] * x[2]) - 1.0 / 2.0
        sol[1] = pow(x[0], 2) - 81 * pow(x[1] + 0.1, 2) + math.sin(x[2]) + 1.06
        sol[2] = math.exp(-x[0] * x[1]) + 20 * x[2] + (10 * math.pi - 3.0) / 3.0
        return sol

    def jacobian(self, x):
        jac = np.zeros(shape=(3, 3))
        jac[0][0] = 3.0
        jac[0][1] = x[2] * math.sin(x[1] * x[2])
        jac[0][2] = x[1] * math.sin(x[1] * x[2])
        jac[1][0] = 2 * x[0]
        jac[1][1] = -162 * (x[1] + 0.1)
        jac[1][2] = math.cos(x[2])
        jac[2][0] = -x[1] * math.exp(-x[0] * x[1])
        jac[2][1] = -x[0] * math.exp(-x[0] * x[1])
        jac[2][2] = 20
        return jac

    def run(self, x):
        """
        given x_0 in R^3 as a starting point.

        :param x: x_0 as described
        :return: the minimizer x* of f
        """
        df = pd.DataFrame(columns=['x' + str(i + 1) for i in range(len(x))] + ['residual', 'actual-residual'])

        row = len(df)
        df.loc[row] = [xe for xe in x] + [np.nan, np.nan]

        while True:
            jac = self.jacobian(x)
            f = -self.f(x)
            y = linalg.solve(jac, f)
            nx = x + y
            residual = linalg.norm(x - nx, np.inf)
            x = nx

            row = len(df)
            df.loc[row] = [nxe for nxe in nx] + [residual, np.nan]
            if residual < TOR:
                break

        for i in range(len(df)):
            xk = np.array([df.loc[i][j] for j in range(len(x))])
            df.loc[i][4] = linalg.norm(x - xk, np.inf)

        print(df)

def main():
    x0 = np.array([0.1, 0.1, -0.1])
    Newton().run(x0)
    return

if __name__ == '__main__':
    main()


import sys
import numpy as np

A = [[31, -13, 0, 0, 0, -10, 0, 0, 0],
    [-13, 35, -9, 0, -11, 0, 0, 0, 0],
    [0, -9, 31, -10, 0, 0, 0, 0, 0],
    [0, 0, -10, 79, -30, 0, 0, 0, -9],
    [0, 0, 0, -30, 57, -7, 0, -5, 0],
    [0, 0, 0, 0, -7, 47, -30, 0, 0],
    [0, 0, 0, 0, 0, -30, 41, 0, 0],
    [0, 0, 0, 0, -5, 0, 0, 27, -2],
    [0, 0, 0, -9, 0, 0, 0, -2, 29]]

b = [-15, 27, -23, 0, -20, 12, -7, 7, 10]

def find_shape(A, b):
    """
    获取 n
    """

    # n行 m列
    n1, m1 = A.shape
    n2, = b.shape

    # print(n1, m1)
    # print(n2)

    # 判断矩阵形状
    if n1 != m1 or n1 != n2:
        print('Error martix shape!')
        sys.exit()
    else:
        N = n1

    return N

def MGauss(A, b):
    """
    列主元 Gauss 消去 消元
    """

    # 判断矩阵形状
    N = find_shape(A, b)

    # 列主元高斯消元
    for k in range(0, N):
        p = k
        maxabs = abs(A[k, k])
        # 找列最大值
        for i in range(k+1, N):
            if abs(A[i, k]) > maxabs:
                p = i
                maxabs = abs(A[i, k])
        # print('maxabs', maxabs)
        # 最大值为 0
        if maxabs == 0:
            print('Singular')
            sys.exit()
        # 最大值不在对角线, 则交换两行
        if p != k:
            A[[p,k],:] = A[[k,p],:]
            b[[p,k]] = b[[k,p]]
        # print('exchange r{0} and r{1}:\r\n'.format(k, p), A)
        # 消元，将对角线以下变为 0
        for i in range(k+1, N):
            m_ik = A[i, k] / A[k, k]
            for j in range(0, N):
                A[i, j] -= A[k, j] * m_ik
            b[i] -= b[k] * m_ik
        # print('After Elimination:\r\n', np.concatenate((A,np.asarray([b]).T), axis = 1))

    if A[N-1, N-1] == 0:
        print('Singular')
        sys.exit()

    return A, b

def bring_back(A, b):
    """
    列主元 Gauss 消去 回带
    """

    # 判断矩阵形状
    N = find_shape(A, b)

    # 回带
    X = np.zeros(N)
    X[N-1] = b[N-1] / A[N-1, N-1]
    for i in range(0, N-1):
        k = N-2-i
        sigma = sum(A[k, j]*X[j] for j in range(k+1, N))
        # print('k:{0}, sigma:{1}'.format(k, sigma))
        X[k] = (b[k] - sigma) / A[k, k]

    return X

# 列主元高斯消去求解
def MGauss_Caculate(A, b):
    A_np = np.asarray(A, dtype = float)
    b_np = np.asarray(b, dtype = float)

    # 列主元、回带
    A_G, b_G = MGauss(A_np, b_np)
    x_G = bring_back(A_G, b_G)

    # print('A_G:b_G\r\n', np.concatenate((A_G,np.asarray([b_G]).T), axis = 1))

    return x_G

def main():
    """
    main
    """

    x_G = MGauss_Caculate(A, b)
    print('x_G', x_G)

if __name__ == "__main__":
    main()


import numpy as np

def L2(v, *args):
    """Returns the L2 norm of a vector v

    INPUTS
    =======
    v: list
       Vector of which the L2 norm to be calculated
    *arg: list, optional
       Weight vector

    RETURNS
    ========
    L2_norm: float
       float unless len(v) != len(args[0])
       in which case a ValueError exception is raised

    EXAMPLES
    =========
    >>> L2([3, 4])
    5.0
    >>> L2([3, 4], [1, 1])
    5.0
    """
    s = 0.0 # Initialize sum
    if len(args) == 0: # No weight vector
        for vi in v:
            s += vi * vi
    else: # Weight vector present
        w = args[0] # Get the weight vector
        if (len(w) != len(v)): # Check lengths of lists
            raise ValueError("Length of list of weights must match length of target list.")
        for i, vi in enumerate(v):
            s += w[i] * w[i] * vi * vi
    return np.sqrt(s)


'''
Project A: An optical ray tracer
Aim: A module to model an optical system using the principle of geometric optics with different types of optical surfaces,
and subsequently optimising the curvature of a singlet lens. This module contains codes for plotting all the graphs.

Written by Son-Gyo Jung; Group F; Lab Group A9; 04.02.16
'''

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from math import *
import scipy.optimize as spo
import genpolarSJ as gp

#Normalisation function and Snell's law are placed at the beginning for convenience.
def normalise(x):
    """
    Normalises a given 3D vector.

    Parameter:
        x - a 3D vector that is an array.
    """
    if x is not None and len(x) == 3:
        return x/np.sqrt(np.dot(x,x))

    else:
        raise Exception("Three coordinates are required in the array.")
        return None

def Snell(k1, s_normal, n1, n2): #note: k1 must be a array
    '''
    Refracts the incident rays using Snell's law.

    Parameters:
        k1 - the direction of the incident ray.
        s_normal - vector normal to the surface.
        n1,n2 - the refractive indices either side of the surface.
    '''

    n_12 = n1/n2
    k1_hat = normalise(k1)
    s_normal_hat = normalise(s_normal)
    cos_angle1 = fabs(np.dot(k1_hat, s_normal_hat))
    angle1 = acos(cos_angle1)

    if sin(angle1) >= n2/n1:
        print "Total internal reflection."

        return None

    else:
        k2 = n_12*k1_hat + (n_12*cos_angle1 - np.sqrt(1 - n_12**2 * (1-cos_angle1)**2)) * s_normal_hat #vector form of Snell's law
        k2_hat = normalise(k2)

        return k2_hat

class ray:
    """
    Class defining the positions and directions of optical rays.

    Parameters:
        p - the position of the ray.
        k - the direction of the ray.
    """

    def __init__(self, p=[0.0, 0.0, 0.0], k=[0.0, 0.0, 1.0]):
        self.__positions = list()
        self.__directions = list()

        if p is None and len(p) != 3:
            raise Exception("p requires exactly 3 arguments.")

        else:
            self.__positions.append(np.array(p))

        if k is None and len(k) != 3:
            raise Exception("k requires exactly 3 arguments.")

        else:
            self.__directions.append(normalise(np.array(k))) #append normalised k as an array

    def p(self):
        '''
        Returns the current position of the ray.
        '''
        return self.__positions[-1]

    def k(self):
        '''
        Returns the current direction of the ray.
        '''
        return self.__directions[-1]

    def vertices(self):
        '''
        Returns the entire points through which the ray propagated.
        '''
        return self.__positions

    def klist(self):
        '''
        Returns the list of directions of the ray.
        '''
        return self.__directions

    def append(self, new_p=None, new_k=None):
        '''
        Add new position and direction of the ray

        Parameters:
            new_p - the new position of the ray.
            new_k - the new direction of the ray.
        '''

        if new_p is not None and len(new_p) == 3:
            self.__positions.append(np.array(new_p))

        else:
            raise Exception("P should only take 3 arguments.")

        if new_k is not None and len(new_k) == 3:
            self.__directions.append(np.array(new_k))

        else:
            raise Exception("K should only take 3 arguments.")

class OpticalElement: #provided in BlackBoard
    '''
    A base class for optical elements being employed; it propagates a ray through the optical element
    '''

    def propagate_ray(self, ray):

        raise NotImplementedError()

class SphericalRefraction(OpticalElement):
    """
    Initialises the interface of refraction with either a spherical lens or a plane lens, where the latter corresponds to the curvature of zero.
    It returns the location of the intercepts and the new direction calculated by calling the Snell's law function.

    Parameters:
        z0 - the intercept of the surface with the z-axis.
        curv - the curvature of the surface.
        n1,n2 - the refractive indices either side of the surface.
        apr - aperture radius which is the maximum extent of the surface from the optical axis.
    """

    def __init__(self, z0=[0, 0, 100], curv=0.03, n1=1.0, n2=1.5168, apr=1000):
        self.__z0 = z0
        self.__curv = curv
        self.__n1 = n1
        self.__n2 = n2
        self.__apr = apr

    def intercept(self, ray):
        '''
        Determines the point of interception of the ray with the given lens.
        '''

        if self.__curv != 0: #when the lens is spherical
            R = 1.0/self.__curv
            self.__centre = np.array([0., 0., R + self.__z0[2]])
            r = ray.p() - self.__centre
            discriminant = np.inner(r, normalise(ray.k()))**2 - (np.inner(r, r) - R**2) #using the equation provided in BlackBoard.

            if discriminant < 0:
                print "No real solution as the ray is complex."
                return None #No real solutions so terminate

            Lplus = -np.inner(r, normalise(ray.k())) + np.sqrt(discriminant) #Possible distance to the intercept
            Lminus = -np.inner(r, normalise(ray.k())) - np.sqrt(discriminant) #Possible distance to the intercept
            interceptplus = ray.p() + Lplus * normalise(ray.k()) #coordinate of the possible intercept
            interceptminus = ray.p() +Lminus * normalise(ray.k()) #coordinate of the possible intercept

            if min(self.__z0[2],self.__centre[2]) <= interceptminus[2] <= max(self.__z0[2],self.__centre[2]):
                return interceptminus

            if min(self.__z0[2],self.__centre[2]) <= interceptplus[2] <= max(self.__z0[2],self.__centre[2]):
                return interceptplus

        else: #when the lens is planar
            number_of_khatz = (self.__z0[2] - ray.p()[2])/normalise(ray.k())[2] #See lab book page 112
            intercept_in_plane = ray.p() + number_of_khatz * normalise(ray.k())
            return intercept_in_plane

    def propagate_ray(self,ray):
        '''
        Determines the new position and direction of the ray after propagating through the lens.
        '''
        intersection = self.intercept(ray)
        if intersection is None:
            print "No intersections with the lens."
            return None

        else: #determining the direction of the surface normal vector (using the convention taught in the vector calculus course)
            if self.__curv == 0:
                new_s_normal_hat = np.array([0., 0., -1.])

            elif self.__curv > 0:
                new_s_normal_hat = normalise(intersection - self.__centre)

            elif self.__curv < 0:
                new_s_normal_hat = normalise(self.__centre - intersection)

            else:
                raise Exception("Curvature is not defined.")

            newdir = Snell(normalise(ray.k()), new_s_normal_hat, self.__n1, self.__n2) #calling Snell's law
            newpos = intersection

            ray.append(newpos, newdir)

class OutputPlane(SphericalRefraction): #take SphericalRefraction class as an argument to inherit the methods
    '''
    Terminates the propagation of the ray by creating a plane with a normal vector parallel to the z-axis.
    Note: the output plane is modelled using a planar lens that has infinite aperture radius.
    '''

    def __init__(self, output_z):
        SphericalRefraction.__init__(self,[0,0,output_z], 0.00, 1., 1., float('inf')) #design output plane using a plane lens with infinite aperture radius

class raybundle(list):
    '''
    Generates a bundle of collimated rays using the genpolar module.
    Note: the parameters can be adjusted to vary the beam radius, distribution and the number of rays.

    Parameters:
        n - Number of rings (this includes the central ray).
        rmax - Parameter which determines the maximum radius. (Note: the array created starts with zero radius by default.)
        m - The step-size of number of rays in each subsequent ring.
    '''

    def __init__(self, n=6, rmax=6., m=6,k=[0., 0., 1.]):
        for r, t in gp.rtuniform(n, rmax, m):
            self.append(ray([r*cos(t), r*sin(t), 0], normalise(k)))

    def propagate_ray(self, lens_element):
        '''
        Propagates the given rays through a single optical element at a time.
        '''
        for r in self:
            lens_element.propagate_ray(r)

    def RMS(self):
        '''
        Caculates the Root-Mean-Square of the spot radius at the paraxial focus for the bundle of rays;
        that is the RMS deviation of the ray positions from the optical axis.
        '''

        rxy_squared = list()

        for r in self:
            rxy_squared.append(r.p()[0]**2 + r.p()[1]**2)

        RMS = np.sqrt(np.mean(rxy_squared))*1e-3

        print 'RMS is %sm' % (RMS)

        return RMS

    def plot2D(self):
        '''
        Plots the paths of the rays in two-dimensions.
        '''
        fig = plt.figure()
        ax = fig.add_subplot(111)
        for r in self:
            x, y, z = zip(*r.vertices())
            ax.plot(z, x, 'b')
        ax.set_title("Ray propagation in z-x plane")
        ax.set_xlabel("z (mm)")
        ax.set_ylabel("x (mm)")
        plt.grid()
        plt.show()

    def plot3D(self):
        '''
        Plots the paths of the rays in three-dimensions.
        '''
        fig = plt.figure()
        ax = fig.add_subplot(111, projection = '3d')
        for r in self:
            x, y, z = zip(*r.vertices())
            ax.plot(z, x, y) #use c = np.random.rand(3,1) to make it pretty
        ax.set_title("Ray propagation in 3D")
        ax.set_xlabel("z (mm)")
        ax.set_ylabel("y (mm)")
        ax.set_zlabel("x (mm)")
        plt.grid()
        plt.show()

    def plot2DOutput(self):
        '''
        Plots the rays that terminate in the output plane as a scatter graph in the x-y plane.
        '''
        fig = plt.figure()
        ax = fig.add_subplot(111)
        for r in self:
            x, y, z = zip(*r.vertices())
            ax.plot( x[-1],y[-1], 'bo')
        ax.set_title("Output in the x-y Plane")
        ax.set_xlabel("x (mm)")
        ax.set_ylabel("y (mm)")
        plt.ticklabel_format(style='sci', axis='both', scilimits=(0,0))
        plt.grid()
        plt.show()

#Extension tasks and additional calculators
def ParaxialFocus(fristlens, secondlens): #to calcuate the paraxial focus
    '''
    Accurately estimates the location of the paraxial focus in the z-direction by utilising a paraxial ray.

    Parameters:
        fristlens - the front/frist optical surface defined using the SphericalRefraction class.
        secondlens - the back/second optical surface defined using the SphericalRefraction class.
    '''

    paraxial_ray = ray([0.00001, 0., 0.], [0., 0., 1.,]) #defines the paraxial ray
    fristlens.propagate_ray(paraxial_ray)

    if secondlens is not None:
        secondlens.propagate_ray(paraxial_ray)

    paraxial_focus = paraxial_ray.p()[2] - paraxial_ray.p()[0] * paraxial_ray.k()[2]/paraxial_ray.k()[0] # using eq. x = m(z-f), see lab book p.132

    print 'Paraxial focus is at %smm' % (paraxial_focus)

    return paraxial_focus

def DiffractionLimit(fristlens, secondlens, FirstLensPos, rmax, n, image_dis, optimisation): #to calculate the diffraction limit
    '''
    Accurately estimates the diffraction limit by employing the ParaxialFocus function.

    Parameters:
        fristlens, secondlens - first and second optical surface defined using the SphericalRefraction class.
        rmax, n - defined using parameters of raybundle class.
    '''

    wavelength = 588e-9
    diameter = np.arange (0.0, rmax, rmax/n)[-1] * 2e-3

    if optimisation is True:
        f_length = image_dis*1e-3 - FirstLensPos*1e-3 - 2.5*1e-3 #for optimising biconvex lens with 5mm thickness

    elif  secondlens is not None:
        f_length = ParaxialFocus(fristlens, secondlens)*1e-3 - FirstLensPos*1e-3 - 2.5*1e-3 #for a lens of 5mm thickness

    else:
        f_length = ParaxialFocus(fristlens, secondlens)*1e-3 - FirstLensPos*1e-3 #for a single refracting surface

    D_limit = wavelength*f_length/diameter

    print 'Diffraction limit is %sm' % (D_limit)

    return D_limit

def OptimiseLenses(lensposition,image_dis, n, rmax, m, k): #to optimise a singlet lens into its best form for a given image distance.
    '''
    Optimises the curvatures of both lenses by minimising the RMS of spot radius at a given image distance;
    Note: both the positions of the lenses and the output plane is required.

    Parameters:
        lensposition - essentially the z-coordinate of z0.
        image_dis - the position of the image (i.e. the position of the output plane).
        n, rmax, m, k - defined using parameters of raybundle class.
    '''

    def singlet(curvatures):
        '''
        Parameter:
            curvatures - an optimizer which minimises the RMS spot radius using the downhill simplex algorithm.
            [Args: singlet - the objective function to be minimized; trial_solution - the initial guesses.]
        '''

        first_lens = SphericalRefraction(z0=[0., 0., lensposition], curv=curvatures[0], n1=1.0, n2=1.5168, apr=float('inf'))
        second_lens = SphericalRefraction(z0=[0., 0., lensposition+5], curv=curvatures[1], n1=1.5168, n2=1.0, apr=float('inf'))
        opp = OutputPlane(image_dis) #output plane positioned at the give image distance

        b = raybundle(n, rmax, m, k) #substitute with new values using ExecuteRayTracerSJ.py
        b.propagate_ray(first_lens)
        b.propagate_ray(second_lens)
        b.propagate_ray(opp)

        rxy_squared = list()

        for r in b:
            rxy_squared.append(r.p()[0]**2 + r.p()[1]**2)
        RMS = np.sqrt(np.mean(rxy_squared))*1e-3
        print 'RMS is %sm' % (RMS)

        return RMS

    trial_solutions = [0.05, -0.05]
    curvatures = spo.fmin(singlet, trial_solutions)

    print 'The best form curvatures for a given image distance of %s mm are: first lens curvature = %s/mm, second lens curvature = %s/mm.' % (image_dis, curvatures[0], curvatures[1])

def RMSvsDiffractionLimit(lensposition, image_dis, n, rmax, rmin, steps, m, k, optcurv_1, optcurv_2): #function to plot RMS spot radius and diffraction limit against increasing beam radius
    rmaxlist = np.linspace(rmin, rmax, steps)
    RMSlist = list()
    DLlist = list()

    first_lens = SphericalRefraction(z0=[0., 0., lensposition], curv=optcurv_1, n1=1.0, n2=1.5168, apr=float('inf'))
    second_lens = SphericalRefraction(z0=[0., 0., lensposition+5], curv=optcurv_2, n1=1.5168, n2=1.0, apr=float('inf'))
    opp = OutputPlane(image_dis)

    for rmax in rmaxlist:
        b = raybundle(n, rmax, m, k) #substitute with new values using ExecuteRayTracerSJ.py
        b.propagate_ray(first_lens)
        b.propagate_ray(second_lens)
        b.propagate_ray(opp)
        RMSlist.append(b.RMS()) #append all the RMS values

        DLlist.append(DiffractionLimit(first_lens, second_lens, lensposition, rmax, n, image_dis, optimisation=True)) #append the diffraction limits calculated using the DiffractionLimit function

    plt.figure()
    line_up, = plt.plot(rmaxlist,RMSlist,'g--', label='RMS Spot Radius')
    line_down, = plt.plot(rmaxlist,DLlist,'r-', label='Diffraction Limit')

    plt.ticklabel_format(style='sci', axis='y', scilimits=(0,0))
    plt.grid()
    plt.xlabel("Beam radius (mm)")
    plt.ylabel('Limiting factor (m)')
    plt.title('Diffraction Limit and RMS Spot Size with Beam Radius \n for Optimised Biconvex lens', fontsize=13)
    plt.autoscale(tight=True)
    plt.legend(handles=[line_up, line_down])
    plt.show()

def RMSPlanoConvex(lensposition, n, rmax, rmin, steps, m, k, optcurv_1, optcurv_2): #to plot the behaviour of RMS spot radius with increasing beam size when using the planocovex lens
    rmaxlist = np.linspace(rmin, rmax, steps)
    RMSlist = list()
    RMSlist2 = list() #both orientations considered

    first_lens = SphericalRefraction(z0=[0., 0., lensposition], curv=optcurv_1, n1=1.0, n2=1.5168, apr=float('inf'))
    second_lens = SphericalRefraction(z0=[0., 0., lensposition+5], curv=optcurv_2, n1=1.5168, n2=1.0, apr=float('inf'))
    third_lens = SphericalRefraction(z0=[0., 0., lensposition], curv=optcurv_2, n1=1.0, n2=1.5168, apr=float('inf'))
    fourth_lens = SphericalRefraction(z0=[0., 0., lensposition+5], curv=-optcurv_1, n1=1.5168, n2=1.0, apr=float('inf'))
    opp1 = OutputPlane(ParaxialFocus(first_lens,  second_lens))
    opp2 = OutputPlane(ParaxialFocus(third_lens, fourth_lens))

    for rmax in rmaxlist:
        b = raybundle(n, rmax, m, k) #substitute with new values using ExecuteRayTracerSJ.py
        b.propagate_ray(first_lens)
        b.propagate_ray(second_lens)
        b.propagate_ray(opp1)
        RMSlist.append(b.RMS()) #list of all the RMS values

    for rmax in rmaxlist:
        b2 = raybundle(n, rmax, m, k) #substitute with new values using ExecuteRayTracerSJ.py
        b2.propagate_ray(third_lens)
        b2.propagate_ray(fourth_lens)
        b2.propagate_ray(opp2)
        RMSlist2.append(b2.RMS()) #list of all the RMS values

    plt.figure()
    line_up, = plt.plot(rmaxlist,RMSlist, 'g-', label='Convex-Plano')
    line_down, = plt.plot(rmaxlist,RMSlist2, 'b--', label='Plano-Convex')
    plt.legend(handles=[line_up, line_down], loc=2)
    plt.ticklabel_format(style='sci', axis='y', scilimits=(0,0))
    plt.grid()
    plt.xlabel("Beam radius (mm)")
    plt.ylabel('RMS Spot Size (m)')
    plt.title('RMS spot size with beam radius\n(planoconvex lenses)')
    plt.autoscale(tight=True)
    plt.show()


#!/usr/bin/env python
"""
Simple vector classes. (Python3)

Why not use numpy instead? A couple of reasons:
1. performance, working with small numpy arrays introduces a lot of overhead
2. .x and .y class properties
3. clear implementation for readability and porting to other languages.

 Copyright (c) 2021 AIGRO B.V. - Jev Kuznetsov
 License: BSD

Special thanks for these information sources and references:

* [vector.py](https://gist.github.com/mostley/3819375)
* [StackOverflow](https://stackoverflow.com/a/43542669)
* [vector.py](https://github.com/betados/vector_2d/blob/develop/vector_2d/vector.py)

"""

from math import atan2, hypot, sin, cos
import numpy as np

def rotation_matrix(theta):
    """ create 2-d rotation matrix """
    return np.array([[cos(theta), sin(theta)], [-sin(theta), cos(theta)]])

class Vector:
    """ 2d vector, compatible with numpy """

    def __init__(self, *args):
        """ create from x,y or xy """
        try:
            self.x, self.y = float(args[0]), float(args[1])
        except (IndexError, TypeError):
            self.x = float(args[0][0])
            self.y = float(args[0][1])

    def __repr__(self):
        return f"(x={self.x:.3f}, y={self.y:.3f})"

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)

    def __eq__(self, other):
        return (self.x == other.x) & (self.y == other.y)

    def cross(self, other):
        return (self.x * other.y) - (self.y * other.x)

    def dot(self, other):
        return self.x * other.x + self.y * other.y

    def inner(self, other):
        return self.dot(other)

    def rotate(self, angle):
        a = np.dot(self, rotation_matrix(angle))
        return Vector(a[0], a[1])

    def round(self, precision=5):
        r = np.round(self, precision)
        return Vector(r)

    def angle(self, other):
        """ angle between vector, *relative* to other, range (-pi..pi)  """
        return -atan2(self.cross(other), self.inner(other))

    def __mul__(self, scalar):
        """ scalar multiplification """
        return Vector(self.x * scalar, self.y * scalar)

    def __rmul__(self, scalar):
        return self.__mul__(scalar)

    def __truediv__(self, scalar):  # note __div__ was removed in Python3
        return Vector(self.x / scalar, self.y / scalar)

    def __neg__(self):
        return Vector(-self.x, -self.y)

    def __abs__(self):
        return hypot(self.x, self.y)

    def distance(self, other):
        return hypot(self.x-other.x, self.y-other.y)

    @classmethod
    def from_polar(cls, r, theta):
        return Vector(r * cos(theta), r * sin(theta))

    def __getitem__(self, item):
        """ make subscriptable """
        return [self.x, self.y][item]

    def __array__(self, dtype=None) -> np.array:
        """ make this class compatable with numpy operations """
        if dtype:
            return np.array([self.x, self.y], dtype=dtype)
        else:
            return np.array([self.x, self.y])

def point_on_line(a: Vector, b: Vector, p: Vector):
    """
    project point p to line defined by a and b
    """
    ap = p - a
    ab = b - a
    return a + ap.dot(ab) / ab.dot(ab) * ab


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

import numpy as np
from scipy import stats
import math
import matplotlib as mpl
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm

def calc_statistics(x):
    n = x.shape[0]  # 样本个数

    # 手动计算
    m = 0
    m2 = 0
    m3 = 0
    m4 = 0
    for t in x:
        m += t
        m2 += t * t
        m3 += t ** 3
        m4 += t ** 4
    m /= n
    m2 /= n
    m3 /= n
    m4 /= n

    mu = m
    sigma = np.sqrt(m2 - mu * mu)
    skew = (m3 - 3 * mu * m2 + 2 * mu ** 3) / sigma ** 3
    kurtosis = (m4 - 4 * mu * m3 + 6 * mu * mu * m2 - 4 * mu ** 3 * mu + mu ** 4) / sigma ** 4 - 3
    print('手动计算均值、标准差、偏度、峰度：', mu, sigma, skew, kurtosis)

    # 使用系统函数验证
    mu = np.mean(x, axis=0)
    sigma = np.std(x, axis=0)
    skew = stats.skew(x)
    kurtosis = stats.kurtosis(x)
    return mu, sigma, skew, kurtosis

if __name__ == '__main__':
    d = np.random.randn(100000)
    print(d)
    mu, sigma, skew, kurtosis = calc_statistics(d)
    print('函数库计算均值、标准差、偏度、峰度：', mu, sigma, skew, kurtosis)
    # 一维直方图
    mpl.rcParams[u'font.sans-serif'] = 'SimHei'
    mpl.rcParams[u'axes.unicode_minus'] = False
    y1, x1, dummy = plt.hist(d, bins=50, normed=True, color='g', alpha=0.75)
    t = np.arange(x1.min(), x1.max(), 0.05)
    y = np.exp(-t ** 2 / 2) / math.sqrt(2 * math.pi)
    plt.plot(t, y, 'r-', lw=2)
    plt.title(u'高斯分布，样本个数：%d' % d.shape[0])
    plt.grid(True)
    plt.show()

    d = np.random.randn(100000, 2)
    mu, sigma, skew, kurtosis = calc_statistics(d)
    print('函数库计算均值、标准差、偏度、峰度：', mu, sigma, skew, kurtosis)
    # 二维图像
    N = 30
    density, edges = np.histogramdd(d, bins=[N, N])
    print('样本总数：', np.sum(density))
    density /= density.max()
    x = y = np.arange(N)
    t = np.meshgrid(x, y)
    fig = plt.figure(facecolor='w')
    ax = fig.add_subplot(111, projection='3d')
    ax.scatter(t[0], t[1], density, c='r', s=15 * density, marker='o', depthshade=True)
    ax.plot_surface(t[0], t[1], density, cmap=cm.Accent, rstride=2, cstride=2, alpha=0.9, lw=0.75)
    ax.set_xlabel(u'X')
    ax.set_ylabel(u'Y')
    ax.set_zlabel(u'Z')
    plt.title(u'二元高斯分布，样本个数：%d' % d.shape[0], fontsize=20)
    plt.tight_layout(0.1)
    plt.show()


import numpy as np

def DFT(M):
    M = np.array(M)
    try:
        N, _ = M.shape
    except:
        N, = M.shape
    FM = np.array(range(N))
    FM = FM.reshape(-1, 1).dot(FM.reshape(1, -1))
    FM = np.exp(FM * (-2 * np.pi * 1j / N))
    print('Discrete Fourier Transform Matrix:\n', FM)
    return FM.dot(M)

def IDFT(M):
    M = np.array(M)
    try:
        N, _ = M.shape
    except:
        N, = M.shape
    IFM = np.array(range(N))
    IFM = IFM.reshape(-1, 1).dot(IFM.reshape(1, -1))
    IFM = np.exp(IFM * (2 * np.pi * 1j / N))
    IFM = IFM /  N
    print('Inverse Discrete Fourier Transform Matrix:\n', IFM)
    return IFM.dot(M)


# Spatial Filter
#
# Applies a Lowpass, Highpass, Band Reject or Bandpass circular symmetric filter
# by convolution
#
import sys,os
import numpy as np
import scipy.misc as sm
import scipy.special as ss
#
# Import the module with the I/O scaffolding of the External Attribute
#
sys.path.insert(0, os.path.join(sys.path[0], '..'))
import extattrib as xa

#
# The attribute parameters
#
xa.params = {
	'Inputs': ['Input'],
	'StepOut' : {'Value': [9,9], 'Minimum': [9,9], 'Hidden': False, 'Same': True},
	'Par_0' : {'Name': 'Norm. Spatial Frequency', 'Value': 0.5},
	'Select' : {'Name': 'Type', 'Values': ['Low Pass', 'High Pass', 'Band Pass', 'Band Reject'], 'Selection': 0},
	'Help'  : 'http://waynegm.github.io/OpendTect-Plugin-Docs/external_attributes/Spatial_Filter_Circular.html'
}
#
# Define the compute function
#
def doCompute():
#
#	Compute the filter kernel
#
	nil = xa.SI['nrinl']
	nxl = xa.SI['nrcrl']
	centre_trace_x = nil//2
	centre_trace_y = nxl//2
	freq = xa.params['Par_0']['Value']
	type = xa.params['Select']['Selection']
	kernelFunc = lpKernel if type==0 else hpKernel if type==1 else bpKernel if type==2 else brKernel
	kernel = np.zeros((nil,nil,1))
	N = nil//2
	if (N%2 == 0):
		N=N-1
		kernel[1:2*N+2,1:2*N+2,1] = kernelFunc(N, freq)
	else:
		kernel = kernelFunc(N, freq)
#
#	This is the trace processing loop
#
	while True:
		xa.doInput()
#
#	Get the input
#
		indata = xa.Input['Input']
#
#	Apply the kernel
#
		outdata = np.sum(kernel * indata, axis=(0,1))
#------------------------------------------------------------------------------------
#
		xa.Output = outdata
		xa.doOutput()

def lpKernel(N, freq):
#
# Lowpass filter kernel generator
#
# N is the filter half-size, must be odd
# freq is the normalised cutoff frequency
#
# Returns the filter kernel of size (2N+1,2N+1)
#
    num = 2*N + 1
    result = np.zeros((num,num,1))
    for m in range(N+1):
        i = m+N
        im = -m+N
        for n in range(N+1):
            j = n+N
            jm = -n+N
            if (m==0 and n==0):
                result[i,j] = np.pi*freq**2/4
            else:
                pandq = m%2 + n%2
                mandn = np.sqrt(m*m+n*n)
                val = (sm.factorial2(N)**4 * np.pi**pandq * freq * ss.jn(1,np.pi * freq * mandn)) / ( 2**(pandq+1) * sm.factorial2(N+m) * sm.factorial2(N-m) * sm.factorial2(N+n) *sm.factorial2(N-n) *mandn )
                result[i,j] = val
                result[j,i] = val
                result[im,j] = val
                result[i,jm] = val
                result[im,jm] = val

    return result/np.sum(result)

def hpKernel(N, freq):
#
# Highpass circular symmetric filter kernel generator
#
# N is the filter half-size, must be odd
# freq is the normalised cutoff frequency
#
# Returns the filter kernel of size (2N+1,2N+1)
#
    result = lpKernel(N, freq)
    for m in range(-N,N+1):
        i = m+N
        for n in range(-N,N+1):
            j = n+N
            if (m==0 and n==0):
                result[i,j] = 1-result[i,j]
            else:
                result[i,j] = -result[i,j]
    return result

def brKernel(N, freq, halfwidth=0.1):
#
# Band Reject circular symmetric filter kernel generator
#
# N is the filter half-size, must be odd
# freq is the normalised centre frequency of the reject band
# halfwidth controls the aperture of the reject band to freq +/- halfwidth
#
# Returns the filter kernel of size (2N+1,2N+1)
#
    kernel_lp = lpKernel(N, freq-halfwidth)
    kernel_hp = hpKernel(N, freq+halfwidth)
    result = kernel_lp + kernel_hp
    return result

def bpKernel(N, freq , halfwidth=0.1):
#
# Bandpass circular symmetric filter kernel generator
#
# N is the filter half-size, must be odd
# freq is the normalised centre frequency of the pass band
# halfwidth controls the aperture of the pass band to freq +/- halfwidth
#
# Returns the filter kernel of size (2N+1,2N+1)
#
    result = brKernel(N, freq, halfwidth)
    for m in range(-N,N+1):
        i = m+N
        for n in range(-N,N+1):
            j = n+N
            if (m==0 and n==0):
                result[i,j] = 1-result[i,j]
            else:
                result[i,j] = -result[i,j]
    return result
#
# Assign the compute function to the attribute
#
xa.doCompute = doCompute
#
# Do it
#
xa.run(sys.argv[1:])


# Bengisu Ayan - 2236974
# Ceren Gürsoy - 2237485

from math import sqrt
import numpy as np
import cv2

# fourier transform the image
def fourier_transform(image):
    image_f_trans = np.fft.fft2(image)
    image_f_shift = np.fft.fftshift(image_f_trans)  # flip the corners

    return image_f_shift

# butterworth filter
def butter_worth_filter(size_x, size_y, r, n):
    b_filter = np.zeros([size_x, size_y])
    for u in range(size_x):
        for v in range(size_y):
            val = pow((u - int(size_x / 2)), 2) + pow((v - int(size_y / 2)), 2)
            if val != 0:  # to prevent zero division
                x = pow(r / sqrt(val), 2 * n)
                b_filter[u][v] = 1 / (1 + x)

    return b_filter

# apply butterworth filter to image
def apply_filter(f_shifted_image, r, n):
    b_filter = butter_worth_filter(f_shifted_image.shape[0], f_shifted_image.shape[1], r, n)
    filtered_image = np.multiply(f_shifted_image, b_filter)

    return filtered_image

# inverse transform the image
def inverse_fourier_transform(filtered_image):
    image_inv_f_shift = np.fft.ifftshift(filtered_image)  # flip the corners back
    image_inv_f_trans = np.fft.ifft2(image_inv_f_shift)

    return image_inv_f_trans

B1 = cv2.imread('B1.jpg', 0)  # read image as grayscale
B2 = cv2.imread('B2.jpg', 0)  # read image as grayscale
B3 = cv2.imread('B3.jpg', 0)  # read image as grayscale

b1_f_shift = fourier_transform(B1)  # fourier transform the image
b1_filtered = apply_filter(b1_f_shift, 80, 2)  # apply butterworth filter to image with r=80 and n=2
b1_inv_f_shift = inverse_fourier_transform(b1_filtered)  # inverse transform the image
b1_abs = np.abs(b1_inv_f_shift)  # take absolute in order to save
cv2.imwrite('B1_output.png', b1_abs)  # save image

b2_f_shift = fourier_transform(B2)  # fourier transform the image
b2_filtered = apply_filter(b2_f_shift, 80, 2)  # apply butterworth filter to image with r=80 and n=2
b2_inv_f_shift = inverse_fourier_transform(b2_filtered)  # inverse transform the image
b2_abs = np.abs(b2_inv_f_shift)  # take absolute in order to save
cv2.imwrite('B2_output.png', b2_abs)  # save image

b3_f_shift = fourier_transform(B3)  # fourier transform the image
b3_filtered = apply_filter(b3_f_shift, 50, 2)  # apply butterworth filter to image with r=50 and n=2
b3_inv_f_shift = inverse_fourier_transform(b3_filtered)  # inverse transform the image
b3_abs = np.abs(b3_inv_f_shift)  # take absolute in order to save
cv2.imwrite('B3_output.png', b3_abs)  # save image


import numpy as np
from numpy.core.umath import sin, cos, e, absolute, sqrt, pi, power

def fun1(x, y):
    return sin(x) * cos(y)

def fun2(x, y):
    return (e**(cos(y) * sin(x) * 2)  +  x + y + sin(x) * 5)**3

def rosenbrock(x, y):
    Z = (power((1 - x), 2) + 100 * power((y - x * x), 2))
    return Z

def rastrigin(x,y):
    return 20 + (x * x - 10 * cos(2 * np.pi * x)) + (y * y - 10 * cos(2 * np.pi * y))

def himmelblau(x,y):
    return (x * x + y -11)**2 + (x + y * y - 7)**2

def htable(x,y):
    return - absolute(sin(x) * cos(x) * power(e, (absolute(1 - (sqrt(x * x + y * y)/pi)))))


from SequenceType.Base import SequenceType
from scipy.special import comb
from scipy.optimize import fsolve
from sympy import factorial

class CatalanNumberSequence(SequenceType):

    def __init__(self, seq):
        super(CatalanNumberSequence, self).__init__(seq)
        self.n = 0  # nth catalan number.  Number in the 0,1,2,3,etc catalan sequence

    def __bool__(self):
        """ Check whether sequence is Catalan Numbers """

        # solve for the catalan term position of the first number
        # arbitrarily start with initial guess of 5
        # todo think about this because it won't work with much larger numbers
        self.n = round(fsolve(lambda x: (comb(2 * x, x) * 1 / (x + 1)) - self.seq.ls[0], 5)[0])

        if self.n == 1 and self.seq.ls[1] == 1:
            self.n = 0

        for i in range(1, self.seq.size):
            if self.seq.ls[i] != comb(2 * (self.n + i), self.n + i) * 1 / (self.n + i + 1):
                return False

        return True

    @staticmethod
    def nth_catalan_number(n):
        """ Get the nth term catalan number """
        return comb(2 * n, n) * 1 / (n + 1)

    def term_number(self, index):
        """ The ith number in the sequence """
        n = self.n + index - 1

        return CatalanNumberSequence.nth_catalan_number(n)

    def seq_str(self, n):
        n_repr = self.n - 1
        expression = factorial(2 * (n + n_repr)) / (factorial(n + n_repr + 1) * factorial(n + n_repr))
        return expression


# coding: utf-8

# In[1]:

import numpy as np
import numpy.linalg as la
import matplotlib.pyplot as plt
import pandas as pd

# In[5]:

ks=pd.read_csv('kstable.csv', sep=',',header=[0]) #Soultion to the Kuramoto-Sivashinsky equation

# In[10]:

plt.imshow(ks, cmap='Set1', interpolation='none') #cmap: magma, inferno, Pastel1...
                                                  #https://matplotlib.org/examples/color/colormaps_reference.html
plt.show()

# In[11]:

U, sigma, V = np.linalg.svd(ks);

# In[13]:

reconstimg = np.matrix(U[:, :1]) * np.diag(sigma[:1]) * np.matrix(V[:1, :])
plt.imshow(reconstimg, cmap='Set1', interpolation='none');
plt.show()

# In[26]:

#Even with just the second and third vectors, the shape of the stiffness mtx begins to appear.
nmax=3;
for i in range(1,nmax):
    reconstimg = np.matrix(U[:, :i]) * np.diag(sigma[:i]) * np.matrix(V[:i, :])
    plt.imshow(reconstimg, cmap='Set1', interpolation='none')
    title = "n = %s" % i
    plt.title(title)
    plt.show()

# In[62]:

fig=plt.figure()
fig.subplots_adjust(wspace=2, hspace=0.6)

plt.subplot(2,2,1)
plt.imshow(np.matrix(U[:, :1]) * np.diag(sigma[:1]) * np.matrix(V[:1, :]), cmap='Set1', interpolation='none')
plt.title('n=1, \ncompressed representation')
plt.subplot(2,2,2)
plt.imshow(np.matrix(U[:, :2]) * np.diag(sigma[:2]) * np.matrix(V[:2, :]), cmap='Set1', interpolation='none')
plt.title('n=2')
plt.subplot(2,2,3)
plt.imshow(np.matrix(U[:, :3]) * np.diag(sigma[:3]) * np.matrix(V[:3, :]), cmap='Set1', interpolation='none')
plt.title('n=3')
plt.subplot(2,2,4)
plt.imshow(ks, cmap='Set1', interpolation='none')
plt.title('full representation')
plt.grid(True)
fig.suptitle('Singular Value Decomposition of Kuramoto-Sivashinsky chaotic PDE', size=14, y=1.08) # or plt.suptitle('Main title')
plt.show()
fig.savefig("ks_SVD.pdf", bbox_inches='tight')
fig.savefig("ks_SVD.png", bbox_inches='tight')

# In[ ]:


from Exercise5 import polynomial
from Exercise5 import splines
import numpy as np
import matplotlib.pyplot as plt
from math import sqrt
from Exercise5 import leastSquares

def plot_two_functions(x_values, y_values, z_values, plot_text):
    plt.figure(figsize=(10, 10))
    plt.grid(True, which='both')
    plt.xlim([min(x_values) - 0.2, max(x_values) + 0.2])
    plt.ylim([-1.1, 1.1])
    plt.xlabel("x", fontsize="xx-large")
    plt.ylabel("f(x)", fontsize="xx-large")
    plt.title("Approximation of " + r'$sinx$' + plot_text, fontsize="xx-large")
    plt.axhline(y=0, color='k')
    plt.axvline(x=0, color='k')
    plt.legend(["f(x)"], loc='upper left', fontsize="xx-large")
    plt.plot(x_values, y_values)
    plt.plot(x_values, z_values)
    plt.show()

def main():
    step = 0.0315

    # Polynomial Interpolation
    x_values = np.arange(-np.pi, np.pi, step)
    print("len x newton = " + str(len(x_values)))
    y_values = []
    for value in x_values:
        y_values.append(polynomial.custom_sin(value))
    real_sin_values = np.sin(x_values)
    plot_two_functions(x_values, y_values, real_sin_values, " using Newton method")
    print("Newton Root Mean Squared Error of prediction = " + str(sqrt(sum((real_sin_values - y_values) ** 2) /
                                                                       len(x_values))))
    # Splines Interpolation
    x_values = np.arange(-np.pi, np.pi, step)
    print("len x splines = " + str(len(x_values)))
    y_values = []
    for value in x_values:
        y_values.append(splines.custom_sin(value))
    real_sin_values = np.sin(x_values)
    plot_two_functions(x_values, y_values, real_sin_values, " using natural cubic Splines")
    print(
        "Splines Root Mean Squared Error of prediction = " + str(sqrt(sum((real_sin_values - y_values) ** 2) /
                                                                      len(x_values))))
    # Least Squares Interpolation
    degree = 10
    x_values = np.arange(-np.pi, np.pi, step)
    print("len x least squares = " + str(len(x_values)))
    y_values = []
    for value in x_values:
        y_values.append(leastSquares.custom_sin(value, degree))
    real_sin_values = np.sin(x_values)
    plot_two_functions(x_values, y_values, real_sin_values, " using Least Squares with " + str(degree) +
                       " degree polynomial")
    rmse = sqrt(sum((real_sin_values - y_values) ** 2) / len(x_values))
    print(
        "Least Squares using " + str(degree) + " degree polynomial model Root Mean Squared Error of prediction = " +
        str(rmse))

    # Comparison on [-π,π]

    # Newton Method
    x_values = np.arange(-np.pi, np.pi, step)
    y_values = []
    for value in x_values:
        y_values.append(polynomial.custom_sin(value))
    real_sin_values = np.sin(x_values)
    error = abs(real_sin_values - y_values)
    for value in error:
        if value > 0.5 * (10 ** (-3)):
            print("newton false assumption of correct digits")
            print(value)
            break

    # error figure for newton method
    plt.figure(figsize=(10, 10))
    plt.grid(True, which='both')
    plt.xlim([min(x_values) - 0.1, max(x_values) + 0.1])
    plt.ylim([-0.00001, 0.0001])
    plt.xlabel("x", fontsize="xx-large")
    plt.ylabel("Error", fontsize="xx-large")
    plt.title("Error of Newton method for 200 points on [-π,π]", fontsize="xx-large")
    plt.axhline(y=0, color='k')
    plt.axvline(x=0, color='k')
    plt.legend(["Error Function"], loc='upper left', fontsize="xx-large")
    plt.plot(x_values, error)
    plt.show()

    print("newton max error = " + str(max(error)))
    print("newton min error = " + str(min(error)))
    for i in range(len(error)):
        error[i] *= 100000
    hist, bin_edges = np.histogram(error)
    bin_edges = np.round(bin_edges, 0)
    plt.style.use('ggplot')
    plt.figure(figsize=[11, 11])
    plt.bar(bin_edges[:-1], hist, width=0.8, color='#2b2b2b', alpha=0.9)
    plt.xlim(min(bin_edges - 1), max(bin_edges + 1))
    plt.grid(axis='y', alpha=0.75)
    plt.xlabel('Error multiplied by 100000', fontsize='xx-large')
    plt.ylabel('Frequency', fontsize='xx-large')
    plt.xticks(fontsize='xx-large')
    plt.yticks(fontsize='xx-large')
    plt.ylabel('Frequency', fontsize='xx-large')
    plt.title("Histogram for error using Newton method", fontsize='xx-large')
    plt.show()

    # Splines method
    x_values = np.arange(-np.pi, np.pi, step)
    y_values = []
    for value in x_values:
        y_values.append(splines.custom_sin(value))
    real_sin_values = np.sin(x_values)
    error = abs(real_sin_values - y_values)
    for value in error:
        if value > 0.5 * (10 ** (-2)):
            print("splines false assumption of correct digits")
            print(value)
            break
    # error figure for splines method
    plt.figure(figsize=(10, 10))
    plt.grid(True, which='both')
    plt.xlim([min(x_values) - 0.1, max(x_values) + 0.1])
    plt.ylim([-0.0001, 0.0045])
    plt.xlabel("x", fontsize="xx-large")
    plt.ylabel("Error", fontsize="xx-large")
    plt.title("Error of Splines method for 200 points on [-π,π]", fontsize="xx-large")
    plt.axhline(y=0, color='k')
    plt.axvline(x=0, color='k')
    plt.legend(["Error Function"], loc='upper left', fontsize="xx-large")
    plt.plot(x_values, error)
    plt.show()
    print("splines max error = " + str(max(error)))
    print("splines min error = " + str(min(error)))
    multiplier = 1000
    for i in range(len(error)):
        error[i] *= multiplier
    hist, bin_edges = np.histogram(error)
    bin_edges = np.round(bin_edges, 0)
    plt.style.use('ggplot')
    plt.figure(figsize=[11, 11])
    plt.bar(bin_edges[:-1], hist, width=0.8, color='#2b2b2b', alpha=0.9)
    plt.xlim(min(bin_edges - 1), max(bin_edges + 1))
    plt.grid(axis='y', alpha=0.75)
    plt.xlabel('Error multiplied by ' + str(multiplier), fontsize='xx-large')
    plt.ylabel('Frequency', fontsize='xx-large')
    plt.xticks(fontsize='xx-large')
    plt.yticks(fontsize='xx-large')
    plt.ylabel('Frequency', fontsize='xx-large')
    plt.title("Histogram for error using natural cubic Splines", fontsize='xx-large')
    plt.show()

    # Least Squares
    x_values = np.arange(-np.pi, np.pi, step)
    y_values = []
    for value in x_values:
        y_values.append(leastSquares.custom_sin(value))
    real_sin_values = np.sin(x_values)
    error = abs(real_sin_values - y_values)
    for value in error:
        if value > 0.5 * (10 ** (-3)):
            print("least squares false assumption of correct digits")
            print(value)
            break
    # error figure for least squares method
    plt.figure(figsize=(10, 10))
    plt.grid(True, which='both')
    plt.xlim([min(x_values) - 0.1, max(x_values) + 0.1])
    plt.ylim([-0.00001, 0.0001])
    plt.xlabel("x", fontsize="xx-large")
    plt.ylabel("Error", fontsize="xx-large")
    plt.title("Error of Least Squares method for 200 points on [-π,π]", fontsize="xx-large")
    plt.axhline(y=0, color='k')
    plt.axvline(x=0, color='k')
    plt.legend(["Error Function"], loc='upper left', fontsize="xx-large")
    plt.plot(x_values, error)
    plt.show()
    print("least squares max error = " + str(max(error)))
    print("least squares min error = " + str(min(error)))
    multiplier = 100000
    for i in range(len(error)):
        error[i] *= multiplier
    hist, bin_edges = np.histogram(error)
    bin_edges = np.round(bin_edges, 0)
    plt.style.use('ggplot')
    plt.figure(figsize=[11, 11])
    plt.bar(bin_edges[:-1], hist, width=0.8, color='#2b2b2b', alpha=0.9)
    plt.xlim(min(bin_edges - 1), max(bin_edges + 1))
    plt.grid(axis='y', alpha=0.75)
    plt.xlabel('Error multiplied by ' + str(multiplier), fontsize='xx-large')
    plt.ylabel('Frequency', fontsize='xx-large')
    plt.xticks(fontsize='xx-large')
    plt.yticks(fontsize='xx-large')
    plt.ylabel('Frequency', fontsize='xx-large')
    plt.title("Histogram for error using least squares with 10th degree polynomial model", fontsize='xx-large')
    plt.show()

main()


import numpy as np
import math
from random import shuffle
from matplotlib import pyplot as plt
import matplotlib.pyplot as plt
import matplotlib.pyplot as pause
from mpl_toolkits.mplot3d import Axes3D
from time import sleep
import matplotlib.animation as animation
from matplotlib import cm
import sys

# DEFAULT PARAMETERS
threshold = 0.0000000000001

def plotPoints(plotOutFunction, fy=[]):
	x = []
	y = []

	for i in range(len(xyPoints)):
		x.append(xyPoints[i][0])
		y.append(xyPoints[i][len(xyPoints[0])-1])

	plt.title("XY points and predicted function")
	plt.scatter(x,y,color='blue',label='Actual Data')
	if (plotOutFunction == 1):
		plt.plot(x,fy,color='red',label='Curve Fitted')

	plt.xlabel('Input Data')
	plt.ylabel('Output Data')
	plt.legend()
	plt.show()

def plot3D(xyPoints,plotThetaX, plotThetaY, plotJo, timeGap):
	theta0, theta1 = np.mgrid[0:2:50j, -1:1:50j]
	mesh = np.c_[theta0.flatten(), theta1.flatten()]

	JVal = (np.array([calculateError(xyPoints,point) for point in mesh]).reshape(50,50))

	plt.ion()
	fig = plt.figure()
	ax = fig.add_subplot(111, projection='3d')

	ax.plot_surface(theta0, theta1, JVal, cmap=cm.RdBu_r)
	ax.set_xlabel(r'$\theta_0$', labelpad=10)
	ax.set_ylabel(r'$\theta_1$', labelpad=10)
	ax.set_zlabel(r'$J(\theta)$', labelpad=10)

	plt.show()

	for i in range(len(plotJo)):
		if i % 100 == 0:
			ax.plot([plotThetaX[i]], [plotThetaY[i]], [plotJo[i]], linestyle='-',color='r', marker='o', markersize=3)
			plt.pause(timeGap)

	plt.savefig("Assign1Q1-3DPlot.png")

def plotContours(xyPoints, plotThetaX, plotThetaY, plotJo,title, doMod100 = True):
	theta0, theta1 = np.mgrid[-2:2:50j, -1:2:50j]
	mesh = np.c_[theta0.flatten(), theta1.flatten()]

	JVal = (np.array([calculateError(xyPoints,point) for point in mesh]).reshape(theta0.shape))

	plt.ion()
	plt.title(title)
	plt.contour(theta0, theta1, JVal, 25, colors="k")
	plt.xlabel(r'$\theta_0$', labelpad=10)
	plt.ylabel(r'$\theta_1$', labelpad=10)

	plt.show()

	for i in range(len(plotJo)):
		if doMod100 and i % 100 == 0:
			plt.plot([plotThetaX[i]], [plotThetaY[i]], linestyle='-',color='r', marker='o', markersize=3)
			plt.pause(0.02)
		elif doMod100 == False:
			plt.plot([plotThetaX[i]], [plotThetaY[i]], linestyle='-',color='r', marker='o', markersize=3)
			plt.pause(0.02)

	plt.savefig("Assign1Q1-Contours.png")
	plt.close()

def readPoints(fileX, fileY):
	noOfPoints = 0
	xyPoints = []

	f1 = open(fileX, "r")
	f2 = open(fileY, "r")

	xPoint = f1.read().splitlines()
	yPoint = f2.read().splitlines()

	for i in range(len(xPoint)):
		xy1Point = []
		xy1Point.append(float(xPoint[i]))
		xy1Point.append(1.0)			# This is the value corresponding to theta0 (since theta0 is the intercept)
		xy1Point.append(float(yPoint[i]))

		xyPoints.append(xy1Point)
		noOfPoints = noOfPoints + 1

	f1.close()
	f2.close()

	return xyPoints, noOfPoints

def normalize(xyPoints):
	mean = []
	var = []

	for j in range(len(xyPoints[0])):
		if j == len(xyPoints[0])-2:
			break

		x = []
		for i in range(len(xyPoints)):
			x.append(xyPoints[i][j])

		mean.append(np.mean(x))
		var.append(np.std(x))

		for i in range(len(xyPoints)):
			xyPoints[i][j] = (xyPoints[i][j] - mean[j]) / var[j]

	return xyPoints, mean, var

def unormalize(xyPoints, mean, var, fx=[], isFxAvailable=False):
	for i in range(len(xyPoints[0])):
		if (i == len(xyPoints[0])-2):
			break

		for j in range(len(xyPoints)):
			xyPoints[j][i] = (xyPoints[j][i] * var[i]) + mean[i]

			if isFxAvailable == True:
				fx[j] = (fx[j] * var[i]) + mean[i]

	return xyPoints, fx

def getNewFunction(xyPoints, thetaVec):
	fy = []

	for k in range(len(xyPoints)):
		fy1 = 0.0
		for i in range(len(thetaVec)):
			fy1 = fy1 + thetaVec[i] * xyPoints[k][i]

		fy.append(fy1)

	return fy

def calculateError(xyPoints, thetaVec):
	diffSq = 0.0
	for i in range(len(xyPoints)):
		htheta = 0.0
		for j in range(len(xyPoints[i])-1):
			htheta = htheta + thetaVec[j] * xyPoints[i][j]

		diff = htheta - xyPoints[i][len(xyPoints[0])-1]
		diffSq = diffSq + diff * diff

	Jo = diffSq / float(2 * len(xyPoints))
	return Jo

def derivateOfError(thetaVec, xyPoints, featureNo):
	derivJo = 0.0
	for i in range(len(xyPoints)):
		htheta = 0.0
		for j in range(len(xyPoints[i])-1):
			htheta = htheta + thetaVec[j] * xyPoints[i][j]

		derivJo = derivJo + (htheta - xyPoints[i][len(xyPoints[0])-1]) * xyPoints[i][featureNo]

	derivJo = derivJo / float(len(xyPoints))
	return derivJo

def updateTheta(xyPoints, thetaVec, alpha):
	tempThetaVec = [0] * (len(xyPoints[0]) - 1)

	# First loop is just to accumulate changed theta. DONOT modify thetaVec in this loop
	for i in range(len(thetaVec)):
		tempThetaVec[i] = thetaVec[i] - alpha * derivateOfError(thetaVec, xyPoints, i)

	# Once all newTheta is calculated, update the thetaVec in this loop
	for i in range(len(thetaVec)):
		thetaVec[i] = tempThetaVec[i]

	return thetaVec

def gradientDescent(xyPoints, noOfPoints, alpha):
	JPvs = 0
	thetaVec = [0] * (len(xyPoints[0]) - 1)		# Last col is y so we will subtract that col in thetaVec and last theta is O0 which we will add a column in theta Vec
	mean = [0] * len(xyPoints[0])
	var = [0] * len(xyPoints[0])

	plotThetaX = []
	plotThetaY = []
	plotJo = []

	while 1:
		thetaVec = updateTheta(xyPoints, thetaVec, alpha)
		JCur = calculateError(xyPoints, thetaVec)

		if JCur == float("inf"):
			break

		if JCur < 1000:
			plotThetaX.append(thetaVec[0])
			plotThetaY.append(thetaVec[1])
			plotJo.append(JCur)

		if abs(JCur-JPvs) <= threshold:
			break							# Converged

		JPvs = JCur

	return thetaVec, plotThetaX, plotThetaY, plotJo

fileX = sys.argv[1] # prints python_script.py
fileY = sys.argv[2] # prints var1
learningRate = float(sys.argv[3]) # prints var2
timeGap = float(sys.argv[4]) # prints var2

xyPoints,noOfPoints = readPoints(fileX, fileY)
xyPoints, mean, var = normalize(xyPoints)

############ Part (a)
alpha = learningRate
thetaVec, plotThetaX, plotThetaY, plotJo = gradientDescent(xyPoints, noOfPoints, alpha)

print ("Learning Rate = ", alpha)
print ("Stopping Criteria = J0 - JPvs <= ", threshold)
print ("Theta Vector = ", thetaVec)

############# Part (b)
fx = getNewFunction(xyPoints, thetaVec)
plotPoints(True, fx)

############# Part (c)
plot3D(xyPoints,plotThetaX, plotThetaY, plotJo, timeGap)

############# Part (d)
plotContours(xyPoints,plotThetaX, plotThetaY, plotJo, "Contours for learning rate = " + str(alpha))

############# Part (e)
alphaL = [0.001, 0.002, 0.003, 0.02, 2.1]
for i in range(len(alphaL)):
	alpha = alphaL[i]
	thetaVec, plotThetaX, plotThetaY, plotJo = gradientDescent(xyPoints, noOfPoints, alpha)
	if i == 4:
		plotContours(xyPoints,plotThetaX, plotThetaY, plotJo, "Contours for learning rate = " + str(alpha),False)
	else:
		plotContours(xyPoints,plotThetaX, plotThetaY, plotJo, "Contours for learning rate = " + str(alpha))


"""
revised_simplex_algorithm.py

Returns the minimizer of the function
"""

import numpy as np
np.set_printoptions(formatter={'float': lambda x: "{0:0.3f}".format(x)});

def pivoting(tabl, p, q):
    # pivoting about (p,q)-th element
    tabl_new = tabl - tabl[p] * (tabl[:, [q]] / tabl[p, [q]]);
    tabl_new[p] = tabl[p] / tabl[p, [q]];

    return tabl_new;

def revised_simplex_algorithm(A, b, C, indx_cols, options):

    b_print = options['b_print'];
    b_blands_rule = options['b_blands_rule'];
    epsilon = -10e-6; # workaround for -0 issues in while loop

    m, n = A.shape;
    Binv = np.linalg.inv(A[:, indx_cols]); # inverse matrix of basic columns of A
    A = Binv @ A;
    b = Binv @ b;
    cB = C[indx_cols]; # vector of cost coefficients for basic cols of A
    r = C - A.T @ cB; # vector of relative cost coefficients
    cost = cB.T @ b; # cost function
    y0 = b;

    if (b_print):
        # Forming initial revised canonical tableau
        tabl = np.concatenate((np.array(indx_cols).reshape((m, 1)), Binv, y0), axis = 1);
        print(' ');
        print('Initial revised canonical Tableau [indx_cols inv(B) y0]');
        print(tabl);

    while (np.sum(r >= epsilon * np.ones((n, 1))) != n):

        if (b_blands_rule):
            q = 0;
            while(r[q] >= 0):
                q += 1;
        else:
            q = np.argmin(r); # col #q comes into basis

        yq = Binv @ A[:, [q]];

        if (np.sum((yq > 0)) == 0):
            print('Problem unbounded, exit..');
            break;

        a = yq / y0;
        p = np.argmax(a); # col #p leaves basis

        # pivoting about (p,q)-th element
        tabl_aug = np.concatenate((Binv, y0, yq), axis = 1);
        tabl_aug = pivoting(tabl_aug, p, m + 1);
        Binv = tabl_aug[:, 0:m];
        y0 = tabl_aug[:, [m]];
        indx_cols[p] = q;
        cB = C[indx_cols];
        r = C - ((cB.T @ Binv) @ A).T; # vector of relative cost coefficients
        cost = cB.T @ b; # cost function

        if (b_print):
            # Forming updated revised canonical tableau
            tabl = np.concatenate((np.array(indx_cols).reshape((m, 1)), Binv, y0), axis = 1);
            print(' ');
            print('Pivoting about (%d, %d)-th element' % (p, q));
            print(' ');
            print('Updated revised canonical Tableau [indx_cols inv(B) y0]');
            print(tabl);

    X = np.zeros((n, 1));
    X[indx_cols] = y0;
    cost = C.T @ X; # cost function

    if (b_print):
        print(' ');
        print('f(X) = %0.3f' % (cost));
        print('Solution:');
        print(X);

    return (X, cost);


import numpy as np

"""
Spheroid
https://en.wikipedia.org/wiki/Spheroid
Spheroid equation
(x ** 2 / rx ** 2) +
(y ** 2 / ry ** 2) +
(z ** 2 / rz ** 2)
= 1
or
(x / rx) ** 2 +
(y / ry) ** 2 +
(z / rz) ** 2
= 1

width = 2*rx
height = 2*ry
depth = 2*rz

Rotation matrix
RX =[[1, 0, 0],
     [0, np.cos(rx), -np.sin(rx)],
     [0, np.sin(rx), np.cos(rx)]]
RY =[[np.cos(ry), 0, np.sin(ry)],
     [0, 1, 0],
     [-np.sin(ry), 0, np.cos(ry)]]
RZ =[[np.cos(rz), -np.sin(rz), 0],
     [np.sin(rz), np.cos(rz), 0],
     [0, 0, 1]]

RX * RY * RZ * [x, y, z]
"""

def spheroid_coordinate(shape, center, radius, rotation=(None, None, None)):
    """
    return coordinates of point in the spheroid.
    rotation on the xy plan around the z axis.
    warning: rotation is not a perfectly verified feature, use at risk.
    """
    x_lim, y_lim, z_lim = np.ogrid[0:float(shape[0]), 0:float(shape[1]), 0:float(shape[2])]
    x_org, y_org, z_org = center
    x_rad, y_rad, z_rad = radius
    x, y, z = (x_lim - x_org), (y_lim - y_org), (z_lim - z_org)

    if rotation == (None, None, None):
        d = (x / x_rad) ** 2 + (y / y_rad) ** 2 + (z / z_rad) ** 2
    else:
        r1, r2, r3 = rotation
        d = ((x * np.cos(r2) * np.cos(r3) + z * np.sin(r2) - y * np.cos(r2) * np.sin(r3)) / x_rad) ** 2 \
            + ((-z * np.cos(r2) * np.sin(r1) + x * (np.cos(r3) * np.sin(r1) * np.sin(r2) + np.cos(r1) * np.sin(r3)) + y * (np.cos(r1) * np.cos(r3) - np.sin(r1) * np.sin(r2) * np.sin(r3))) / y_rad) ** 2\
            + ((z * np.cos(r1) * np.cos(r2) + x * (-np.cos(r1) * np.cos(r3) * np.sin(r2) + np.sin(r1) * np.sin(r3)) + y * (np.cos(r3) * np.sin(r1) + np.cos(r1) * np.sin(r2) * np.sin(r3))) / z_rad) ** 2

    return np.nonzero(d < 1)

def spheroid(array, fill, center, radius, rotation=(None, None, None)):
    """
    return the given array with a filled spheroid.
    warning: using the returned value is not mandatory.
             the array is passed by reference, if you want to avoid modification on the original array, use np.copy
    """
    array[spheroid_coordinate(array.shape, center, radius, rotation)] = fill
    return array

def spheroid_array(radius):
    """
    return a numpy array containing a spheroid filled with 1.0.
    not to be used, but easiest way to understand the equation.
    """

    rx, ry, rz = radius

    shape = np.zeros((2*rx, 2*ry, 2*rz), dtype=np.float32)

    for x_ in range(shape.shape[0]):
        for y_ in range(shape.shape[1]):
            for z_ in range(shape.shape[2]):
                x = x_ - rx
                y = y_ - ry
                z = z_ - rz

                if (x / rx) ** 2 + (y / ry) ** 2 + (z / rz) ** 2 <= 1:
                    shape[x_, y_, z_] = 1.0

    return shape


# -*- coding: utf-8 -*-
"""
Created on Tue Oct 23 17:36:35 2018

@author: ark4d
"""
import numpy as np

class Probability():
    '''
    This class contains methods to work with probability
    '''
    @staticmethod
    def get_distr(data, un_values=[]):
        '''
        Creates array with probability of each values that to occur
        Parameters
        ----------
        data : numpy-array(int)
            data probability of element which will be tested
        un_values : array-like(int), optional
            unique values probability of which will be created
        Returns
        ----------
        numpy-array(float)
            chace of occurence
        '''
        if len(un_values)==0:
            un_values=np.unique(data)
        values, count = [],[]
        for value in un_values:
            values.append(value)
            count.append(len(data[data==value]))
        dist = np.array([values, np.array(count)/data.shape[0]])
        return dist

    @staticmethod
    def dist_table(data1, data2):
        '''
        Create table that represent joint probability of two distributions

        Parameters
        ----------
        data1 : numpy-array (int)
            data
        data2 : numpy-array (int)
            data

        Returns
        ----------
        list
            unique values of data1
        list
            unique values of data2
        numpy matrix
            joint probability of two distribution

        Raises
        ----------
        ValueError
            if `data1` shape is not equal to `data2` shape
        '''
        if data1.shape == data2.shape:
            size = data1.shape[0]
            un_x_values = np.unique(data1)
            un_y_values = np.unique(data2)
            table = np.zeros((un_x_values.shape[0], un_y_values.shape[0]))
            for row in np.transpose(np.array([data1,data2])):
                table[
                    np.where(un_x_values==row[0]),
                    np.where(un_y_values==row[1])
                ]+=1
            return un_x_values, un_y_values, table/size
        else:
            raise ValueError('{} != {}'.format(data1.shape, data2.shape))

class Itheory():
    @staticmethod
#    note: if you were to check this function with the function in scipy.stats
#    you will get different values# because scipy calculates entropy with base
#    e instead of 2. The values is different, but they showing same thing. you
#    can say that the function in scipy return decibels of entropy whereas
#    the function beneath returns bits (shannons)
#    of entropy.
    def entropy(pmf):
        '''
        Calculates entropy

        Parameters
        ----------
        pmf : numpy-array (float)
            Probabilities of each element to occur

        Retuns
        ----------
        float
            entropy
        '''
        entrp = -(pmf*np.nan_to_num(np.log2(pmf),0)).sum()
        return entrp

    @staticmethod
    def joint_entropy(table):
        '''
        Calculates joint entropy of 2 distributions

        Parameters
        ----------
        table : numpy matrix
            Joint probability distribution matrix (can be generated
            by `dist_table` function)

        Returns
        ----------
        float
            joint entropy
        '''
        j_ent = np.nan_to_num(table*np.log2(table),0).sum()
        return -j_ent

    @staticmethod
    def conditional_entropy(table, axis_marginal):
        '''
        Calculates conditional entropy of 2 distributions

        Parameters
        ----------
        table : numpy matrix
            Joint probability distribution matrix (can be generated
            by `dist_table` function)

        axis_marginal : int, 1 or 0
            Determines given which data entropy will be calculater
            0 - H(`data1`|`data2`)
            1 - H(`data2`|`data1`)
            look in `dist_table` for definition of `data1` and `data2`
        Returns
        ----------
        float
            conditional entropy
        '''
        #informal: relatively hard function to comprehend.
        marg = np.apply_along_axis(lambda x: x.sum(),axis_marginal,table)
        table=np.apply_along_axis(lambda x: x*marg**-1,1-axis_marginal,table)
        table=np.apply_along_axis(lambda x: Itheory.entropy(x),axis_marginal,table)
        table = np.nan_to_num(table,0)
        c_ent = np.matmul(marg, table)
        return c_ent

    @staticmethod
    def rel_entropy(prob_dist1, prob_dist2):
        '''
        Calculates relative entropy

        Parameters
        ----------
        prob_dist1 : numpy-array
            marginal probability of a distribution
        prob_dist2 : numpy-array
            marginal probability of a distribution

        Returns
        ----------
        float
            relative entropy
        '''
        div = prob_dist1/prob_dist2
        if np.isinf(div).any():
            return np.inf
        else:
            log = np.log2(div)
            log[np.isinf(log)]=0
            log[np.isnan(log)]=0
            rel_entropy = np.sum(prob_dist1*log)
            return rel_entropy

    @staticmethod
    def mutual_information(prob_dist1, prob_dist2, table):
        '''
        Calculates mutual information between 2 distributions

        Parameters
        ----------
        prob_dist1 : numpy-array
            marginal probability of a distribution
        prob_dist2 : numpy-array
            marginal probability of a distribtuion
        table: numpy-matrix
            Joint probability distribution matrix (can be generated
            by `dist_table` function)

        Returns
        ----------
        float
            Mutual information
        '''
        div = table/(prob_dist1.reshape(-1,1)* prob_dist2)
        if np.isinf(div).any():
            return np.inf
        else:
            log = np.log2(div)
            log[np.isinf(log)]=0
            log[np.isnan(log)]=0
            mutual_inf = np.sum(table*log)
            return mutual_inf

# TO-DO: Add docs to utility.
class Utility():

    @staticmethod
    def make_ax_look_good(ax):
        '''
        Makes axes look fancy :)

        Parameters
        ----------
        ax : matplotlib axes
        '''
        ax.grid(alpha=0.2)
        for spine in ax.spines:
                ax.spines[spine].set_visible(False)

    @staticmethod
    def less_ent_d(s, step_m = 2, low_boundary = 4, times =2 ):
        '''
        Changes distribution such that the entropy descends.
        '''
        mult = 2
        s_m = s.mean().round().astype('int')
        vals = np.array(np.unique(s,return_counts=True))
        vals_gtlw = (vals[0,vals[1,:]>low_boundary])
        vals_gtlw = vals_gtlw[(vals_gtlw<s_m) | (vals_gtlw>s_m)]
        np.set_printoptions(threshold=np.nan)
        for i in range(0,times):
            for indx, val in enumerate(vals_gtlw[vals_gtlw<s_m][::-1]):
                step_arr=np.zeros(shape=(len(s[s==val]),),dtype='int')
                step_arr[:int(len(s[s==val])/3)]=+step_m
                s[s==val]+=step_arr
                mult+=1
            mult=2
            for indx, val in enumerate(vals_gtlw[vals_gtlw>s_m]):
                step_arr=np.zeros(shape=(len(s[s==val]),),dtype='int')
                step_arr[:int(len(s[s==val])/3)]=-step_m
                s[s==val]+=step_arr
                mult+=1

        return s

    @staticmethod
    def less_ent(dist, multiplier=0.01,steps=2, low_border=0.004):
        '''
        Chages data such that the entropy descends.
        '''
        size = len(dist)
        s_indx = np.argmax(dist!=0)
        e_indx = np.argmax(np.flip(dist)!=0)
        m_indx_d = np.round(((size-(s_indx+e_indx))/2)).astype('int')
        m_indx = m_indx_d+s_indx
#        print(s_indx, -e_indx, m_indx)
        for step in range(0,steps):
            for i in range(s_indx,m_indx):
                distance = np.abs(m_indx - i)
                distance_coef = distance/m_indx_d
                buff = dist[i]*distance_coef* multiplier
                if (dist[i]-buff)<(low_border) and dist[i]>=low_border:
                    buff = (dist[i]-low_border)
                dist[i], dist[i+1] = dist[i]-buff, dist[i+1]+buff

            for i in range(size-1-e_indx, m_indx,-1):
                distance = np.abs(m_indx - i)
                distance_coef = distance/m_indx_d
                buff = dist[i]*distance_coef* multiplier
                if (dist[i]-buff)<(low_border) and dist[i]>=low_border:
                    buff = (dist[i]-low_border)
                dist[i], dist[i-1] = dist[i]-buff, dist[i-1]+buff
        return dist

    #this function shuffles values of !probability distribution!
    #dist: a probability distribution
    #times: greater - more shufflier
    #multiplier: greater - more shufflier
    @staticmethod
    def shuffle_dist(dist, times=10, multiplier=0.4):
        '''
        Changes values of distribtuions. Depends on variables that are passed.

        Parameters
        ----------
        dist : nparray
            distribution of a random variable
        times : int
            amount of times for the process to repeat
        multiplier : float
            determines how much the function will change the distribution on a iteration

        Returns
        ----------
        dist : nparray
            changed distribtuion
        '''
        deck = dist[dist!=0]
        start_index = np.argmax(dist!=0)
        end_index = start_index+deck.shape[0]
        buff=0
        for i in range(0, times):
            for i in range(start_index, end_index):
                if np.random.choice([True,False]):
                    buff, dist[i] = dist[i]*multiplier, (dist[i]-dist[i]*multiplier)+buff
            dist[np.random.randint(start_index, end_index)]+=buff
            buff=0
        return dist

    @staticmethod
    def _decide(chance):
        if np.random.rand()<chance:
            return True
        else:
            return False

    @staticmethod
    def shuffle_data(data, chance = 0.5, times=10, count_low_boundary=1, count_max_boundary=40):
        '''
        Changes data by changing a value to another value. Another value can be picked from range of
        unique values that are presented in the data.
        '''
        uni,vals = np.unique(data,return_counts = True)
        for j in range(0, times):
            for i in range(data.shape[0]):
                if (vals[np.where(uni==data[i])]>count_low_boundary)and(vals[np.where(uni==data[i])]<count_max_boundary):
                    if Utility._decide(chance):
                        n_i = np.random.randint(low=0,high=data.shape[0])
                        vals[np.where(uni==data[i])]-=1
                        data[i]=data[n_i]
                        vals[np.where(uni==data[n_i])]+=1
                else:
                    pass
        return data


# import math
# import numpy as np

def lagrange(f, xs, x):
    ys = [f(i) for i in xs]
    n = len(xs)
    y = 0
    for k in range(0, n):
        t = 1
        for j in range(0, n):
            if j != k:
                s = (x - xs[j]) / (xs[k] - xs[j])
                t = t * s
        y = y + t * ys[k]

    return y

def difference_quotient(f, xs):
    res = 0
    n = len(xs)
    for k in range(n):
        t = 1
        for j in range(n):
            if j != k:
                t *= (xs[k] - xs[j])
        res += f(xs[k]) / t

    return res

def newtown(f, xs, x):
    n = len(xs)
    y = f(xs[0])
    for k in range(1, n):
        t = difference_quotient(f, xs[:k + 1])
        for j in range(k):
            t *= (x - xs[j])
        y += t

    return y

def piecewise_linear(f, xs, x):
    interval = [0, 1]
    if x < xs[0]:
        interval = [xs[0], xs[1]]
    elif x > xs[-1]:
        interval = [xs[-2], xs[-1]]
    else:
        for i in range(len(xs) - 1):
            if x >= xs[i] and x <= xs[i + 1]:
                interval = [xs[i], xs[i + 1]]
                break

    return lagrange(f, interval, x)

# print(lagrange(lambda x: 1 / (1 + x * x), np.linspace(-5, 5, 11), 0.5))
# print(lagrange(lambda x: 1 / (1 + x * x), np.linspace(-5, 5, 11), 4.5))
# print(piecewise_linear(lambda x: 1 / (1 + x * x), np.linspace(-5, 5, 11), 0.5))
# print(piecewise_linear(lambda x: 1 / (1 + x * x), np.linspace(-5, 5, 11), 4.5))

# print(
#     difference_quotient(lambda x: x * x * x - 4 * x, (np.arange(5))[1:]),
#     difference_quotient(lambda x: x * x * x - 4 * x, [1, 2]),
#     difference_quotient(lambda x: x * x * x - 4 * x, [1, 2, 3])
# )

# print(newtown(lambda x: x * x * x - 4 * x, (np.arange(5))[1:], 5))
# print(newtown(lambda x: math.sqrt(x), [1, 4, 9], 5))


import numpy as np
import math
from lmfit import Model

def calculate_HHCF(image , image_width):
    """
     Calculates the height-height correlation function for an image. The HHCF is calculated along the rows of the image.

     An Open Access reference for calculating HHCF from an image:
        "Height-Height Correlation Function to Determine Grain Size in Iron Phthalocyanine Thin Films", Gredig, T; Silverstein, E; Byrne, M. Journal of Physics:Conference Series. (2013). DOI: 10.1088/1742-6596/417/1/012069

     Inputs:
        image: numpy array. Contains the height image.
        image_width: float. Specifies the physical width corresponding to the image frame expressed in meters. (E.g. a 200 nm wide scan frame should be inputted as 1e-9.)

    Outputs:
        distances: numpy array. Contains the distances values from the reference pixel corresponding to each value in the HHCF array. This is the X data for plotting in a HHCF plot.
        HHCF: numpy array. Contains the calculated height-height correlation function data.
    """
    # Initialize parameters and an array for data storage.
    num_columns = np.shape(image)[0]
    HHCF = np.zeros(num_columns)

    # Perform calculation of HHCF. This works by finding the difference in value between pairs of pixels separated by a set number of pixels (corresponding to a physical distance).
    # This is a semi-vectorized implementation of the calculation. The idea is to effectively have two copies of the image stacked atop one another. Then the top image is shifted to the left relative to the bottom image by some number of pixels (corresponding to a certain physical difference).
    # Then the two copies of the image are 'cropped' to contain only the portions of the images which overlap with their shifted counterparts, discarding any pixels which do not have a pair.
    # Loop over all allowable (non-zero) pixel-wise separation distances.
    for separation_distance in range(1,num_columns):
        # Slice the image to generate the 'bottom' copy of the image as described above, discarding pixels on the right side, since they won't have pairs in the 'top' copy of the image.
        array1 = image[: , 0:-separation_distance]
        # Slice the image to generate the 'top' copy of the image as described above, discarding pixels on the left side, since they won't have pairs in the 'bottom' copy of the image.
        array2 = image[: , separation_distance:]
        # Calculate the HHCF value for each pixel pair in the overlapping regions of the 'image stack', then take the average to represent the HHCF for the current pixel-wise separation distance.
        HHCF[separation_distance] = np.average(np.square(array2-array1))

    # Determing the physical length corresponding to a single pixel.
    pixel_width = image_width/(num_columns - 1)

    # Generate an array of physical separation distances corresponding to the pixel-wise separation distances used to calculate the HHCF.
    distances = np.arange(num_columns) * pixel_width

    return [distances , HHCF]

def HHCF(r, sigma, xi , alpha):
    """
    Model for height-height correlation function. This model is suitable for fitting when data is present for both above and below the correlation length.

    Inputs:
        r: Numpy array. This contains the distance data which would be used for the x coordinate on a HHCF plot.
        sigma: float. This is related to the RMS roughness of the sample.
        xi: float. This is the correlation length of the sample.
        alpha: float. This is the Hurst parameter.

    Outputs:
        The output of this function is the height-height correlation function for the given distances and parameters.
    """
    return 2 * sigma**2 * (1 - np.exp(- (r/xi)**(2 * alpha)))

def fit_HHCF(HHCF_data , parameter_guesses = None):
    """
    Fit an analytic model for height-height correlation function to experimental HHCF data. This function assumes the HHCF data includes length scales both above and below the correlation length.

    The analytic model is mentioned in the following Open Source article:
        "Height-Height Correlation Function to Determine Grain Size in Iron Phthalocyanine Thin Films", Gredig, T; Silverstein, E; Byrne, M. Journal of Physics:Conference Series. (2013). DOI: 10.1088/1742-6596/417/1/012069

    Inputs:
        HHCF_data: Numpy array. Contains two columns, with the first column being the distance data and the second being the HHCF calculated from an experimental image.
        parameter_guesses Dict. This is an optional input containing guesses for the model parameters. If it is provided, it must have key:value pairs for the following parameters: sigma, xi, alpha.
    """
    # Generate initial guesses of model parameters for fitting if none are provided.
    if parameter_guesses == None:
        parameter_guesses = {'sigma':math.sqrt(np.max(HHCF_data[1])/2)}
        index_of_max = np.argmax(HHCF_data[1])
        parameter_guesses['xi'] = HHCF_data[0][int(index_of_max/4)]
        parameter_guesses['alpha'] = 0.5

    # Create a model object to fit to the HHCF data and assign initial guesses for the fitting parameters.
    fitting_model = Model(HHCF)
    params = fitting_model.make_params(sigma = parameter_guesses['sigma'], xi = parameter_guesses['xi'], alpha = parameter_guesses['alpha'])

    # Try to fit the model to the data.
    try:
        result = fitting_model.fit(HHCF_data[1], params, r = HHCF_data[0])
        return result
    except:
        print('There was an issue processing the data.')

def HHCF_short_range(r, A, alpha):
    """
    Model for height-height correlation function. This model is suitable for fitting when data is present only below the correlation length.

    Inputs:
        r: Numpy array. This contains the distance data which would be used for the x coordinate on a HHCF plot.
        A: float. Scaling constant used in the fitting process.
        alpha: float. This is the Hurst parameter.

    Outputs:
        The output of this function is the height-height correlation function for the given distances and parameters.
    """
    return A * np.power(r , 2 * alpha)

    ####### The function below still needs work!
def fit_HHCF_short_range(HHCF_data , parameter_guesses = None):
    """
    Fit an analytic model for height-height correlation function to experimental HHCF data. This function assumes the HHCF data includes only length scales below the correlation length.

    The analytic model is mentioned in the following Open Source article:
        "Height-Height Correlation Function to Determine Grain Size in Iron Phthalocyanine Thin Films", Gredig, T; Silverstein, E; Byrne, M. Journal of Physics:Conference Series. (2013). DOI: 10.1088/1742-6596/417/1/012069

    Inputs:
        HHCF_data: Numpy array. Contains two columns, with the first column being the distance data and the second being the HHCF calculated from an experimental image.
        parameter_guesses: Dict. This is an optional input containing guesses for the model parameters. If it is provided, it must have key:value pairs for the following parameters: A, alpha.
    """
    # Generate initial guesses of model parameters for fitting if none are provided.
    if parameter_guesses == None:
        # Guess the parameter values by assuming that the data will be linear on a log-log plot and using a linear fit.
        mid = int( len(HHCF_data) / 2)
        # Calculate the log of two data points near the middle of the data array.
        x = np.log( HHCF_data[0][mid : mid + 5])
        y = np.log( HHCF_data[1][mid : mid + 5] )
        # Perform a 'linear regression' according to: log(HHCF) = log(A) + 2 * alpha * log(r)
        alpha_guess = 0.5 * (y[1] - y[0]) / (x[1] - x[0]) # slope
        A_guess = np.exp((y[0]*x[1] - y[1]*x[0]) / (x[1] - x[0])) # y-intercept
        # Store parameter guesses in a dictionary.
        parameter_guesses = {'A': A_guess , 'alpha':alpha_guess}

    # Create a model object to fit to the HHCF data and assign initial guesses for the fitting parameters.
    fitting_model = Model(HHCF_short_range)
    params = fitting_model.make_params(A = parameter_guesses['A'], alpha = parameter_guesses['alpha'])

    # Try to fit the model to the data.
    try:
        result = fitting_model.fit(HHCF_data[1], params, r = HHCF_data[0])
        return result
    except:
        print('There was an issue processing the data.')

def make_histogram(image, num_bins = 100):
    """
    Bin a collection of numeric data for use in histogram plotting or analysis.

    Inputs:
        image: Numpy array. Contains the data to be binned.
        num_bins: int. Specifies the number of bins to use.

    Outputs:
        bin_centers: Float. The center coordinate for the histogram bins.
        counts: Int. The number of data points contained in each bin.
        bin_width: Float. The width of the bins.
    """
    # Group data into desired number of bins.
    counts , bin_edges = np.histogram(image , bins = num_bins)

    # calculate the center values for the bins
    bin_centers = 0.5*(bin_edges[1:] + bin_edges[:-1])

    # calculate the width of the bins.
    bin_width = (bin_edges[-1]-bin_edges[0])/len(counts)
    return bin_centers, counts, bin_width


"""
@author: Abhirup Mishra
@Description: The class contains the methods for pricing options using Black-Scholes Formula
"""

from math import sqrt, pi
import scipy as sp
import scipy.special
import numpy as np

def mass_broadcast(*args):
	'''Cast some list of vectors of shape(a_i, 1) or (a_i,) to the same uniform (l,1)
	'''
	l = 0
	xs = []
	for i, x in enumerate(args):
		xs.append( np.asarray(x).reshape(-1,1) )
		l = max(l, xs[i].shape[0])

	for i, x in enumerate(xs):
		xs[i] = np.broadcast_to(x, (l, 1))

	return xs

class IncorrectInputs(Exception): pass

class TimeoutException(Exception): pass

class BlackScholes:

	#the constructor for the class
	def __init__(self, spot_price, strike_price, time_to_maturity, interest_rate, sigma, dividend_yield=0):

		''' initializtion parameters '''

		[
			self.spot_price,
			self.strike_price,
			self.time_to_maturity,
			self.interest_rate,
			self.sigma,
			self.dividend_yield
		] = mass_broadcast(
			spot_price, strike_price, time_to_maturity, interest_rate, sigma, dividend_yield
		)

		# self.spot_price = np.asarray(spot_price).astype(float)
		#
		# #checking if strike price is an array or not
		# if(not(hasattr(strike_price, "__len__"))):
		# 	self.strike_price = np.ones(len(spot_price))*strike_price
		# else:
		# 	self.strike_price = strike_price

		# #checking if time to maturity is an array or not
		# if(not(hasattr(time_to_maturity, "__len__"))):
		# 	self.time_to_maturity = np.ones(len(spot_price))*time_to_maturity
		# else:
		# 	self.time_to_maturity = time_to_maturity

		# #checking if interest rate is an array or not
		# if(not(hasattr(interest_rate, "__len__"))):
		# 	self.interest_rate = np.ones(len(spot_price))*interest_rate
		# else:
		# 	self.interest_rate = interest_rate

		# #checking if volatility is an array or not
		# if(not(hasattr(sigma, "__len__"))):
		# 	self.sigma = np.ones(len(spot_price))*sigma
		# else:
		# 	self.sigma = sigma
		#
		# #checking if dividend yield is an array or not
		# if(not(hasattr(dividend_yield, "__len__"))):
		# 	self.dividend_yield = np.ones(len(spot_price))*dividend_yield
		# else:
		# 	self.dividend_yield = dividend_yield

	#private method for erf function
	def bls_erf_value(self,input_number):
		erf_out = 0.5*(1 + sp.special.erf(input_number/sqrt(2.0)))
		return erf_out

	#vectorized method to price call option
	def european_option_price(self):

		"Price of the call option"
		"the vectorized method can compute price of multiple options in array"
		numerator = sp.add(
			sp.log(
				sp.divide(
					self.spot_price,
					self.strike_price,
				)
			),
			sp.multiply(
				(
					self.interest_rate - self.dividend_yield +
					0.5*sp.power(self.sigma,2)
				),
				self.time_to_maturity)
		)
		d1 = sp.divide(
			numerator,
			sp.prod(
				[
					self.sigma,
					sp.sqrt(self.time_to_maturity)
				],
				axis=0,
			)
		)
		d2 = sp.add(
			d1,
			-sp.multiply(
				self.sigma,
				sp.sqrt(self.time_to_maturity)
			)
		)

		ecall = sp.product(
			[
				self.spot_price,
				self.bls_erf_value(d1),
				sp.exp(sp.multiply(-self.dividend_yield,self.time_to_maturity))
			],
			axis=0
		) - sp.product(
			[
				self.strike_price,
				self.bls_erf_value(d2),
				sp.exp(
					-sp.multiply(
						self.interest_rate,
						self.time_to_maturity,
					)
				)
			],
			axis=0,
		)

		eput = sp.product(
			[
				-self.spot_price,
				self.bls_erf_value(-d1),
				sp.exp(
					sp.multiply(
						-self.dividend_yield,
						self.time_to_maturity
					)
				)
			],
			axis=0,
		) + sp.product(
			[
				self.strike_price,
				self.bls_erf_value(-d2),
				sp.exp(
					-sp.multiply(
						self.interest_rate,
						self.time_to_maturity
					)
				)
			],
			axis=0,
		)
		return ecall, eput

	#delta of the option
	def european_option_delta(self):
		numerator = sp.add(
			sp.log(
				sp.divide(
					self.spot_price,
					self.strike_price
				)
			),
			sp.multiply(
				( self.interest_rate - self.dividend_yield + 0.5*sp.power(self.sigma,2)),
				self.time_to_maturity
			)
		)
		d1 = sp.divide(
			numerator,
			sp.prod(
				[
					self.sigma,
					sp.sqrt(self.time_to_maturity)
				],
				axis=0,
			)
		)
		call_delta = self.bls_erf_value(d1)
		put_delta = call_delta - 1

		return call_delta, put_delta

	#gamma of the option (under construction)
	def european_option_gamma(self):
		pass

	#vega of the option
	def european_option_vega(self):
		numerator = sp.add(
			sp.log(
				sp.divide(
					self.spot_price,
					self.strike_price
				)
			),
			sp.multiply(
				( self.interest_rate - self.dividend_yield + 0.5*sp.power(self.sigma,2)),
				self.time_to_maturity
			)
		)
		d1 = sp.divide(
			numerator,
			sp.prod(
				[
					self.sigma,
					sp.sqrt(self.time_to_maturity)
				],
				axis=0,
			)
		)

		val = sp.multiply(
			sp.multiply(
				self.spot_price,
				sp.exp(
					-sp.multiply(
						self.dividend_yield,
						self.time_to_maturity
					)
				)
			),
			sp.exp(-sp.square(d1)*0.5)
		)
		val = sp.multiply(
			val,
			sp.sqrt(self.time_to_maturity)
		)
		vega = (1/sqrt(2*pi))*val

		return vega

	#theta of the option
	def european_option_theta(self):
		pass

	#rho of the option
	def european_option_rho(self):
		"Price of the call option"
		"the vectorized method can compute price of multiple options in array"
		numerator = sp.add(
			sp.log(
				sp.divide(
					self.spot_price,
					self.strike_price,
				)
			),
			sp.multiply(
				(
					self.interest_rate - self.dividend_yield +
					0.5*sp.power(self.sigma,2)
				),
				self.time_to_maturity)
		)
		d1 = sp.divide(
			numerator,
			sp.prod(
				[
					self.sigma,
					sp.sqrt(self.time_to_maturity)
				],
				axis=0,
			)
		)
		d2 = sp.add(
			d1,
			-sp.multiply(
				self.sigma,
				sp.sqrt(self.time_to_maturity)
			)
		)

		j = sp.product(
			[
				self.spot_price,
				self.time_to_maturity,
				sp.exp(
					sp.multiply(
						-self.interest_rate,
						self.time_to_maturity
					)
				),
			],
			axis=0
		)

		c_rho = j * self.bls_erf_value(d2)
		p_rho = -j * self.bls_erf_value(-d2)

		return c_rho, p_rho


"""
Libary of linear regressions.
"""
import numpy as np
from scipy.stats import linregress
from astroML.linear_model import TLS_logL
from scipy.odr import Model, Data, RealData, ODR
import scipy.optimize as op
import emcee

def mcmc_linear_model(x, y, xerr, yerr, nwalkers=100, nruns=2000,
                      cut=100):
    """
    This was built using the following references:

    * http://dan.iel.fm/emcee/current/user/line/
    * http://www.astroml.org/book_figures/chapter8/fig_total_least_squares.html
    * https://github.com/astroML/astroML/blob/master/astroML/linear_model/TLS.py

    The linear models is $y=a*x+b$

    Returns (a,b), ((a_lower, a_upper), (b_lower, b_upper))
    """

    # translate between typical slope-intercept representation,
    # and the normal vector representation

    def get_a_b(theta):
        b = np.dot(theta, theta) / theta[1]
        a = -theta[0] / theta[1]

        return a, b

    def get_beta(a, b):
        denom = (1 + a * a)
        return np.array([-b * a / denom, b / denom])

    # Define the log-maximum likelihood
    def lnlike(theta, x, y, xerr, yerr):
        arr = np.column_stack((x, y))
        arr_err = np.column_stack((xerr, yerr))

        return TLS_logL(theta, arr, arr_err**2)

    # Get a first extimative of the parameters
    # based on the maximum likelihood
    nll = lambda *args: -lnlike(*args)
    x0 = get_beta(*linregress(x, y)[:2]) # Initial guesses from
                                         # Ordinary Least Squares
    result = op.minimize(nll, x0=x0, args=(x, y, xerr, yerr),
                         method='Nelder-Mead')
    theta = result["x"]

    # Define the log-prior
    def lnprior(theta):
        a, b = get_a_b(theta)
        if -5.0 < a < 0.5 and 0.0 < b < 10.0:
            return 0.0
        return -np.inf

    # Define the full log-probability function
    def lnprob(theta, x, y, xerr, yerr):
        lp = lnprior(theta)
        if not np.isfinite(lp):
            return -np.inf
        return lp + lnlike(theta, x, y, xerr, yerr)

    # Set the MCMC walkers
    ndim = 2
    pos = [theta + 1e-4*np.random.randn(ndim) for i in range(nwalkers)]

    # Set and Run
    sampler = emcee.EnsembleSampler(nwalkers, ndim, lnprob, args=(x, y, xerr, yerr))
    sampler.run_mcmc(pos, nruns)

    # Get the samples. Do not get the first `cut` ones.
    samples = sampler.chain[:, cut:, :].reshape((-1, ndim))
    coeffs = np.array([get_a_b(bt) for bt in samples])

    # Get the a, b and their errors
    # The value of a and are the median of the
    # sampling distribution, while the lower and
    # upper values are the 16% and 84% percentiles.

    a_mcmc, b_mcmc = map(lambda v: (v[1], v[2]-v[1], v[1]-v[0]),
                         zip(*np.percentile(coeffs, [16, 50, 84], axis=0)))

    return (a_mcmc[0], b_mcmc[0]), ((a_mcmc[1], a_mcmc[2]),
                                    (b_mcmc[1], b_mcmc[2]))

def odr_linear_fit(x, y, xerr, yerr):
    """
    Obtained from Scipy ODR webpage with small modifications

    https://docs.scipy.org/doc/scipy/reference/odr.html
    """
    def f(B, x):
        '''Linear function y = m*x + b'''
        # B is a vector of the parameters.
        # x is an array of the current x values.
        # x is in the same format as the x passed to Data or RealData.
        #
        # Return an array in the same format as y passed to Data or RealData.
        return B[0]*x + B[1]

    # Create a model
    linear = Model(f)

    # Create a Data instance

    mydata = Data(x, y, wd=1./np.power(xerr,2), we=1./np.power(yerr,2))

    #Instantiate ODR with your data, model and initial parameter estimate.:
    beta0 = linregress(x, y)[:2]
    myodr = ODR(mydata, linear, beta0=beta0)

    #Run the fit.:
    myoutput = myodr.run()

    a, b = myoutput.beta
    a_error, b_error = myoutput.sd_beta

    return a, b, a_error, b_error


import matplotlib
matplotlib.use("TKagg")
import matplotlib.pyplot as plt
import seaborn as sns; sns.set()
import numpy as np

#Simple Linear Regression
#y =  ax + b

rng = np.random.RandomState(1)
x = 10 * rng.rand(50)
y = 2 * x - 5 + rng.randn(50)
plt.scatter(x, y);
plt.show()
plt.clf()

from sklearn.linear_model import LinearRegression
model = LinearRegression(fit_intercept=True)

model.fit(x[:, np.newaxis], y)

xfit = np.linspace(0,10,1000)
yfit = model.predict(xfit[:, np.newaxis])

plt.scatter(x,y)
plt.plot(xfit,yfit)
plt.show()
plt.clf()

#relvent info for the line
print("Model slope:    ", model.coef_[0])
print("Model intercept:", model.intercept_)

#we can do much more

rng = np.random.RandomState(1)
X = 10 * rng.rand(100, 3)
y = 0.5 + np.dot(X, [1.5, -2., 1.])

model.fit(X,y)
print(model.intercept_)
print(model.coef_)

#Basic Function Regression
##Polynomial basis functions

from sklearn.preprocessing import PolynomialFeatures
x = np.array([2,3,4])
poly = PolynomialFeatures(3, include_bias=False)
print(poly.fit_transform(x[:, None]))

from sklearn.pipeline import make_pipeline
poly_model = make_pipeline(PolynomialFeatures(7), LinearRegression())
#make a 7degree polynomial

#create a noisy sine wave
rng = np.random.RandomState(1)
x = 10 * rng.rand(50)
y = np.sin(x) + 0.1 * rng.randn(50)

poly_model.fit(x[:, np.newaxis], y)
yfit = poly_model.predict(xfit[:, np.newaxis])

plt.scatter(x, y)
plt.plot(xfit, yfit);
plt.show()
plt.clf()

#Example: predicting bicycle traffic

import pandas as pd
counts = pd.read_csv('data/FremontBridge.csv', index_col='Date', parse_dates=True)
weather = pd.read_csv('data/BicycleWeather.csv', index_col='DATE',
                      parse_dates=True)
#compute total daily traffic
daily = counts.resample('d').sum()
daily['Total'] = daily.sum(axis=1)
daily = daily[['Total']] # cut other columns
#add binary columns for days of week
days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
for i in range(7):
    daily[days[i]] = (daily.index.dayofweek == i).astype(float)

#account for holidays
from pandas.tseries.holiday import USFederalHolidayCalendar
cal = USFederalHolidayCalendar()
holidays = cal.holidays('2012', '2016')
daily = daily.join(pd.Series(1, index=holidays, name='holiday'))
daily['holiday'].fillna(0, inplace=True)

# add hours of daylight
def hours_of_daylight(date, axis=23.44, latitude=47.61):
    """Compute the hours of daylight for the given date"""
    days = (date - pd.datetime(2000, 12, 21)).days
    m = (1. - np.tan(np.radians(latitude))
         * np.tan(np.radians(axis) * np.cos(days * 2 * np.pi / 365.25)))
    return 24. * np.degrees(np.arccos(1 - np.clip(m, 0, 2))) / 180.

daily['daylight_hrs'] = list(map(hours_of_daylight, daily.index))
daily[['daylight_hrs']].plot()
plt.ylim(8, 17)
plt.show()
plt.clf()
#add temp and precip

# temperatures are in 1/10 deg C; convert to C
weather['TMIN'] /= 10
weather['TMAX'] /= 10
weather['Temp (C)'] = 0.5 * (weather['TMIN'] + weather['TMAX'])

# precip is in 1/10 mm; convert to inches
weather['PRCP'] /= 254
weather['dry day'] = (weather['PRCP'] == 0).astype(int)

daily = daily.join(weather[['PRCP', 'Temp (C)', 'dry day']])

#add year counter
daily['annual'] = (daily.index - daily.index[0]).days / 365.

print(daily.head())

#drop null rows
daily.dropna(axis=0, how='any', inplace=True)

column_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun', 'holiday',
                'daylight_hrs', 'PRCP', 'dry day', 'Temp (C)', 'annual']
X = daily[column_names]
y = daily['Total']

model = LinearRegression(fit_intercept=False)
model.fit(X,y)
daily['predicted'] = model.predict(X)
daily[['Total', 'predicted']].plot(alpha=0.5);
plt.show()
plt.clf()

params = pd.Series(model.coef_, index=X.columns)

print(params)

from sklearn.utils import resample
np.random.seed(1)
err = np.std([model.fit(*resample(X,y)).coef_
              for i in range(1000)], 0)
print(pd.DataFrame({'effect': params.round(0),
                    'error': err.round(0)}))


import numpy as np
import matplotlib.pyplot as plt
from scipy.interpolate import UnivariateSpline

if __name__ == '__main__':
    N = 200
    step = 10
    NPulses = 6

    #Step 1: Generate a random 1D function
    np.random.seed(9)
    x = np.arange(0, N, step)
    y = np.random.randn(x.size)
    s = UnivariateSpline(x, y, s=1)
    xs = np.arange(N)
    y = s(xs)

    #Step 2: Generate a random set of impulses
    PIdx = np.random.permutation(N)[0:NPulses]
    idx = np.argsort(PIdx)
    PIdx = np.sort(PIdx)
    PMag = np.abs(np.random.randn(NPulses))
    PMag = PMag[idx]

    #Step 3: Plot Original function and impulse response

    #Step 4: Plot adding one impulse at a time
    R = np.max(np.abs(y))*np.max(np.abs(PMag))
    out = np.zeros(2*N)
    for p in range(len(PMag)):
        out[PIdx[p]:PIdx[p]+N] += PMag[p]*y
        plt.figure(figsize=[16, 10])
        plt.subplot(311)
        plt.plot(y)
        plt.xlim(0, 2*N)
        plt.ylim(-R, R)
        plt.xlabel('Sample Number')
        plt.ylabel('Amplitude')
        plt.title('Original Function')

        plt.subplot(312)
        plt.stem(PIdx, PMag, 'k')
        plt.plot([PIdx[p], PIdx[p]], [-R, R])
        plt.xlim(0, 2*N)
        plt.ylim(-R, R)
        plt.ylabel('Amplitude')
        plt.xlabel('Sample Number')

        plt.subplot(313)
        x = np.arange(len(y))
        for i in range(p+1):
            if (i == p):
                plt.plot(x+PIdx[i], y*PMag[i], 'b', linewidth=4.0)
            else:
                plt.plot(x+PIdx[i], y*PMag[i], 'k', linewidth=1.0)
        plt.plot([PIdx[p], PIdx[p]], [-R, R])
        plt.xlim(0, 2*N)
        plt.ylim(-R, R)
        plt.ylabel('Amplitude')
        plt.xlabel('Sample Number')

        plt.savefig("Conv%i.svg"%p, bbox_inches='tight')

    #Now plot the result
    plt.figure(figsize=[16, 10])
    plt.subplot(311)
    plt.plot(y)
    plt.xlim(0, 2*N)
    plt.ylim(-R, R)
    plt.xlabel('Sample Number')
    plt.ylabel('Amplitude')
    plt.title('Original Function')

    plt.subplot(312)
    plt.stem(PIdx, PMag, 'k')
    plt.xlim(0, 2*N)
    plt.ylim(-R, R)
    plt.ylabel('Amplitude')
    plt.xlabel('Sample Number')

    plt.subplot(313)
    plt.plot(out)
    plt.ylabel('Amplitude')
    plt.xlabel('Sample Number')
    plt.savefig("ConvResult.svg", bbox_inches='tight')

    #Convolution ground truth
#    plt.figure()
#    P = np.zeros(2*N)
#    P[PIdx] = PMag
#    plt.plot(np.convolve(y, P))
#    plt.show()