import array

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn import preprocessing

plt.rcParams['figure.figsize'] = (12.0, 9.0)
#plt.ticklabel_format(style='plain')

class LinearRegression:
    def __init__(self, filepath):
        self.data = pd.read_csv(filepath)

        self.x_name = self.data.columns[0]
        self.y_name = self.data.columns[1]

        self.x = self.data[self.x_name]
        self.y = self.data[self.y_name]

        self.b1 = 0
        self.b0 = 0

        self.iterations = []
        self.costs = []

    # Plots the data, including the linear regression line, if the model has been trained.
    def plot_model_data(self):
        plt.xlabel(self.x_name)
        plt.ylabel(self.y_name)
        plt.suptitle(self.y_name + ' against ' + self.x_name)
        plt.scatter(self.data[self.x_name], self.data[self.y_name], c="black")

        #If model has been trained, plot the line for the linear-regression aswell.
        if self.b1 != 0 and self.b0 != 0:
            pred_y = self.b1 * self.x + self.b0
            plt.plot([min(self.x), max(self.x)], [min(pred_y), max(pred_y)], color='red')  # regression line
        plt.show()

    def plot_loss(self):
        plt.xlabel('Iterations')
        plt.ylabel('Loss')
        plt.suptitle('Loss over iteration')
        plt.plot(self.iterations, self.costs, linewidth=1)
        plt.show()

    def train(self, learning_rate=0.0001, iterations=500):
        n = float(len(self.x))  # Number of samples

        self.b1 = 0  # Resets the values, if one wants to train the model again. The reason for wanting
        self.b0 = 0  # to train a model again can be to change learning rate, amount of iterations, etc.

        # Trains the model
        for i in range(iterations):
            predicted_y = self.b1 * self.x + self.b0

            der_b1 = (-2 / n) * sum(self.x * (self.y - predicted_y))    # Partial derivative of b1.
            der_b0 = (-2 / n) * sum(self.y - predicted_y)               # Partial derivative of b0.

            self.b1 = self.b1 - learning_rate * der_b1
            self.b0 = self.b0 - learning_rate * der_b0

            self.iterations.append(i)
            self.costs.append(self.calc_loss(predicted_y))

        print(self.b1, self.b0)

    def calc_loss(self, pred_y):
        n = len(self.x)
        return (sum((self.b0 + self.b1 * self.x)-pred_y)**2)/2*n

    def predict(self, x):
        if self.b1 != 0 and self.b0 != 0:
            predicted_y = self.b1 * x + self.b0
            print("The predicted value is: " + str(predicted_y))
        else:
            print("Please train the model before attempting to make any predictions!")


# -*- coding: utf-8 -*-
"""
Author: Paschalis Giakoumoglou
Date: Thu Jun 17 23:29:08 2021
"""

import matplotlib.pyplot as plt
import numpy as np
import math
import sympy as symb
from autograd import grad
import autograd

def print_derivatives(degree):
    print("Function and its derivatives:")
    x = symb.Symbol('x')
    y = x*symb.log(x)
    print(f"> Initial function = {y}")
    for i in range(1, degree+1):
        print(f"> Derivative {i} = {y.diff(x, i)}")
    return y

def f(x):
    return x**3

def find_max_grad_f(f, degree, a, b):
    ff =  f
    for _ in range(degree):
        ff = grad(ff)
    x = np.linspace(a,b,1000)
    ffx = [ff(float(i)) for i in x]
    return max(ffx)

def rect_left(f, a, b, N):
    print("Rectangle Left Method")
    h = (b-a)/N
    print(f"a = {a}, b = {b}, N = {N}")
    print(f"h = (b-a)/N = {h}")
    tot = 0
    for i in range(N):
        tot += f(a+i*h)
        print(f"f({a+i*h}) = {f(a+i*h)}")
    print(f"I = h*sum[f(x_i)] = {tot*h}")
    error = h/2*(b-a)*find_max_grad_f(f, 1, a, b)
    print(f"E = h/2*(b-a)*max(f'(x)) = {error}")
    print_derivatives(1+1)
    print()
    #return tot*h

def rect_right(f, a, b, N):
    print("Rectangle Right Method")
    h = (b-a)/N
    tot = 0
    for i in range(N):
        tot += f(a+i*h+h)
        print(f"f({a+i*h}) = {f(a+i*h)}")
    print(f"I = h*sum(f(a+i*h+h)) = {tot*h}")
    print()
    #return tot*h

def trapezoid(f, a, b, N):
    print("Trapezoidal Method")
    h = (b - a) / N
    print(f"a = {a}, b = {b}, N = {N}")
    print(f"h = (b-a)/N = {h}")
    tot = f(a)+f(b)
    print(f"f({a}) = {f(a)}")
    for i in range(1, N):
        tot += 2 * f(a + i * h)
        print(f"f({a+i*h}) = {f(a+i*h)}")
    print(f"f({b}) = {f(b)}")
    print(f"I = (h/2) * [f(x0) + 2f(x1) + 2f(x2) + ... + 2f(xN-1) + f(xN)] = {(h / 2) * tot}")
    error = (h**2)*(b-a)*find_max_grad_f(f,2,a,b)/12
    print(f"E = (h^2/12) * (b-a) * max(f''(x)) = {error}")
    print_derivatives(2+1)
    print()
    #return (h / 2) * tot

def simpson(f, a,b,N):
    print("Simpson's Method")
    print("N must be a multitude of 2")
    h = (b-a)/N
    print(f"a = {a}, b = {b}, 2N = {N}")
    print(f"h = (b-a)/N = {2*h}")
    tot = f(a) + f(b)
    print(f"f({a}) = {f(a)}")
    for i in range(1,N,1):
        tot += f(a+i*h)*(2*(i%2 == 0) + 4*(i%2 == 1))
        print(f"f({a+i*h}) = {f(a+i*h)}")
    print(f"f({b}) = {f(b)}")
    print(f"I = (h/6) * [f(x0) + 4f(x1) + 2f(x2) + 4f(x3) + 2f(x4) + ... + 2f(x2N-2) + 4f(x2N-1) + f(x2N)] = {(h / 3) * tot}")
    error = (h**2) * (b-a) * find_max_grad_f(f,4,a,b) / 180
    print(f"E = ((h/2)^4/180) * (b-a) * max(f''''(x)) = {error}")
    print_derivatives(3+1)
    print()
    #return tot*h/3

def simpson38(f, a,b,N):
    print("Simpson's Method")
    print("N must be a multitude of 3")
    h = (b-a)/N
    print(f"a = {a}, b = {b}, N = {N}")
    print(f"h = (b-a)/N = {h}")
    tot = f(a) + f(b)
    print(f"f({a}) = {f(a)}")
    for i in range(1,N,1):
        tot += f(a+i*h)*(2*(i%3 == 0) + 3*(i%3 != 0))
        print(f"f({a+i*h}) = {f(a+i*h)}")
    print(f"f({b}) = {f(b)}")
    print(f"I = (h/6) * [f(x0) + 3f(x1) + 3f(x2) + 2f(x3) + 3f(x4) + ... + 3f(xN-2) + 3f(xN-1) + f(xN)] = {(3*h / 8) * tot}")
    error = (h**4) * (b-a) * find_max_grad_f(f,4,a,b) / 80
    print(f"E = ((h/2)^4/80) * (b-a) * max(f''''(x)) = {error}")
    print_derivatives(3+1)
    print()
    #return tot*h/3

def midpoint(f, a, b, N):
    print("Midpoint Method")
    h = (b - a) / N
    print(f"a = {a}, b = {b}, N = {N}")
    print(f"h = (b-a)/N = {h}")
    tot = 0
    for i in range(0, N):
        tot += f(a+i*h + h/2)
        print(f"f({a+i*h + h/2}) = {f(a+i*h + h/2)}")
    print(f"I = h*sum[f(xi+h/2)] = {h*tot}")
    error = ((h**2)/24) * (b-a) * find_max_grad_f(f,2,a,b)
    print(f"E = (h^2/24) * (b-a) * max(f''(x)) = {error}")
    print_derivatives(2+1)
    print()
    #return tot*h

if __name__ == "__main__":

    # Examples for all methods
    rect_left(f, a=1, b=2, N=8)
    rect_right(f, a=1, b=2, N=8)
    trapezoid(f, a=1, b=2, N=8)
    simpson(f, a=1, b=2, N=8)
    simpson38(f, a=1, b=2, N=9)
    midpoint(f, a=1, b=2, N=8)

    #Example
    def f(x):
        return x*autograd.numpy.log(x)
    simpson(f, 1, 2, 4)


import numpy as np
from mpi4py import MPI

from sympde.calculus      import grad, dot
from sympde.topology      import ScalarFunctionSpace
from sympde.topology      import elements_of
from sympde.topology      import Square
from sympde.topology      import PolarMapping
from sympde.expr.expr     import LinearForm, BilinearForm
from sympde.expr.expr     import integral
from sympde.expr.expr     import Norm
from sympde.expr.equation import find, EssentialBC

from psydac.api.discretization import discretize

#==============================================================================
def run_poisson_2d(solution, f, domain, ncells, degree, comm=None):

    #+++++++++++++++++++++++++++++++
    # 1. Abstract model
    #+++++++++++++++++++++++++++++++

    V    = ScalarFunctionSpace('V', domain, kind=None)

    u, v = elements_of(V, names='u, v')

    bc   = EssentialBC(u, solution, domain.boundary)

    error  = u - solution

    a = BilinearForm((u,v),  integral(domain, dot(grad(u),grad(v))))
    l = LinearForm(v, integral(domain, f*v))

    equation = find(u, forall=v, lhs=a(u,v), rhs=l(v), bc=bc)

    l2norm = Norm(error, domain, kind='l2')
    h1norm = Norm(error, domain, kind='h1')

    #+++++++++++++++++++++++++++++++
    # 2. Discretization
    #+++++++++++++++++++++++++++++++

    domain_h = discretize(domain, ncells=ncells)
    Vh       = discretize(V, domain_h, degree=degree)

    equation_h = discretize(equation, domain_h, [Vh, Vh])

    l2norm_h = discretize(l2norm, domain_h, Vh)
    h1norm_h = discretize(h1norm, domain_h, Vh)

    uh = equation_h.solve()

    l2_error = l2norm_h.assemble(u=uh)
    h1_error = h1norm_h.assemble(u=uh)

    return l2_error, h1_error

#------------------------------------------------------------------------------
def test_poisson_2d_analytical_mapping_0():

    domain  = Square('A',bounds1=(0., 1.), bounds2=(0, np.pi))
    mapping = PolarMapping('M1',2, c1= 0., c2= 0., rmin = 0., rmax=1.)

    mapped_domain = mapping(domain)

    x,y = mapped_domain.coordinates
    solution = x**2 + y**2
    f        = -4

    l2_error, h1_error = run_poisson_2d(solution, f, mapped_domain, ncells=[2**2,2**2], degree=[2,2])

    expected_l2_error = 1.0930839536997034e-09
    expected_h1_error = 1.390398663745195e-08

    assert ( abs(l2_error - expected_l2_error) < 1e-7 )
    assert ( abs(h1_error - expected_h1_error) < 1e-7 )

#==============================================================================
# CLEAN UP SYMPY NAMESPACE
#==============================================================================
def teardown_module():
    from sympy.core import cache
    cache.clear_cache()

def teardown_function():
    from sympy.core import cache
    cache.clear_cache()


# Custom Functions
import numpy as np
import pandas as pd

def ATransformMatrix (theta): #A
    ATransform = np.array([[np.cos(theta), -np.sin(theta)],
                            [np.sin(theta), np.cos(theta)]])
    return ATransform

def ATransformMatrixTHETA (theta): #A_theta
    ATransformTHETA = np.array([[-np.sin(theta), -np.cos(theta)],
                                [np.cos(theta), -np.sin(theta)]])
    return ATransformTHETA

def prettyMatVect(matVect):
    prettyMatVect = pd.DataFrame(matVect, columns =
                                ['R1x', 'R1y', 'theta1',
                                'R2x','R2y','theta2',
                                'R3x','R3y','theta3',
                                'R4x','R4y','theta4'])
    return prettyMatVect

def calcGlobalCoor(qi, u_bar_iP, u_bar_jP, link_i, link_j):
    # To calculate Point of Interest positions in terms of global coordinates
    index_for_i = 3*(link_i-1)
    index_for_j = 3*(link_j-1)
    Ri = np.array([qi[index_for_i], qi[index_for_i+1]], dtype = float)
    Rj = np.array([qi[index_for_j], qi[index_for_j+1]], dtype = float)

    # Point P at link i
    riP = Ri + np.matmul(ATransformMatrix(float(qi[index_for_i+2])), u_bar_iP)
    # Point P at link j
    rjP = Rj + np.matmul(ATransformMatrix(float(qi[index_for_j+2])), u_bar_jP)

    return riP, rjP

def constraintEquation(constraintVector, qi, r1O2, r2O2, r2A, r3A,
                    r3B, r4B, r1O4, r4O4,
                    theta2Initial, omega2, timeNow):
    # Ground constraint
    for i in range(3):
        constraintVector[i] = 0 #qi[i] # just zero

    # Pin joint O2
    constraintPinO = r2O2# r1O -
    for i in range(np.size(constraintPinO)):
    # Equation 4-5
        constraintVector[i+3] = constraintPinO[i]

    # Pin joint A
    constraintPinA = r2A - r3A
    for i in range(np.size(constraintPinA)):
    # Equation 6-7
        constraintVector[i+5] = constraintPinA[i]

    # Pin joint B
    constraintPinB = r3B - r4B
    for i in range(np.size(constraintPinB)):
    # Equation 8-9
        constraintVector[i+7] = constraintPinB[i]

    # Pin joint O4
    constraintPinO4 = r1O4-r4O4
    for i in range(np.size(constraintPinO4)):
    # Equation 10-11
        constraintVector[i+9] = constraintPinB[i]

    # Equation 12 ===> Driving constraint
    constraintVector[11] = qi[5] - theta2Initial - omega2*timeNow

    return constraintVector

def jacobianMatrix(qi, u_bar_2O2, u_bar_2A, u_bar_3A, u_bar_3B,
                    u_bar_4B, u_bar_1O4, u_bar_4O4):
    n = np.size(qi)
    jacobianMatrixCq = np.zeros((n,n), dtype = float)
    identity3x3 = np.identity(3, dtype = float)
    identity2x2 = np.identity(2, dtype = float)

    # row 1-3 --> Ground constraint
    jacobianMatrixCq[0:3,0:3] = identity3x3

    # row 4-5 (r1O2 = r2O2)
    Cq45 = np.matmul(ATransformMatrixTHETA(float(qi[5])), u_bar_2O2)
    jacobianMatrixCq[3:5,3:5] = identity2x2
    jacobianMatrixCq[3:5,5:6] = Cq45

    # row 6-7 (r2A = r3A)
    Cq67_link2 = np.matmul(ATransformMatrixTHETA(float(qi[5])), u_bar_2A)
    Cq67_link3 = np.matmul(ATransformMatrixTHETA(float(qi[8])), u_bar_3A)
    jacobianMatrixCq[5:7,3:5] = identity2x2
    jacobianMatrixCq[5:7,5:6] = Cq67_link2
    jacobianMatrixCq[5:7,6:8] = -identity2x2
    jacobianMatrixCq[5:7,8:9] = -Cq67_link3

    # row 8-9 (r3B = r4B)
    Cq89_link3 = np.matmul(ATransformMatrixTHETA(float(qi[8])), u_bar_3B)
    Cq89_link4 = np.matmul(ATransformMatrixTHETA(float(qi[11])), u_bar_4B)
    jacobianMatrixCq[7:9,6:8] = identity2x2
    jacobianMatrixCq[7:9,8:9] = Cq89_link3
    jacobianMatrixCq[7:9,9:11] = -identity2x2
    jacobianMatrixCq[7:9,11:12] = -Cq89_link4

    # row 10-11 (r1O4 = r4O4)
    Cq1011_link1 = np.matmul(ATransformMatrixTHETA(float(qi[2])), u_bar_1O4)
    Cq1011_link4 = np.matmul(ATransformMatrixTHETA(float(qi[11])), u_bar_4O4)
    jacobianMatrixCq[9:11,0:2] = identity2x2
    jacobianMatrixCq[9:11,2:3] = Cq1011_link1
    jacobianMatrixCq[9:11,9:11] = -identity2x2
    jacobianMatrixCq[9:11,11:12] = -Cq1011_link4

    # row 12
    jacobianMatrixCq[11][5] = 1

    return jacobianMatrixCq

def constrEqDot(constraintVectorVel, omega):

    constraintVectorVel[11] = -omega

    return constraintVectorVel

def positionAnalysis(constraintVector, jacobianMatrix, qi):
    inverse_jacobian = np.linalg.inv(jacobianMatrix)
    delta_qi = - np.matmul(inverse_jacobian, constraintVector)
    delta_qi_norm = np.linalg.norm(delta_qi)
    qi = qi + delta_qi

    return qi, delta_qi_norm

def velocityAnalysis(constraintVectorVel, jacobianMatrix, qiDot):
    inverse_jacobian = np.linalg.inv(jacobianMatrix)
    qiDot= - np.matmul(inverse_jacobian, constraintVectorVel)

    return qiDot

def accelerationAnalysis(jacobianMatrix, qiDotDot, qiDot, qi,
                        u_bar_2O, u_bar_2A, u_bar_3A, u_bar_3B, u_bar_4B,
                        u_bar_1O4, u_bar_4O4):
    # Qd line 4-5
    Qd45 = np.square(float(qiDot[5]))*np.dot(ATransformMatrix(float(qi[5])), u_bar_2O)

    # Qd line 6-7
    Qd67a = np.square(float(qiDot[5]))*np.dot(ATransformMatrix(float(qi[5])), u_bar_2A)
    Qd67b = np.square(float(qiDot[8]))*np.dot(ATransformMatrix(float(qi[8])), u_bar_3A)
    Qd67 = Qd67a-Qd67b

    # Qd line 8-9
    Qd89a = np.square(float(qi[8]))*np.dot(ATransformMatrix(float(qi[8])), u_bar_3B)
    Qd89b = np.square(float(qi[11]))*np.dot(ATransformMatrix(float(qi[11])), u_bar_4B)
    Qd89 = Qd89a-Qd89b

    # Qd line 10-11
    Qd1011a = np.square(float(qi[2]))*np.dot(ATransformMatrix(float(qi[2])), u_bar_1O4)
    Qd1011b = np.square(float(qi[11]))*np.dot(ATransformMatrix(float(qi[11])), u_bar_4O4)
    Qd1011 = Qd1011a-Qd1011b

    Qd = np.array([[0],[0],[0], Qd45[0], Qd45[1],
                    Qd67[0], Qd67[1], Qd89[0], Qd89[1],
                    Qd1011[0], Qd1011[1], [0]])

    inverse_jacobian = np.linalg.inv(jacobianMatrix)
    qiDotDot= np.dot(inverse_jacobian, Qd)

    return qiDotDot

def eulerMethod(initialState, time, stepSize, systemFunction):
    # initialState = y1 and y2 (e.g. position and velocity)
    totalData = np.size(time)
    position = np.zeros(totalData, dtype = float)
    velocity = np.zeros(totalData, dtype = float)
    acceleration = np.zeros(totalData, dtype = float)

    state = initialState

    for i in range(totalData):
        eulerMeth = systemFunction(time[i], state)
        state = state + stepSize*np.array([state[1], [eulerMeth]], dtype = float)

        position[i] = (state[0])
        velocity[i] = (state[1])
        acceleration[i] = eulerMeth

    return position, velocity, acceleration

def rungeKutta4 (y, time, systemFunction, stepSize):
    # This function works with ANY number of DOF

    numberOfDOF = int(np.size(y)/2)
    totalData   = np.size(time)
    state       = np.zeros((totalData,numberOfDOF), dtype = float)
    stateDot    = np.zeros((totalData,numberOfDOF), dtype = float)
    stateDotDot = np.zeros((totalData,numberOfDOF), dtype = float)

    # RungeKutta4
    for currentTime in range(totalData):

        t1  = time[currentTime]
        f_1 = systemFunction(y, t1)
        k1  = np.zeros((np.size(y), 1), dtype = float)
        for x    in range(numberOfDOF):
            k1[x]   = y[x+numberOfDOF]
            k1[x+numberOfDOF] = f_1[x]

        t2  = time[currentTime]+0.5*stepSize
        y2  = y + 0.5*k1*stepSize
        f_2 = systemFunction(y2, t2)
        k2  = np.zeros((np.size(y), 1), dtype = float)
        for x    in range(numberOfDOF):
            k2[x]   = y2[x+numberOfDOF]
            k2[x+numberOfDOF] = f_2[x]

        t3  = time[currentTime]+0.5*stepSize
        y3  = y + 0.5*k2*stepSize
        f_3 = systemFunction(y3, t3)
        k3  = np.zeros((np.size(y), 1), dtype = float)
        for x    in range(numberOfDOF):
            k3[x]   = y3[x+numberOfDOF]
            k3[x+numberOfDOF] = f_3[x]

        t4  = time[currentTime]+stepSize
        y4  = y + k3*stepSize
        f_4 = systemFunction(y4, t4)
        k4  = np.zeros((np.size(y), 1), dtype = float)
        for x    in range(numberOfDOF):
            k4[x]   = y4[x+numberOfDOF]
            k4[x+numberOfDOF] = f_4[x]

        RKFunct = (k1 + 2*k2 + 2*k3 + k4)/6

        y = y + stepSize*RKFunct

        for dof in range(numberOfDOF):
            state       [currentTime][dof] = y[dof]
            stateDot    [currentTime][dof] = y[dof+numberOfDOF]
            stateDotDot [currentTime][dof] = RKFunct[dof+numberOfDOF]

    return state, stateDot, stateDotDot


import numpy as np

def gaussian_elimination(A, b):
    if len(A) != len(A[0]) or len(A[0]) != len(b):
        raise ValueError('gaussian elimination: invalid matrix or vector sizes')

    if not np.any(np.diag(A)):
        raise ValueError('gaussian elimination: no solution')

    A = np.array(A)
    b = np.array(b)
    n = len(A)

    for row in range(n - 1):
        for i in range(row + 1, n):
            factor = A[i, row] / A[row, row]
            for j in range(row, n):
                A[i, j] -= factor * A[row, j]
            b[i] -= factor * b[row]

    x = np.empty(n)
    x[n - 1] = b[n - 1] / A[n - 1, n - 1]
    for row in reversed(range(n - 2)):
        sums = b[row]
        for j in range(row + 1, n):
            sums -= A[row, j] * x[j]
        x[row] = sums / A[row, row]
    return x

def jacobi_method(A, b, n=10):
    if len(A) != len(A[0]) or len(A) != len(b):
        raise ValueError('jacobi method: invalid matrix and vector sizes')

    D = np.diagflat(np.diag(A))
    R = np.array(A) - D

    D_inv = np.linalg.inv(D)
    b = np.array(b)
    x = np.zeros(len(A))

    for _ in range(n):
        x = np.dot(D_inv, b - np.dot(R, x))

    return x

def gauss_seidel_method(A, b, n=10):
    if len(A) != len(A[0]) or len(A) != len(b):
        raise ValueError('gauss seidel method: invalid matrix and vector sizes')

    L = np.tril(A, k=0)
    U = np.triu(A, k=1)

    L_inv = np.linalg.inv(L)
    b = np.array(b)
    x = np.zeros(len(A))

    for _ in range(n):
        x = np.dot(L_inv, b - np.dot(U, x))

    return x

def successive_over_relaxation_method(A, b, w, n=10):
    if len(A) != len(A[0]) or len(A) != len(b):
        raise ValueError('successive over relaxation method: invalid matrix and vector sizes')

    D = np.diagflat(np.diag(A))
    L = np.tril(A, k=-1)
    U = np.triu(A, k=1)

    inv = np.linalg.inv(D + w * L)
    x = np.zeros(len(A))
    wb = w * np.array(b)
    M = w * U + (w + 1) * D

    for _ in range(n):
        x = np.dot(inv, wb - np.dot(M, x))

    return x


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

data=genfromtxt("multi_var.csv",delimiter=',')
x_data=data[:,0:2]
y_data=data[:,2]

#hypothesis function
def hypothesis(theta_0,theta_1,theta_2,x1,x2):
    return theta_0+theta_1*x1+theta_2*x2

#cost function
def square_error(theta_0,theta_1,theta_2,x_data,y_data):
    sqr_error=0
    for i in range(0,len(x_data)):
        sqr_error+=(hypothesis(theta_0,theta_1,theta_2,x_data[i,0],x_data[i,1])-y_data[i])**2
    return sqr_error/len(x_data)/2

#gradient descent
def gradient_descent(x_data,y_data,theta_0,theta_1,theta_2,learning_rate,iteration):
    N=len(x_data)
    cost_function=[]
    for i in range(iteration):
        theta_0_grad=0
        theta_1_grad=0
        theta_2_grad=0
        for j in range(0,N):
            theta_0_grad+=(1/N)*(hypothesis(theta_0,theta_1,theta_2,x_data[j,0],x_data[j,1])-y_data[j])
            theta_1_grad+=(1/N)*(hypothesis(theta_0,theta_1,theta_2,x_data[j,0],x_data[j,1])-y_data[j])*x_data[j,0]
            theta_2_grad+=(1/N)*(hypothesis(theta_0,theta_1,theta_2,x_data[j,0],x_data[j,1])-y_data[j])*x_data[j,1]
        theta_0=theta_0-(learning_rate*theta_0_grad)
        theta_1=theta_1-(learning_rate*theta_1_grad)
        theta_2=theta_2-(learning_rate*theta_2_grad)
        cost_function.append(square_error(theta_0,theta_1,theta_2,x_data,y_data))
        if i%200==0:
            #plot data
            ax=plt.figure().add_subplot(111,projection='3d')
            ax.scatter(x_data[:,0],x_data[:,1],y_data,c='r',marker='o',s=100)
            #plot the hypothesis
            x0=x_data[:,0]
            x1=x_data[:,1]
            x0,x1=np.meshgrid(x0,x1)
            z=theta_0+x0*theta_1+x1*theta_2
            ax.plot_surface(x0,x1,z)

            ax.set_title("the %d times of iteration" %(i))
            ax.set_xlabel("feature 1")
            ax.set_ylabel("feature 2")
            ax.set_zlabel("target")
            plt.show()
    return theta_0,theta_1,theta_2,cost_function

learning_rate=0.000001
theta_0=0
theta_1=0
theta_2=0
iteration=1000

print("Initial State:")
print("theta_0=%.3lf,   theta_1=%.3lf,  theta_2=%.3lf" %(theta_0,theta_1,theta_2))
print("Running for the method of gradient descent")
theta_0,theta_1,theta_2,cost_function=gradient_descent(x_data,y_data,theta_0,theta_1,theta_2,learning_rate,iteration)
print("Final state:")
print("theta_0=%.3lf,   theta_1=%.3lf,  theta_2=%.3lf" %(theta_0,theta_1,theta_2))

#plot the cost function versus iteration times
for i in range(0,len(cost_function)):
    plt.plot(i,cost_function[i],'b.')
plt.title("cost function versus iteration times")
plt.xlabel("iteration times")
plt.ylabel("cost function")
plt.show()


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

def get_points():
    x = list(map(float, input().split()))
    y = list(map(float, input().split()))

    return x, y

def lls(k, x, y):
    x = np.array(x)
    t = np.array([[i ** j for i in x] for j in reversed(range(k))])
    t_trans = np.transpose(t)
    g = t @ t_trans
    a = inv(g) @ t @ y

    return a

def calc_err(x, y, coeff):
    y_err = [np.polyval(coeff, i) for i in x]

    err = sum([(y_err[idx] - i) ** 2 for idx, i in enumerate(y)])

    return err

def main():
    x, y = get_points()

    coef_2d = lls(2, x, y)
    print(coef_2d)
    coef_3d = lls(3, x, y)
    print(coef_3d)
    x_vals = np.linspace(x[0], x[-1])
    y_2d = [np.polyval(coef_2d, i) for i in x_vals]
    y_3d = [np.polyval(coef_3d, i) for i in x_vals]

    plt.scatter(x, y, color='r')
    plt.plot(x_vals, y_2d, color='b')
    plt.plot(x_vals, y_3d, color='g')
    plt.show()
    print(f'error for ax + b = {calc_err(x, y, coef_2d)}')
    print(f'error for ax^2 + bx + c = {calc_err(x, y, coef_3d)}')

main()


import numpy as np

def gauss_jordan(matrix, sol_matrix):
    matrix = np.array(matrix, float)
    sol_matrix = np.array(sol_matrix, float)
    n = len(sol_matrix)

    for k in range(n):
        if np.fabs(matrix[k,k])< 1.0e-12:
            for i in range(k+1,n):
                if np.fabs(matrix[i,k]) > np.fabs(matrix[k,k]):
                    for j in range(k,n):
                        matrix[k,j], matrix[i,j] = matrix[i,j],matrix[k,j]
                    sol_matrix[k],sol_matrix[i] = sol_matrix[i],sol_matrix[k]
                    break
        pivot = matrix[k,k]
        for j in range(k,n):
            matrix[k,j] /= pivot
        sol_matrix[k] /= pivot

        for i in range(n):
            if i == k or matrix[i,k] == 0: continue
            factor = matrix[i,k]
            for j in range(k,n):
                matrix[i,j] -= factor * matrix[k,j]
            sol_matrix[i] -= factor*sol_matrix[k]

    return sol_matrix.tolist(), matrix.tolist()


import numpy as np

from qr import QR

def Eigen(matrix):
    N = matrix.shape[0]
    np_dtype = np.float64
    values = np.zeros(N, dtype=np_dtype)
    vectors = np.eye(N, dtype=np_dtype)
    m = np.ascontiguousarray(matrix, dtype=np_dtype)
    for i in range(30):
        q, r = QR(m)
        np.matmul(vectors, q, out=vectors)
        np.matmul(r, q, out=m)
    for i in range(N):
        values[i] = m[i, i]
    return values, vectors

if __name__ == "__main__":
    A = np.array(
        [[1, 1, 1],
         [1, 2, 1],
         [1, 1, 2]])
    values, vectors = Eigen(A)

    values_np, vectors_np = np.linalg.eig(A)
    print('-----------test-result--------------')
    print('eigen value:\n', values)
    print('eigen vector:\n', vectors)
    print(np.matmul(A, vectors))
    print('----------------end-----------------')

    print('--------------numpy----------------')
    print('eigen value:\n', values_np)
    print('eigen vector:\n', vectors_np)
    print(np.matmul(A, vectors_np))
    print('----------------end-----------------')


"""
demo_system_of_linear_equations.py

Solving a system of linear equations in a least squares way:
A @ X = B (matrix form)

Normal equation with Tikhonov regularization

Gradient algorithm with variable step size (steepest descent).
    Only for quadratic f(X) function. Requires gradient and hessian.
    Guaranteed to converge in n steps for n parameters, i.e. for X = [x1, ..., xn].T.
    X0 converges to X* for any X0.
    In parameter space, moving along orthogonal directions with variable step size.

Gradient algorithm with fixed step size.
    Only for quadratic f(X) function. Requires gradient and hessian.
    X0 converges to X* for any X0.
    In parameter space, moving along orthogonal directions with fixed step size.

Gradient algorithm with variable step size based on line search
    General algorithm for any f(X) function. Requires only gradient.
    X0 converges to X* for any X0.
    In parameter space, moving along orthogonal directions with variable step size determined by line search.
    Step size is determined by line search to ensure the descent property f(Xn+1) < f(Xn)

Conjugate gradient algorithm with variable step size (steepest descent).
    Only for quadratic f(X) function. Requires gradient and hessian.
    Guaranteed to converge in n steps for n parameters, i.e. for X = [x1, ..., xn].T.
    X0 converges to X* for any X0.
    In parameter space, moving along mutually conjugate directions with variable step size.

Conjugate gradient algorithm with variable step size based on line search
    General algorithm for any f(X) function. Requires only gradient.
    X0 converges to X* for any X0.
    In parameter space, moving along mutually conjugate directions with variable step size.
    Step size is determined by line search to ensure the descent property f(Xn+1) < f(Xn).
    For non-quadratic f(X) conjugate direction vectors will tend to deteriorate and need to be
        reset to the negative gradient every n steps.

Newton's algorithm.
    General algorithm for any f(X) function. Requires gradient and hessian.
    Can converge in as few as 1 step (for quadratic f(X)).
    X0 should be close to X*.
    In parameter space, moving to the MIN of parabola in 1 step.

Newton's algorithm with variable step size based on line search
    General algorithm for any f(X) function. Requires gradient and hessian.
    Can converge in as few as 1 step (for quadratic f(X)).
    X0 converges to X* for any X0.
    In parameter space, moving to the MIN of parabola in 1 step. Might be affected by linesearch.
    Step size is determined by line search to ensure the descent property f(Xn+1) < f(Xn).

Quasi-Newton algorithm with variable step size (steepest descent).
    Only for quadratic f(X) function. Requires gradient and hessian.
    Guaranteed to converge in n steps for n parameters, i.e. for X = [x1, ..., xn].T.
    X0 converges to X* for any X0.
    In parameter space, moving along mutually conjugate directions with variable step size.
    For non-quadratic f(X) conjugate direction vectors will tend to deteriorate and need to be
        reset to the negative gradient every n steps.

Quasi-Newton algorithm with variable step size based on line search
    General algorithm for any f(X) function. Requires only gradient.
    X0 converges to X* for any X0.
    In parameter space, moving along mutually conjugate directions with variable step size.
    Step size is determined by line search to ensure the descent property f(Xn+1) < f(Xn).
    For non-quadratic f(X) conjugate direction vectors will tend to deteriorate and need to be
        reset to the negative gradient every n steps.

Naive random walk algorithm
    General algorithm for any f(X) function. Requires only residuals.
    X0 converges to X* for any X0.

Simulated annealing algorithm
    General algorithm for any f(X) function. Requires only residuals.
    X0 converges to X* for any X0.

Particle swarm optimization algorithm
    General algorithm for any f(X) function. Requires only residuals.
    X0 converges to X* for any X0.
"""

import numpy as np
from gradient_algorithm import gradient_algorithm_var_alpha, gradient_algorithm_fixed_alpha, gradient_algorithm_linesearch
from conjugate_gradient_algorithm import conjugate_gradient_algorithm, conjugate_gradient_algorithm_linesearch
from newton_algorithm import newton_algorithm, newton_algorithm_linesearch
from quasi_newton_algorithm import quasi_newton_algorithm, quasi_newton_algorithm_linesearch
from naive_random_search_algorithm import naive_random_search_algorithm
from simulated_annealing_algorithm import simulated_annealing_algorithm
from particle_swam_optimization_algorithm import particle_swam_optimization_algorithm
from print_report import print_report
import time

reg_coef = 0.01; # regularization coefficient
# A @ X = B (matrix form)
# Minimize the following objective function:
# X = [x0, ..., xn].T;
# f(X) = norm(A * X - B)^2 =  X.T * (A.T @ A) @ X - 2 * (A.T @ B).T @ X + B.T @ B + reg_coef * X.T @ X;
N_eqs = 5000; # number of equations
N_pars = 10; # number of parameters.
A = np.random.rand(N_eqs, N_pars);
B = np.random.rand(N_eqs, 1);
Q = A.T @ A;
func = lambda X : X.T @ Q @ X - 2 * (A.T @ B).T @ X + B.T @ B + reg_coef * X.T @ X;
func_grad = lambda X : 2 * Q @ X - 2 * (A.T @ B) + 2 * reg_coef * X;
func_hessian = lambda X : 2 * Q + 2 * reg_coef;
# Objective function for naive random walk and simulated annealing algorithms
func_error = lambda X : np.linalg.norm((A @ X - B + reg_coef * X.T @ X), axis = 0) ** 2;
# Objective function for particle swarm optimization algorithm (particles along row dimension, axis = 0)
def func_error_ps(X):
    a = np.linalg.norm((X @ A.T - B.T + reg_coef * np.diag(X @ X.T).reshape(-1, 1)), axis = 1) ** 2;
    return a.reshape(-1,1);

# Normal equation with Tikhonov regularization
print('***********************************************************************');
print('Normal equation with Tikhonov regularization');
start = time.time();
X = np.linalg.solve(Q + reg_coef * np.eye(N_pars), A.T @ B);
end = time.time();
print("Norm(X): %0.3f" % (np.linalg.norm(X)));
print('f(X): %0.3f' % (func(X)));
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Gradient algorithm with variable step size (steepest descent)
print('***********************************************************************');
print('Gradient algorithm with variable step size (steepest descent)');
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max};
X0 = np.zeros((N_pars, 1));
start = time.time();
X, report = gradient_algorithm_var_alpha(X0, func, func_grad, func_hessian, options);
end = time.time();
print_report(func, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Gradient algorithm with fixed step size
print('***********************************************************************');
print('Gradient algorithm with fixed step size');
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max};
X0 = np.zeros((N_pars, 1));
start = time.time();
X, report = gradient_algorithm_fixed_alpha(X0, func, func_grad, func_hessian, options);
end = time.time();
print_report(func, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Gradient algorithm with variable step size based on line search
print('***********************************************************************');
print('Gradient algorithm with variable step size based on line search');
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max};
X0 = np.zeros((N_pars, 1));
start = time.time();
X, report = gradient_algorithm_linesearch(X0, func, func_grad, options);
end = time.time();
print_report(func, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Conjugate gradient algorithm with variable step size (steepest descent)
print('***********************************************************************');
print('Conjugate gradient algorithm with variable step size (steepest descent)');
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max};
X0 = np.zeros((N_pars, 1));
start = time.time();
X, report = conjugate_gradient_algorithm(X0, func, func_grad, func_hessian, options);
end = time.time();
print_report(func, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Conjugate gradient algorithm with variable step size based on line search
print('***********************************************************************');
print('Conjugate gradient algorithm with variable step size based on line search');
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max};
X0 = np.zeros((N_pars, 1));
start = time.time();
X, report = conjugate_gradient_algorithm_linesearch(X0, func, func_grad, options);
end = time.time();
print_report(func, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Newton's algorithm
print('***********************************************************************');
print('Newton\'s algorithm');
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max};
X0 = np.zeros((N_pars, 1));
start = time.time();
X, report = newton_algorithm(X0, func, func_grad, func_hessian, options);
end = time.time();
print_report(func, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Newton's algorithm with variable step size based on line search
print('***********************************************************************');
print('Newton\'s algorithm with variable step size based on line search');
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max};
X0 = np.zeros((N_pars, 1));
start = time.time();
X, report = newton_algorithm_linesearch(X0, func, func_grad, func_hessian, options);
end = time.time();
print_report(func, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Quasi-Newton algorithm with variable step size (steepest descent)
print('***********************************************************************');
print('Quasi-Newton algorithm with variable step size (steepest descent)');
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max};
X0 = np.zeros((N_pars, 1));
start = time.time();
X, report = quasi_newton_algorithm(X0, func, func_grad, func_hessian, options);
end = time.time();
print_report(func, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Quasi-Newton algorithm with variable step size based on line search
print('***********************************************************************');
print('Quasi-Newton algorithm with variable step size based on line search');
N_iter_max = 1000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max};
X0 = np.zeros((N_pars, 1));
start = time.time();
X, report = quasi_newton_algorithm_linesearch(X0, func, func_grad, options);
end = time.time();
print_report(func, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Naive random walk algorithm
print('***********************************************************************');
print('Naive random walk algorithm');
N_iter_max = 10000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
X_lower = -1 * np.ones((N_pars, 1)); # X lower bound
X_upper = 1 * np.ones((N_pars, 1)); # X upper bound
alpha = 1.0; # step size
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max, 'x_lower' : X_lower,
           'x_upper' : X_upper, 'alpha' : alpha};
X0 = X_lower + (X_upper - X_lower) * np.random.rand(X_lower.size, 1);
start = time.time();
X, report = naive_random_search_algorithm(X0, func_error, options);
end = time.time();
print_report(func, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Simulated annealing algorithm
print('***********************************************************************');
print('Simulated annealing algorithm');
N_iter_max = 10000;
tolerance_x = 10e-6;
tolerance_y = 10e-8;
X_lower = -1 * np.ones((N_pars, 1)); # X lower bound
X_upper = 1 * np.ones((N_pars, 1)); # X upper bound
alpha = 1; # step size
gamma = 1.0; # controls temperature decay, gamma > 0
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max, 'x_lower' : X_lower,
           'x_upper' : X_upper, 'alpha' : alpha, 'gamma' : gamma};
X0 = X_lower + (X_upper - X_lower) * np.random.rand(X_lower.size, 1);
start = time.time();
X, report = simulated_annealing_algorithm(X0, func_error, options);
end = time.time();
print_report(func, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');

# Particle swarm optimization algorithm
print('***********************************************************************');
print('Particle swarm optimization algorithm');
N_iter_max = 1000;
tolerance_x = 10e-8;
tolerance_y = 10e-8;
X_lower = -1 * np.ones((N_pars, 1)); # X lower bound
X_upper = 1 * np.ones((N_pars, 1)); # X upper bound
d_lower = -0.25; # direction (aka velocity) lower bound
d_upper = 0.25; # direction (aka velocity) upper bound
N_ps = 1000; # number of particles
w = 1.0; # inertial constant, w < 1
c1 = 1.0; # cognitive/independent component, c1 ~ 2
c2 = 0; # social component, c2 ~ 2
alpha = 1.0; # step size
options = {'tolerance_x' : tolerance_x, 'tolerance_y' : tolerance_y, 'N_iter_max' : N_iter_max, 'x_lower' : X_lower,
           'x_upper' : X_upper, 'alpha' : alpha, 'd_lower' : d_lower, 'd_upper' : d_upper, 'N_ps' : N_ps, 'w' : w, 'c1' : c1, 'c2' : c2};
start = time.time();
X, report = particle_swam_optimization_algorithm(func_error_ps, options);
end = time.time();
print_report(func, report);
print('Elapsed time [s]: %0.5f' % (end - start));
print('***********************************************************************\n');


"""Utility functions for computing useful statistics."""

import numpy as np

def pearson_residuals(y, pred):
    """Compute Pearson residuals.

    Reference:
    https://web.as.uky.edu/statistics/users/pbreheny/760/S11/notes/4-12.pdf

    :param array-like[int] y: target labels. 1 is positive label, 0 is negative
        label
    :param array-like[float] pred: predicted labels.
    :returns: pearson residual.
    :rtype: array-like[float]
    """
    y, pred = np.array(y), np.array(pred)
    return (y - pred) / np.sqrt(pred * (1 - pred))

def deviance_residuals(y, pred):
    """Compute Deviance residuals.

    Reference:
    https://web.as.uky.edu/statistics/users/pbreheny/760/S11/notes/4-12.pdf

    Formula:
    d = sign * sqrt(-2 * {y * log(p) + (1 - y) * log(1 - p)})
    - where sign is -1 if y = 1 and 1 if y = 0
    - y is the true label
    - p is the predicted probability

    :param array-like[int] y: target labels. 1 is positive label, 0 is negative
        label
    :param array-like[float] pred: predicted labels.
    :returns: deviance residual.
    :rtype: array-like[float]
    """
    y, pred = np.array(y), np.array(pred)
    sign = np.array([1 if y_i else -1 for y_i in y])
    return sign * np.sqrt(-2 * (y * np.log(pred) + (1 - y) * np.log(1 - pred)))


import numpy as np

def SPSA_gradient(loss, currentParams, gradientCoefficient):
    r'''Computes an estimator of the gradient using the procedure
    described in the SPSA algorithm.

    Inputs:
        loss: The loss function
        currentParams: The current value for the parameters
        gradientCoefficient: The coefficient c_n, which controls how much the current parameters are
                                               perturbed when computing the gradient

    Returns:
        gradient: The SPSA-based gradient of the loss function at currentParams'''

    numParams = len(currentParams)
    # Generate a random perturbation using the Rademacher distribution
    randomPerturbation = 2*np.random.binomial(1, .5, size=numParams) - 1

    gradient = (loss(currentParams + gradientCoefficient*randomPerturbation) - loss(currentParams - gradientCoefficient*randomPerturbation))\
                        /(gradientCoefficient*randomPerturbation)

    return gradient

def SPSA_update(loss, currentParams, updateCoefficient, gradientCoefficient):
    r'''Performs a parameter update according to the SPSA approach.

    NOTE: This function isn't aware of the notion of iterations, or anything of that sort.

    Inputs:
        loss: The loss function
        currentParams: The current value for the parameters
        updateCoefficient: The coefficient a_n, which controls how the current parameters are updated
                                            when including the gradient
        gradientCoefficient: The coefficient c_n, which controls how much the current parameters are
                                               perturbed when computing the gradient

    Returns:
        The updated parameter values'''

    grad = SPSA_gradient(loss, currentParams, gradientCoefficient)

    return currentParams - updateCoefficient*grad


try:
    from numpy import euler_gamma as _euler_gamma
    import numpy as _np
    from math import sqrt as _sqrt, log as _log, pi as _pi, exp as _exp
    from typing import Union, Dict, List
    from univariate._base import SemiInfinite
except Exception as e:
    print(f"some modules are missing {e}")

class Gumbell(SemiInfinite):
    """
    This class contains methods concerning Gumbel Distirbution [#]_.

    .. math::
        \\text{Gumbel}(x;\\mu,\\beta) = \\frac{1}{\\beta} \\exp{-\\Big( \\frac{x-\\mu}{\\beta} + \\exp{ \\frac{x-\\mu}{\\beta}} \\Big)}

    Args:

        location(float): location parameter (:math:`\\mu`)
        scale(float): scale parameter (:math:`\\beta`) where scale > 0
        x(float): random variable

    Reference:
        .. [#] Wikipedia contributors. (2020, November 26). Gumbel distribution. https://en.wikipedia.org/w/index.php?title=Gumbel_distribution&oldid=990718796
    """

    def __init__(self, location: float, scale: float):
        if scale < 0:
            raise ValueError(
                f'scale parameter should be greater than 0. The value of the scale parameter is: {scale}')

        self.location = location
        self.scale = scale

    def pdf(self, x: Union[List[float], _np.ndarray, float]) -> Union[float, _np.ndarray]:
        """

        Args:
            x (Union[List[float], numpy.ndarray, float]): random variable(s)

        Returns:
            Union[float, numpy.ndarray]: evaluation of pdf at x
        """
        mu = self.location
        beta = self.scale

        if isinstance(x, (_np.ndarray, List)):
            if not type(x) is _np.ndarray:
                x = _np.array(x)
            z = (x-mu)/beta
            return (1/beta)*_np.exp(-(z+_np.exp(-z)))

        z = (x-mu)/beta
        return (1/beta)*_exp(-(z+_exp(-z)))

    def cdf(self, x: Union[List[float], _np.ndarray, float]) -> Union[float, _np.ndarray]:
        """
        Args:
            x (Union[List[float], numpy.ndarray, float]): data point(s) of interest

        Returns:
            Union[float, numpy.ndarray]: evaluation of cdf at x
        """
        mu = self.location
        beta = self.scale

        if isinstance(x, (_np.ndarray, List)):
            if not type(x) is _np.ndarray:
                x = _np.array(x)
            return _np.exp(-_np.exp(-(x-mu)/beta))
        return _exp(-_exp(-(x - mu)/beta))

    def mean(self) -> float:
        """
        Returns: Mean of the Gumbel distribution.
        """
        return self.location+(self.scale*_euler_gamma)

    def median(self) -> float:
        """
        Returns: Median of the Gumbel distribution.
        """
        return self.location - (self.scale*_log(_log(2)))

    def mode(self) -> float:
        """
        Returns: Mode of the Gumbel distribution.
        """
        return self.location

    def var(self) -> float:
        """
        Returns: Variance of the Gumbel distribution.
        """
        return pow(_pi, 2/6)*pow(self.scale, 2)

    def std(self) -> float:
        """
        Returns: Standard deviation of the Gumbel distribution.
        """
        return _sqrt(self.var())

    def skewness(self) -> float:
        """
        Returns: Skewness of the Gumbel distribution.
        """
        return 1.14

    def kurtosis(self) -> float:
        """
        Returns: Kurtosis of the Gumbel distribution.
        """
        return 2.4

    def summary(self) -> Dict[str, float]:
        """
        Returns:
            Dictionary of Gumbel distirbution moments. This includes standard deviation.
        """
        return {
            'mean': self.mean(), 'median': self.median(), 'mode': self.mode(),
            'var': self.var(), 'std': self.std(), 'skewness': self.skewness(), 'kurtosis': self.kurtosis()
        }


import csv
import math
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import time
import numpy as np
from scipy.special import expit
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
from matplotlib.ticker import LinearLocator, FormatStrFormatter

# read the data
csvfile=open("q4x.dat", 'r')
x = list(csv.reader(csvfile,delimiter=' '))
csvfile=open("q4y.dat", 'r')
y = list(csv.reader(csvfile))

# Alaska is 0 and Canada is 1
m=len(x)
n=2
x3=[]
y2=[]
for i in range(m):
    x3.append([float(x[i][0]),float(x[i][1])])
    if(y[i][0]=="Alaska"):
        y2.append(0)
    else:
        y2.append(1)

# normalise the data
meanx=[0,0]
# meanx[0]=sum(x3[0])/m
# meanx[1]=sum(x3[1])/m

for i in range(m):
    meanx[0]+=x3[i][0]
    meanx[1]+=x3[i][1]
meanx[0]/=m
meanx[1]/=m

v=[0,0]
for i in range(m):
    temp=x3[i][0]-meanx[0]
    v[0]+=temp*temp
    temp=x3[i][1]-meanx[1]
    v[1]+=temp*temp
v[0]/=m
v[0]=math.sqrt(v[0])
v[1]/=m
v[1]=math.sqrt(v[1])
for i in range(m):
    x3[i][0]=(x3[i][0]-meanx[0])/v[0]
    x3[i][1]=(x3[i][1]-meanx[1])/v[1]

x2=[]
# for y=1
x4=[]
x5=[]
# for y=0
x6=[]
x7=[]

# calculate u0 & u1
u0=np.array([0.,0.])
u1=np.array([0.,0.])
phi=0
m0=0
m1=0
for i in range(m):
    x2.append(np.array([x3[i][0],x3[i][1]]))

    if(y2[i]==1):
        x4.append(float(x3[i][0]))
        x5.append(float(x3[i][1]))
        u1+=x2[i]
        phi+=1
        m1+=1
    elif(y2[i]==0):
        x6.append(float(x3[i][0]))
        x7.append(float(x3[i][1]))
        u0+=x2[i]
        m0+=1
phi/=m
X=np.array(x2)
Y=np.array(y2)

u0/=m0
u1/=m1

print("u0=\n{}\n".format(u0))
print("u1=\n{}\n".format(u1))

# calculate Sigma
sigma=np.zeros((n,n))
for j in range(m):
    if(Y[j]==0):
        sigma+=np.outer(X[j]-u0,X[j]-u0)
    else:
        sigma+=np.outer(X[j]-u1,X[j]-u1)

sigma/=m
print("Sigma=\n{}\n".format(sigma))

sigmainv=np.linalg.inv(sigma)

# Q4(b)&(c)
plt.ion()
fig=plt.figure()
ax1 = fig.add_subplot(1,1,1)

# plots training data and linear separator
def pl():
    ax1.clear()
    ax1.scatter(x4, x5, label= "y='Canada' Training Data", color= "red",
                marker= ".", s=10)
    ax1.scatter(x6, x7, label= "y='Alaska' Training Data", color= "blue",
                marker= ".", s=10)

    xvalues=np.linspace(min(min(x4),min(x6)),max(max(x4),max(x6)),100)

    Xcoeff = np.matmul(sigmainv,(u0-u1)) + np.matmul((u0-u1).T,sigmainv)

    the=[0,0,0]

    # theta[0]=intercept = (constant in Xcoeff) + u1' * inv(sigma) * u1  -  u0' * inv(sigma) * u0  -  2log(phi/(1-phi))
    the[0]=np.matmul(u1.T,np.matmul(sigmainv,u1)) - np.matmul(u0.T,np.matmul(sigmainv,u0)) - 2*math.log((phi/(1-phi)))
    the[1]=Xcoeff[0]
    the[2]=Xcoeff[1]

    yvalues=(-the[1]*xvalues-the[0])/the[2]

    ax1.plot(xvalues, yvalues, label="Linear separator",color ="#66cc00")

    plt.xlabel('x1')
    plt.ylabel('x2')
    plt.legend()
    plt.title('Q4 (b)&(c)')
    plt.show()
    plt.pause(0.001)

# plt.ioff()
pl()

# Q4(d)

sigma0=np.zeros((n,n))
sigma1=np.zeros((n,n))
m0=0
m1=0
for j in range(m):
    if(Y[j]==0):
        sigma0+=np.outer(X[j]-u0,X[j]-u0)
        m0+=1
    else:
        sigma1+=np.outer(X[j]-u1,X[j]-u1)
        m1+=1

sigma0/=m0
sigma1/=m1

print("Sigma0=\n{}\n".format(sigma0))
print("Sigma1=\n{}\n".format(sigma1))

sigma0inv=np.linalg.inv(sigma0)
sigma1inv=np.linalg.inv(sigma1)

# constant = log(det(sigma1)/det(sigma0)) - 2*log(phi/(1-phi))
constant= math.log(abs(np.linalg.det(sigma1)/np.linalg.det(sigma0))) + 2*math.log(phi/(1-phi))

def findval(x,y):
    v = np.array([x,y])
    # (X-u1)' * inv(sigma1) * (X-u1)  -  (X-u0)' * inv(sigma0) * (X-u0)  +  constant
    ans=np.matmul((v-u1).T,np.matmul(sigma1inv,(v-u1))) - np.matmul((v-u0).T,np.matmul(sigma0inv,(v-u0))) + constant
    return ans

# Plots Quadratic Decision boundary as a contour
def plot_2():

    xvalues = np.linspace(min(min(x4),min(x6)),max(max(x4),max(x6)), 100)
    yvalues = np.linspace(min(min(x5),min(x7)),max(max(x5),max(x7)), 100)

    xvalues, yvalues = np.meshgrid(xvalues, yvalues)
    Zs = np.array([findval(x,y) for x,y in zip(np.ravel(xvalues), np.ravel(yvalues))])
    Z = Zs.reshape(xvalues.shape)

    CS=plt.contour(xvalues, yvalues, Z,[0])
    CS.collections[0].set_label("Quadratic boundary")

    plt.xlabel('x1')
    plt.ylabel('x2')
    plt.legend()
    plt.title('Q4 (e)')
    plt.show()
    plt.pause(0.001)

plt.ioff()
plot_2()


import numpy as np

def train_LDA(X, y):
    '''
    Synopsis:
        w, b= train_LDA(X, y)
    Arguments:
        X: data matrix (features X samples)
        y: labels with values 0 and 1 (1 x samples)
    Output:
        w: LDA weight vector
        b: bias term
    '''
    mu1 = np.mean(X[:, y == 0], axis=1)
    mu2 = np.mean(X[:, y == 1], axis=1)

    ## Three ways to get an estimate of the covariance
    # -- 1. Simply average class-covariance matrices
    #C1 = np.cov(X[:, y==0])
    #C2 = np.cov(X[:, y==1])
    #C = (C1 + C2) / 2
    # -- 2. Weighted average of class-covariance matrices
    #C1 = np.cov(X[:, y==0])
    #C2 = np.cov(X[:, y==1])
    #N1= np.sum(y==0)           # this would be the weighted average
    #N2= np.sum(y==1)
    #C= (N1-1)/(N1+N2-1)*C1 + (N2-1)/(N1+N2-1)*C2
    # -- 3. Center features classwise to estimate covariance on all samples at once
    Xpool = np.concatenate((X[:, y==0]-mu1[:,np.newaxis], X[:, y==1]-mu2[:,np.newaxis]), axis=1)
    C = np.cov(Xpool)

    w = np.linalg.pinv(C).dot(mu2-mu1)
    b = w.T.dot((mu1 + mu2) / 2)
    return w, b

def crossvalidation(classifier_fcn, X, y, folds=10, verbose=False):
    '''
    Synopsis:
        loss_te, loss_tr= crossvalidation(classifier_fcn, X, y, folds=10, verbose=False)
    Arguments:
        classifier_fcn: handle to function that trains classifier as output w, b
        X:              data matrix (features X samples)
        y:              labels with values 0 and 1 (1 x samples)
        folds:         number of folds
        verbose:        print validation results or not
    Output:
        loss_te: value of loss function averaged across test data
        loss_tr: value of loss function averaged across training data
    '''
    nDim, nSamples = X.shape
    inter = np.round(np.linspace(0, nSamples, num=folds + 1)).astype(int)
    perm = np.random.permutation(nSamples)
    errTr = np.zeros([folds, 1])
    errTe = np.zeros([folds, 1])

    for ff in range(folds):
        idxTe = perm[inter[ff]:inter[ff + 1] + 1]
        idxTr = np.setdiff1d(range(nSamples), idxTe)
        w, b = classifier_fcn(X[:, idxTr], y[idxTr])
        out = w.T.dot(X) - b
        errTe[ff] = loss_weighted_error(out[idxTe], y[idxTe])
        errTr[ff] = loss_weighted_error(out[idxTr], y[idxTr])

    if verbose:
        print('{:5.1f} +/-{:4.1f}  (training:{:5.1f} +/-{:4.1f})  [using {}]'.format(errTe.mean(), errTe.std(),
                                                                                errTr.mean(), errTr.std(),
                                                                                classifier_fcn.__name__))
    return np.mean(errTe), np.mean(errTr)

def loss_weighted_error(out, y):
    '''
    Synopsis:
        loss= loss_weighted_error( out, y )
    Arguments:
        out:  output of the classifier
        y:    true class labels
    Output:
        loss: weighted error
    '''
#    loss = 50 * (np.mean(out[y == 0] >= 0) + np.mean(out[y == 1] < 0))
    err1 = 0 if sum(y==0) == 0 else np.mean(out[y == 0] >= 0)
    err2 = 0 if sum(y==1) == 0 else np.mean(out[y == 1] < 0)
    return 100 * np.mean([err1, err2])
    return loss


import numpy as np

# Generate a random Latin Hypercube Design (LHD)

def rLHD(nrows,ncols,unit_cube=False):
  """ Generate a random Latin Hypercube Design (LHD)

  Args:
      nrows (int): A positive integer specifying the number of rows
      ncols (int): A postive integer specifying the number of columns
      unit_cube (bool): If True, design will be in the unit cube [0,1]^ncols.

  Returns:
      numpy.ndarray: return a random (nrows by ncols) LHD

  Examples:
      >>> pyLHD.rLHD(nrows=5,ncols = 4,unit_cube = False)
      >>> pyLHD.rLHD(nrows=5,ncols = 4,unit_cube = True)

  """
  rng = np.random.default_rng()
  rs = np.arange(start=1, stop=nrows+1)
  space = []
  for _ in range(ncols):
    space.append(rng.choice(rs, nrows, replace=False))
  D = np.asarray(space).transpose()

  if unit_cube:
    return (D-0.5)/nrows
  else:
    return D

# Good Lattice Point Design

def GLPdesign(nrows,ncols,h = None):
  """ Good Lattice Point (GLP) Design

  Args:
      nrows (int): A positive integer specifying the number of rows
      ncols (int): A postive integer specifying the number of columns
      h (list, optional): A list whose length is same as (ncols), with its elements that are smaller than and coprime to (nrows).
      Defaults to None. If None, a random sample of (ncols) elements between 1 and (nrows-1).

  Returns:
      numpy.ndarray: A (nrows by ncols) GLP design.

  Examples:
      >>> pyLHD.GLPdesign(nrows=5,ncols=3)
      >>> pyLHD.GLPdesign(nrows=8,ncols=4,h=[1,3,5,7])
  """
  rng = np.random.default_rng()
  if h is None:
    seq = np.arange(start=1,stop=nrows)
    h_sample = rng.choice(seq,ncols,replace=False)
  else:
    h_sample = rng.choice(h,ncols,replace=False)

  mat = np.zeros((nrows,ncols))

  for i in range(nrows):
    for j in range(ncols):
      mat[i,j] = ((i+1)*h_sample[j])% nrows
  return mat.astype(int)


import numpy as np
import sys
from sympy import *
from matplotlib import pyplot as plt

def biseccion(func,rango, tol, iterMax):

    #Funcion de prueba: "E**x - x - 2", [0,2], 10**(-10), 100

    x = Symbol('x') #Inicializa "x" como símbolo
    f = sympify(func) #Se traduce el string "func" a una función de sympy
    fx = lambdify(x, f, modules=['numpy']) #Se inicializa la función f(x)

    D = [] #Datos eje x (Iteraciones)
    A = [] #Datos eje y (Errores)

    cont = 1 #Se inicializa el contador

    #Verifica que la tolerancia no sea negativa
    if (tol < 0):
        return print("La tolerancia debe ser mayor que cero")

    #Verifica que se cumpla con el teorema de Bolzano
    if(not(bolzano(fx(rango[0]),fx(rango[1])))):
        return print("No cumple con el teorema de Bolzano")

    while(cont <= iterMax):

        x = (rango[0] + rango[1]) / 2 #Calcula punto medio
        y = fx(x) #Se calcula el valor de f(x)

        D.append(cont)
        A.append(y)

        if(fx(rango[0])*y < 0):
            rango[1] = x

        elif(y*fx(rango[1]) < 0):
            rango[0] = x

        cont += 1 #Incrementa contador

        if(np.absolute(y) < tol):
            break

    return [x, np.absolute(y), cont, D, A] #Retorna la aproximación del cero de f(x), el error, y la cantidad de iteraciones

def falsa_posicion(func, rango, tol, iterMax):

    #Funcion de prueba: "E**x - x - 2", [0,2], 10**(-10), 100

    x = Symbol('x') #Inicializa "x" como símbolo
    f = sympify(func) #Se traduce el string "func" a una función de sympy
    fx = lambdify(x, f, modules=['numpy']) #Se inicializa la función f(x)

    cont = 0 #Se inicializa el contador

    D = [] #Datos eje x (Iteraciones)
    A = [] #Datos eje y (Errores)

    #Verifica que la tolerancia no sea negativa
    if (tol < 0):
        return print("La tolerancia debe ser mayor que cero")

    #Verifica que se cumpla con el teorema de Bolzano
    if(not(bolzano(fx(rango[0]),fx(rango[1])))):
        return print("No cumple con el teorema de Bolzano")

    b = rango[0]
    a = rango[1]

    xk = a - ((a-b)/(fx(a)-fx(b)))*fx(a)

    D.append(cont)
    A.append(fx(xk))

    while(cont < iterMax):

        if(bolzano(fx(a),fx(xk))):
            if((fx(xk)-fx(a)) == 0): #Indefinición de denominador
                break
            b = xk
            xk = xk - ((xk-a)/(fx(xk)-fx(a))*fx(xk))
        elif(bolzano(fx(xk),fx(b))):
            if((fx(xk)-fx(b)) == 0): #Indefinición de denominador
                break
            a = xk
            xk = xk - ((xk-b)/(fx(xk)-fx(b))*fx(xk))

        D.append(cont)
        A.append(fx(xk))

        cont += 1 #Incrementa contador

        if(np.absolute(fx(xk)) < tol):
            break

    return [xk, np.absolute(fx(xk)), cont, D, A]

def newton_raphson(func, xk, tol, iterMax):

    #Funcion de prueba: "cos(2*x)**2 - x**2", 3/4, 10**(-10), 100

    x = Symbol('x') #Inicializa "x" como símbolo
    f = sympify(func) #Se traduce el string "func" a una función de sympy
    df = f.diff(x) #Se calcula la derivada de "f"

    fx = lambdify(x, f, modules=['numpy']) #Se inicializa la función f(x)
    dfx = lambdify(x, df, modules=['numpy']) #Se inicializa la derivada de la función f(x)

    D = [] #Datos eje x (Iteraciones)
    A = [] #Datos eje y (Errores)

    cont = 1 #Se inicializa el contador

    #Verifica que la tolerancia no sea negativa
    if (tol < 0):
        return print("La tolerancia debe ser mayor que cero")

    while(cont <= iterMax):

        if(dfx(xk) == 0): #Indefinición de denominador
            break

        xk = xk - ((fx(xk))/(dfx(xk)))
        y = fx(xk)

        D.append(cont)
        A.append(y)

        if(np.absolute(y) < tol):
            break

        cont += 1 #Incrementa contador

    return [xk, np.absolute(y), cont, D, A] #Retorna la aproximación del cero de la función, el error, y la cantidad de iteraciones

def secante(func, x0, x1, tol, iterMax):

    #Función de prueba: "cos(2*x)**2 - x**2", 3/4, 1/2, 10**(-10), 100

    x = Symbol('x') #Inicializa "x" como símbolo
    f = sympify(func) #Se traduce el string "func" a una función de sympy
    fx = lambdify(x, f, modules=['numpy']) #Se inicializa la función f(x)

    cont = 1 #Se inicializa el contador

    D = [] #Datos eje x (Iteraciones)
    A = [] #Datos eje y (Errores)

    #Verifica que la tolerancia no sea negativa
    if (tol < 0):
        return print("La tolerancia debe ser mayor que cero")

    while(cont <= iterMax):

        if((fx(x1)-fx(x0)) == 0): #Indefinición de denominador
            break

        xk = x1 - ((x1-x0)/(fx(x1)-fx(x0)))*fx(x1) #Se calcula xk
        error = np.absolute(fx(xk)) #Se calcula el error

        D.append(cont)
        A.append(error) #Se añade el error a la lista de errores

        if(error < tol):
            break

        x0 = x1 #Reasigna x0
        x1 = xk #Reasigna x1

        cont += 1 #Incrementa contador

    return [xk, np.absolute(fx(xk)), cont, D, A] #Retorna xk, las iteraciones, y f(x)

def graphicPlot(D, A):

    #Funcion utilizada para graficar
    plt.figure("Grafica")
    plt.plot(np.array(D), np.array(A))
    plt.title("Gráfico Iteraciones - Errores")
    plt.xlabel('Iteraciones')
    plt.ylabel('Error')
    plt.legend(["Error"])
    plt.show()

def bolzano(x0,x1):

    #Verificacion del teorema de bolzano
    if(x0*x1 < 0):
        return True
    else:
        return False


import numpy as np

A = np.array([[2, -3, -1], [3, 2, -5], [2, 4, -1]])
print('A:', A)

b = np.array([3, -9, -5])
print('b:', b)

n = len(b)
x = np.zeros(n)

for k in range(n - 1):
    for i in range(k + 1, n):
        if A[i, k] == 0:continue
        factor = A[k, k] / A[i, k]
        for j in range(k, n):
            A[i, j] = A[k, j] - A[i, j] * factor
        b[i] = b[k] - b[i] * factor

x[n - 1] = b[n - 1] / A[n - 1, n - 1]
for i in range(n - 2, -1, -1):
    sum_Ax = 0
    for j in range(i + 1, n):
        sum_Ax += A[i, j] * x[j]
    x[i] = (b[i] - sum_Ax) / A[i, i]

print('***SOLUTION***')
print('x:', x)


from sympy import *
import math

def newton_iter(f, x_0, eps=0, eta=0, df=None, max_steps=20, frac=False, verbose=False):
    """牛顿迭代法

    Args:
        f: function, 迭代函数
        x_0: float, 初值
        eps: float, 根的容许误差, default 0.
        eta: float, abs(f(x)) 的容许误差, default 0.

        df: function, f 的导函数, 默认 None 表示自动调用 sympy.diff 求导(会导致后续迭代中使用分数运算)。
        max_steps: int, 最大迭代次数, default 20.
        frac: bool, True 则输出分数(仅对 df=None 时生效)，否则使用 float, default False.
        verbose: bool, 打印出每一步的值, default False.

    Returns:
        x_final: float, 最终的近似根 x
    """

    if df == None:
        __x = Symbol('x')
        __df = diff(f(__x), __x)

        df = lambda x: __df.subs(__x, x)

    x = x_0
    for i in range(max_steps):
        if verbose:
            print(i, x)

        x_next = x - f(x) / df(x)

        if abs(x_next - x) <= eps or abs(f(x)) <= eta:
            break

        x = x_next

    if not frac:
        x = float(x)
    return x


"""
  Name     : c11_17_modifed_VaR_one_day.py
  Book     : Python for Finance (2nd ed.)
  Publisher: Packt Publishing Ltd.
  Author   : Yuxing Yan
  Date     : 6/6/2017
  email    : yany@canisius.edu
             paulyxy@hotmail.com
"""
import numpy as np
import pandas as pd
from scipy.stats import stats,norm
from matplotlib.finance import quotes_historical_yahoo_ochl as getData
#
ticker='WMT'            # input 1
n_shares=500            # input 2
confidence_level=0.99   # input 3
begdate=(2000,1,1)      # input 4
enddate=(2016,12,31)    # input 5
#
# Method I: based on the first two moments
z=abs(norm.ppf(1-confidence_level))
x=getData(ticker,begdate,enddate,asobject=True,adjusted=True)
ret = x.aclose[1:]/x.aclose[:-1]-1
position=n_shares*x.close[0]
mean=np.mean(ret)
std=np.std(ret)
VaR1=position*(mean-z*std)
print("Holding=",round(position,2), "VaR1=", round(VaR1,2), "for 1 day ")
#
# Modified VaR: based on 4 moments
s=stats.skew(ret)
k=stats.kurtosis(ret)
t=z+1/6.*(z**2-1)*s+1/24.*(z**3-3*z)*k-1/36.*(2*z**3-5*z)*s**2
mVaR=position*(mean-t*std)
print("Holding=",round(position,2), "modified VaR=", round(mVaR,2), "for 1 day ")


import numpy as np
import matplotlib.pyplot as plt

# rosenbrock function
# from http://docs.scipy.org/doc/scipy-0.14.0/reference/tutorial/optimize.html
def rosenbrock(x):
  return sum(100.0*(x[1:]-x[:-1]**2.0)**2.0 + (1-x[:-1])**2.0)

ub = 3.0
lb = -3.0

d = 2 # dimension of decision variable space
num_seeds = 30

popsize = 10
CR = 0.9 # crossover probability
F = 0.9 # between 0 and 2, vector step
max_NFE = 10000 # should be a multiple
convergence_threshold = 10**-6
nfe_to_converge = []

# differential evolution (a simple version)
for seed in range(num_seeds):
  np.random.seed(seed)

  # random initial population (popsize x d matrix)
  P = np.random.uniform(lb, ub, (popsize,d))
  f = np.zeros(popsize) # we'll evaluate them later
  nfe = 0
  f_best, x_best = 9999999, None
  ft = []

  while nfe < max_NFE and f_best > convergence_threshold:

    # for each member of the population ..
    for i,x in enumerate(P):

      # pick two random population members
      # "x" will be the one we modify, but other variants
      # will always modify the current best solution instead
      xb,xc = P[np.random.randint(0, popsize, 2), :]
      v = x + F*(xb-xc) # mutant vector

      # crossover: either choose from x or v
      trial_x = np.copy(x)
      for j in range(d):
        if np.random.rand() < CR:
          trial_x[j] = v[j]

      f[i] = rosenbrock(x)
      trial_f = rosenbrock(trial_x)
      nfe += 1

      # if this is better than the parent, replace
      if trial_f < f[i]:
        P[i,:] = trial_x
        f[i] = trial_f

    # keep track of best here
    if f_best is None or f.min() < f_best:
      f_best = f.min()
      x_best = P[f.argmin(),:]

    ft.append(f_best)

  nfe_to_converge.append(nfe)
  fta = np.array(ft)
  plt.subplot(1,2,1)
  plt.semilogy(range(popsize,nfe+1,popsize), fta, color='steelblue', linewidth=1)
  plt.subplot(1,2,2)
  plt.plot(range(2*popsize,nfe+1,popsize), fta[1:]/fta[:-1], color='steelblue', linewidth=1)
  del ft[:]

print(nfe_to_converge)

plt.subplot(1,2,1)
plt.xlabel('NFE')
plt.ylabel('Objective Value')

plt.subplot(1,2,2)
plt.xlabel('NFE')
plt.ylabel('$f_{t} / f_{t-1}$')
plt.show()


"""

https://en.wikipedia.org/wiki/Routh%E2%80%93Hurwitz_stability_criterion
http://et.engr.iupui.edu/~skoskie/ECE680/Routh.pdf
http://control.asu.edu/Classes/MAE318/318Lecture10.pdf
http://blog.codelv.com/2013/02/control-systems-in-python-part-2.html

"""

from sympy import *
from sympy.abc import *

"""

A system is considered stable if the poles of its transfer functions all have negative real part
A way to figure this out for large polynomials is to use the routh table

A routh table is the following table
s^n    a0 a2 a4 a6 ...
s^n-1  a1 a3 a5 a7 ...
s^n-2  b1 b2 b3 b4 ...
s^n-3  c1 c2 c3 c4 ...
...
s^2    e1 e2
s^1    f1
s^0    g0

Where the coefficients are
b1 = (a1*a2-a0*a3)/a1
b2 = (a1*a4-a0*a5)/a1
b3 = (a1*a6-a0*a7)/a1
...
c1 = (b1*a3-a1*b2)/b1
c2 = (b1*a5-a1*b3)/b1
c3 = (b1*a7-a1*b4)/b1
...
d1 = (c1*b2-b1*c2)/c1
d2 = (c1*b3-b1*c3)/c1

Then verify the table has all positive coefficients in the first row (all coefficients of a are positive)
and all the coefficient in the first column be positive, if there are any sign changes in the column
they tell us that there is a root that has positive real part (make it unstable)

"""

# does not work with all zero rows
def routh_hurwitz(Ts,*args):
    den = Poly(Ts.as_numer_denom()[1],s).all_coeffs()
    n = len(den)-1
    if n < 2:
        return None
    m = zeros(n + 1)

    c = [den[i] for i in range(0, n + 1, 2)]
    for i in range(0,len(c)):
        m[0, i] = c[i]

    c = [den[i] for i in range(1, n + 1, 2)]
    for i in range(0,len(c)):
        m[1, i] = c[i]

    for i in range(2, n+1):
        for j in range(1, n):
            if m[i-1,j] == 0:
                m[i, j-1] = m[i-2, j]
            elif m[i-1, 0] != 0:
                m[i, j-1] = ((m[i-1, 0]*m[i-2, j]-m[i-2, 0]*m[i-1, j])/m[i-1, 0]).together().simplify()

    m = m.col_insert(0, Matrix(([s**(n-i) for i in range(0,n+1)])))
    return m

# stable
pprint(routh_hurwitz(1/(s**4 + 2*s**3 + 3*s**2 + s + 1)))
pprint(routh_hurwitz(1/(s*s*s + a*s*s + b*s + c)))
pprint(routh_hurwitz((k*s**2 + s + 1)/(s**4 + 2*s**3 + (3+k)*s**2 + (1+k)*s + (1+k))))

# unstable
pprint(routh_hurwitz(1/(s**3 + s**2 + s + 2)))
pprint(routh_hurwitz(1000/(s**3 + 10*s**2 + 31*s + 1030)))

# should fail on this one since it has some all zeros row
pprint(routh_hurwitz(1/(s**5 + 2*s**4 + 24*s**3 + 48*s**2 - 50)))


import math
import multiprocessing as mp
import random
import sys
import time

import numpy as np
import pandas as pd

# List comprehension

a = [random.randint(1, 100) for _ in range(1000000)]
b = [random.randint(1, 100) for _ in range(1000000)]

start_time = time.perf_counter()

[x * y for x, y in zip(a, b)]

print(f"--- List comprehension ---\n{time.perf_counter() - start_time} seconds ")

# Vectorized with numpy

a = np.array([random.randint(1, 100) for _ in range(1000000)])
b = np.array([random.randint(1, 100) for _ in range(1000000)])

start_time = time.perf_counter()

a * b

# Harversine formula (great circle distance of two points on Earth)

def haversine(row):
    """
    Non-vectorized harversine
    """
    a_lat, a_lng, b_lat, b_lng = row
    R = 6371  # earth radius in km

    a_lat = math.radians(a_lat)
    a_lng = math.radians(a_lng)
    b_lat = math.radians(b_lat)
    b_lng = math.radians(b_lng)

    d_lat = b_lat - a_lat
    d_lng = b_lng - a_lng

    a = math.pow(math.sin(d_lat / 2), 2) + math.cos(a_lat) * math.cos(b_lat) * math.pow(
        math.sin(d_lng / 2), 2
    )
    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))

    return R * c

n_values = 10000

coords = np.array(
    [
        np.random.uniform(-90, 90, n_values),  # random origin latitude
        np.random.uniform(-180, 180, n_values),  # random origin longitude
        np.random.uniform(-90, 90, n_values),  # random destination latitude
        np.random.uniform(-180, 180, n_values), # random destination longitude
    ]
).T

start_time = time.perf_counter()

np.apply_along_axis(haversine, 1, coords)

print(
    f"--- Non-vectorized harversine ---\n{time.perf_counter() - start_time} seconds "
)

def vec_haversine(a_lat, a_lng, b_lat, b_lng):
    """
    Vectorized harversine
    """
    R = 6371  # earth radius in km

    a_lat = np.radians(a_lat)
    a_lng = np.radians(a_lng)
    b_lat = np.radians(b_lat)
    b_lng = np.radians(b_lng)

    d_lat = b_lat - a_lat
    d_lng = b_lng - a_lng

    d_lat_sq = np.sin(d_lat / 2) ** 2
    d_lng_sq = np.sin(d_lng / 2) ** 2

    a = d_lat_sq + np.cos(a_lat) * np.cos(b_lat) * d_lng_sq
    c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a))

    return R * c  # returns distance between a and b in km

start_time = time.perf_counter()

vec_haversine(coords[:, 0], coords[:, 1], coords[:, 2], coords[:, 3])

print(
    f"--- Vectorized harversine ---\n{time.perf_counter() - start_time} seconds "
)

# Harversine with pandas

df_coords = pd.DataFrame(
    coords,
    columns=["origin_lat", "origin_lng", "destination_lat", "destination_lng"],
)

start_time = time.perf_counter()

vec_haversine(
    df_coords.origin_lat,
    df_coords.origin_lng,
    df_coords.destination_lat,
    df_coords.destination_lng,
)

print(
    f"--- Vectorized harversine on dataframe ---\n{time.perf_counter() - start_time} seconds "
)

# Multi-processing

def process_chunk(proc_chunk):
    chunk_res = vec_haversine(
        proc_chunk.origin_lat,
        proc_chunk.origin_lng,
        proc_chunk.destination_lat,
        proc_chunk.destination_lng,
    )
    chunk_res.index = proc_chunk.index

    return chunk_res

n_proc = mp.cpu_count()

start_time = time.perf_counter()

chunksize = len(df_coords)

proc_chunks = []
for i_proc in range(n_proc):
    chunkstart = i_proc * chunksize
    # make sure to include the division remainder for the last process
    chunkend = (i_proc + 1) * chunksize if i_proc < n_proc - 1 else None

    proc_chunks.append(df_coords.iloc[slice(chunkstart, chunkend)])

# check whether all data are assigned to a process
assert sum(map(len, proc_chunks)) == len(df_coords)

with mp.Pool(processes=n_proc) as pool:
    # starts the sub-processes without blocking
    # pass the chunk to each worker process
    proc_results = [
        pool.apply_async(process_chunk, args=(chunk,)) for chunk in proc_chunks
    ]

    # blocks until all results are fetched
    result_chunks = [r.get() for r in proc_results]

results = pd.concat(result_chunks)
results = pd.concat((df_coords, results), axis=1)
# make sure we got a result for each coordinate pair:
assert len(results) == len(df_coords)

print(f"--- Parallel processes ---\n{time.perf_counter() - start_time} seconds ")


import numpy as np

def Normalization(nd, x):
    '''
    Normalization of coordinates (centroid to the origin and mean distance of sqrt(2 or 3).

    Input
    -----
    nd: number of dimensions, 3 here
    x: the data to be normalized (directions at different columns and points at rows)
    Output
    ------
    Tr: the transformation matrix (translation plus scaling)
    x: the transformed data
    '''

    x = np.asarray(x)
    m, s = np.mean(x, 0), np.std(x)
    if nd == 2:
        Tr = np.array([[s, 0, m[0]], [0, s, m[1]], [0, 0, 1]])
    else:
        Tr = np.array([[s, 0, 0, m[0]], [0, s, 0, m[1]], [0, 0, s, m[2]], [0, 0, 0, 1]])

    Tr = np.linalg.inv(Tr)
    x = np.dot( Tr, np.concatenate( (x.T, np.ones((1,x.shape[0]))) ) )
    x = x[0:nd, :].T

    return Tr, x

def DLTcalib(nd, xyz, uv):
    '''
    Camera calibration by DLT using known object points and their image points.

    Input
    -----
    nd: dimensions of the object space, 3 here.
    xyz: coordinates in the object 3D space.
    uv: coordinates in the image 2D space.

    The coordinates (x,y,z and u,v) are given as columns and the different points as rows.

    There must be at least 6 calibration points for the 3D DLT.

    Output
    ------
     L: array of 11 parameters of the calibration matrix.
     err: error of the DLT (mean residual of the DLT transformation in units of camera coordinates).
    '''
    if (nd != 3):
        raise ValueError('%dD DLT unsupported.' %(nd))

    # Converting all variables to numpy array
    xyz = np.asarray(xyz)
    uv = np.asarray(uv)

    n = xyz.shape[0]

    # Validating the parameters:
    if uv.shape[0] != n:
        raise ValueError('Object (%d points) and image (%d points) have different number of points.' %(n, uv.shape[0]))

    if (xyz.shape[1] != 3):
        raise ValueError('Incorrect number of coordinates (%d) for %dD DLT (it should be %d).' %(xyz.shape[1],nd,nd))

    if (n < 6):
        raise ValueError('%dD DLT requires at least %d calibration points. Only %d points were entered.' %(nd, 2*nd, n))

    # Normalize the data to improve the DLT quality (DLT is dependent of the system of coordinates).
    # This is relevant when there is a considerable perspective distortion.
    # Normalization: mean position at origin and mean distance equals to 1 at each direction.
    Txyz, xyzn = Normalization(nd, xyz)
    Tuv, uvn = Normalization(2, uv)

    A = []

    for i in range(n):
        x, y, z = xyzn[i, 0], xyzn[i, 1], xyzn[i, 2]
        u, v = uvn[i, 0], uvn[i, 1]
        A.append( [x, y, z, 1, 0, 0, 0, 0, -u * x, -u * y, -u * z, -u] )
        A.append( [0, 0, 0, 0, x, y, z, 1, -v * x, -v * y, -v * z, -v] )

    # Convert A to array
    A = np.asarray(A)

    # Find the 11 parameters:
    U, S, V = np.linalg.svd(A)

    # The parameters are in the last line of Vh and normalize them
    L = V[-1, :] / V[-1, -1]
    print(L)
    # Camera projection matrix
    H = L.reshape(3, nd + 1)
    print(H)

    # Denormalization
    # pinv: Moore-Penrose pseudo-inverse of a matrix, generalized inverse of a matrix using its SVD
    H = np.dot( np.dot( np.linalg.pinv(Tuv), H ), Txyz )
    print(H)
    H = H / H[-1, -1]
    print(H)
    L = H.flatten(0)
    print(L)

    # Mean error of the DLT (mean residual of the DLT transformation in units of camera coordinates):
    uv2 = np.dot( H, np.concatenate( (xyz.T, np.ones((1, xyz.shape[0]))) ) )
    uv2 = uv2 / uv2[2, :]
    # Mean distance:
    err = np.sqrt( np.mean(np.sum( (uv2[0:2, :].T - uv)**2, 1)) )

    return L, err

def DLT():
    # Known 3D coordinates
    xyz = [[-875, 0, 9.755], [442, 0, 9.755], [1921, 0, 9.755], [2951, 0.5, 9.755], [-4132, 0.5, 23.618],
    [-876, 0, 23.618]]
    # Known pixel coordinates
    uv = [[76, 706], [702, 706], [1440, 706], [1867, 706], [264, 523], [625, 523]]

    nd = 3
    P, err = DLTcalib(nd, xyz, uv)
    print('Matrix')
    print(P)
    print('\nError')
    print(err)

if __name__ == "__main__":
    DLT()


# 28 February 2018 Miroslav Gasparek
# Python tutorial on random number generation

import random
import numpy as np

import matplotlib.pyplot as plt
# Some pretty Seaborn settings
import seaborn as sns
rc={'lines.linewidth': 2, 'axes.labelsize': 18, 'axes.titlesize': 18}
sns.set(rc=rc)

# Generate sorted random numbers
x = np.sort(np.random.random(size=100000))
# Generate y-axis for CDF
y = np.arange(1, len(x)+1)/len(x)
# Plot CDF from random numbers (only 100 points for the plotting purposes)
fig1 = plt.figure(1)
plt.plot(x[::1000], y[::1000], marker='.', linestyle='none', markersize=10)
# Plot expected CDF (just a straight line from (0,0) to (1,1) )
plt.plot([0, 1], [0, 1], 'k--')
fig1.show()

# Doing 'coin flips'
# Generate 20 random numbers on uniform interval
# x = np.random.random(size=20)
# # Make them coin flips
# heads = x > 0.5
# # Show which were heads, and count the number of heads
# print(heads)
# print('\nThere were', np.sum(heads), ' heads.')

# Seed the RNG
np.random.seed(42)
# Generate random numbers
rand_ar1 = np.random.random(size=10)

# Re-seed the RNG
np.random.seed(42)
# Generate random numbers
rand_ar2 = np.random.random(size=10)

# Seed with a number that is close to the answer to everything
np.random.seed(43)
rand_ar3 = np.random.random(size=10)

# Set parameters
mu = 10
sigma = 1
# Draw 10000 random samples
x = np.random.normal(mu, sigma, size=10000)
# Plot a histogram of our draws
fig2 = plt.figure(2)
_ = plt.hist(x, bins=100)
fig2.show()

sample_mean = np.mean(x)
sample_std = np.std(x)

# Selections from discrete distributions
# Draw random integers on [0, 4), i. e., exclusive of last one.
rand_ar_int = np.random.randint(0, 4, 20)

# This can be used to generate random DNA sequences
# Key of bases
bases = 'ATGC'
# Draw random numbers for sequence
x = np.random.randint(0, 4, 50)
# Make sequence
seq_list = [None]*50
for i,b in enumerate(x):
    seq_list[i] = bases[b]
# Join the sequence
seq = ''.join(seq_list)

# Choosing elements from an array
# Choose elements from array without replacement
rand_ar4 = np.random.choice(np.arange(51), 20, replace=False)
print(rand_ar4)
# Shuffiling an array
rand_ar5 = np.random.permutation(np.arange(53))
print('\n', rand_ar5)

# Random module
# Make sequence
seq_list = [None]*50
for i in range(len(seq_list)):
    seq_list[i] = random.choice('ATGC')

# Join the sequence
seq2 = ''.join(seq_list)


from scipy.stats import moment
import math as math
import mgt2001.per as per

def interval(df):
    upl_1 = round(df.mean() + df.std(), 4)
    upl_2 = round(df.mean() + 2 * df.std(), 4)
    upl_3 = round(df.mean() + 3 * df.std(), 4)
    lowl_1 = round(df.mean() - df.std(), 4)
    lowl_2 = round(df.mean() - 2 * df.std(), 4)
    lowl_3 = round(df.mean() - 3 * df.std(), 4)
    result = f"""1 std limits = ({lowl_1}, {upl_1})
2 std limits = ({lowl_2}, {upl_2})
3 std limits = ({lowl_3}, {upl_3})
    """
    print(result)

def quartiles(data, base=per, show=False):
    """
    default show = False

    Return Q1, Q2, Q3, IQR
    """
    Q1 = base.percentile(data, 25)
    Q2 = base.percentile(data, 50)
    Q3 = base.percentile(data, 75)
    IQR = Q3 - Q1  # IQR is interquartile range.
    description = """
Q1 = {}
Q2 = {}
Q3 = {}
IQR = {}
    """.format(Q1, Q2, Q3, IQR)
    if show:
        print(description)

    return Q1, Q2, Q3, IQR

def outlier(data, base=per, show=True):
    """
    The default is set to per.percentile()
    default show = True

    Return a list of outliers and description

    Usages
    ------
    >>> df = whatever DataFrame you construct
    # to use per.percentile (textbook) to compute quartiles
    >>> per.outlier(df["column_name"], per)
    # to use np.percentile to compute quartiles
    >>> per.outlier(df["column_name"], np)

    Examples
    ------
    In HW04, 4.73, the results could be different:
    >>> per.outlier(df["Time_Public"].dropna(), np)
    Quartiles for playing on a public course:

    Q1 = 279.5
    Q2 = 296.0
    Q3 = 307.0
    IQR = 27.5

    Outliers are listed as follows:
    [238.0, 359.0]

    >>> per.outlier(df["Time_Public"].dropna()) # default = per
    Quartiles for playing on a public course:

    Q1 = 279.0
    Q2 = 296.0
    Q3 = 307.0
    IQR = 28.0

    Outliers are listed as follows:
    [359.0]

    """
    # Q1 = base.percentile(data, 25)
    # Q2 = base.percentile(data, 50)
    # Q3 = base.percentile(data, 75)
    # IQR = Q3 - Q1  # IQR is interquartile range.
    Q1, Q2, Q3, IQR = quartiles(data, base=base)
    filter = (data < Q1 - 1.5 * IQR) | (data > Q3 + 1.5 * IQR)
    if (len(data.loc[filter].to_list()) != 0):
        outlier_prompt = "Outliers are listed as follows:\n{}".format(
            data.loc[filter].to_list())
    else:
        outlier_prompt = "There are no outliers."

    description = """
Q1 = {}
Q2 = {}
Q3 = {}
IQR = {}

{}
    """.format(Q1, Q2, Q3, IQR, outlier_prompt)
    if show:
        print(description)

    if (len(data.loc[filter].to_list()) != 0):
        return data.loc[filter].to_list(), description
    else:
        return list(), description

def kurtosis(df):
    """
    How to read the kurtosis value?

    K > 3 : Leptokurtic (Narrow-tall)
    K = 3 : Mesokurtic (Regular)
    K < 3 : Platykurtic (Wide-low)
    -------
    To convert excel kurtosis to a real one, please consider the function "convert_excel_kurtosis(K, n)"

    """
    m2 = moment(df, moment=2)
    m4 = moment(df, moment=4)
    kurtosis_f = m4 / pow(m2, 2)
    return kurtosis_f

def convert_excel_kurtosis(K, n):
    """
    K is the kurtosis you get from Excel.

    The function will return (n-2) * (n-3) * K / ((n + 1) * (n - 1)) + 3 * (n - 1) / (n + 1).
    """
    if (n < 3):
        return None
    return (n-2) * (n-3) * K / ((n + 1) * (n - 1)) + 3 * (n - 1) / (n + 1)

def skew(df):
    """
    How to read skew value?

    g > 0 : Skewed to right
    g = 0 : Symmetric
    g < 0 : Skewed to left
    -------
    To convert excel skewness to a real one, please consider the function "convert_excel_skew(G, n)"
    """
    m2 = moment(df, moment=2)
    m3 = moment(df, moment=3)
    skew_f = m3 / pow(pow(m2, 0.5), 3)
    return skew_f

def convert_excel_skew(G, n):
    """
    G is the skewness you get from Excel.

    The function will return (n-2) * G / math.sqrt(n * (n - 1)).
    """
    if (n < 2):
        return None
    return (n-2) * G / math.sqrt(n * (n - 1))


import time
import numpy as np
from scipy.special import eval_legendre as lp
import matplotlib.pyplot as plt
#
def polleg(x, kmax):
    '''
    Purpose:
	    To compute the Legendre polynomials, Pk(x), for all orders k=0:kmax
    In:
        x     d[nx]          abscissas, x = [-1:+1], nx=len(x)
	    kmax  i[1]           maximum order, k = 0,1,2...kmax
    Out:
	    pk    [nx, kmax+1]   Legendre polynomials
    Tree:
	    -
    Comments:
        Q&A:

        Q1 shall we add theory/equations like this (will not be possible for the RT solver) >>>
	    Pk(x) = Qkm(x) for m = 0. The Bonnet recursion formula [1, 2]:
	    (k+1)P{k+1}(x) = (2k+1)*P{k}(x) - k*P{k-1}(x),                           (1)
	    where k = 0:K, P{0}(x) = 1.0, P{1}(x) = x.
	    For fast summation over k, this index changes first.

        Q2: name of the subroutine - why not 'legendre', 'legpol', 'lp', etc...
        A2: Later we will have other polynomials, so i belive it is a good idea to
        start with 'pol' and then specify the type of the polynomial: 'polleg', 'polqkm', 'polrtm'...
    References:
	    1. https://en.wikipedia.org/wiki/Legendre_polynomials
	    2. http://mathworld.wolfram.com/LegendrePolynomial.html
        3. https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.legendre.html
        4. https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.eval_legendre.html
    '''
    nx = len(x)
    nk = kmax+1
    pk = np.zeros((nk, nx))
    if kmax == 0:
        pk[0,:] = 1.0
    else:
        pk[0,:] = 1.0
        pk[1,:] = x
        for ik in range(2,nk):
            pk[ik,:] = (2.0 - 1.0/ik)*x*pk[ik-1,:] - (1.0 - 1.0/ik)*pk[ik-2,:]
    return pk
#==============================================================================
#
if __name__ == "__main__":
    x = np.linspace(-1.0, 1.0, 201)
    nx = len(x)
    fig, axs = plt.subplots(nrows=1, ncols=2)
#
#   Plot a few first orders
    axs[0].plot(x, lp(0, x), 'r', x, lp(1, x), 'g', x, lp(2, x), 'b', x, lp(10, x), 'k')
    axs[0].set_title('python')
    axs[0].grid(True)
#
    pk = polleg(x, 10)
    axs[1].plot(x, pk[0,:], 'r', x, pk[1,:], 'g', x, pk[2,:], 'b', x, pk[10,:], 'k')
    axs[1].set_title('home-cooked')
    axs[1].grid(True)
#
#   Compare for all k=0:kmax & all x
    kmax = 999
    time_start = time.time()
    pk = polleg(x, kmax)
    time_end = time.time()
    print('polleg runtime = %.1f sec.'%(time_end-time_start))
    pk_py = np.zeros_like(pk)
    time_start = time.time()
    for ik in range(kmax+1):
        pk_py[ik,:] = lp(ik, x)
    time_end = time.time()
    print("python's runtime = %.1f sec."%(time_end-time_start))
    print("max absolute difference = %.1e."%np.amax(np.abs(pk - pk_py)))


# -*- coding: utf-8 -*-
"""
Módulo de diferencias finitas en Python 3.x
"""

# Escribire los objetos que me interese importar
from numpy import diag, ones, linspace, array

def laplace1d(f, ua, ub, n, a=0, b=1):
    """
    Esta función resuelve el problema de Laplace 1d
        u''  = f     en [a,b]
        u(a) = ua
        u(b) = ub
    mediante el método de las diferencias finitas
    """
    h = (b-a)/(n+1) # Tamaño de la partición
    # 1. Matriz
    A_h = (1./h**2) * (
            2*diag( ones(n) )
            - diag( ones(n-1), +1 )
            - diag( ones(n-1), -1)
            )
    # 2. Segundo miembro
    f_h = []
    x = linspace(0, 1, num=n+2) # x_0, ..., x_{n-1}
    x_interior = x[1:n+1]

    f_h = f(x_interior) # f_h es el array resultante de aplicar f a cada elmento del array x
    f_h[0]  += ua/h**2
    f_h[-1] += ub/h**2

    # 3. Resolver sistema
    from numpy.linalg import solve
    u_h = solve(A_h, f_h)

    # Concatenamos la solución con los datos en los extremos del intervalo
    u = array( [ua] + list(u_h) + [ub] )

    # Devolvemos la partición x y la solución obtenida
    return x, u

def calor_1d_EulerImp(f, ua, ub, u0, n, k, a=0, b=1):
    """
    Esta función resuelve el problema de Laplace 1d
        u''  = f     en [a,b]
        u(a) = ua
        u(b) = ub
    mediante el método de las diferencias finitas
    """
    h = (b-a)/(n+1) # Tamaño de la partición
    # 1. Matriz
    A_h = (k/h**2) * (
            2*diag( ones(n) )
            - diag( ones(n-1), +1 )
            - diag( ones(n-1), -1)
            )
    # 2. Segundo miembro
    f_h = []
    x = linspace(0, 1, num=n+2) # x_0, ..., x_{n-1}
    x_interior = x[1:n+1]

    f_h = f(x_interior) # f_h es el array resultante de aplicar f a cada elmento del array x
    f_h[0]  += ua/h**2
    f_h[-1] += ub/h**2

    # 3. Resolver sistema
    from numpy.linalg import solve
    u_h = solve(A_h, f_h)

    # Concatenamos la solución con los datos en los extremos del intervalo
    u = array( [ua] + list(u_h) + [ub] )

    # Devolvemos la partición x y la solución obtenida
    return x, u