import numpy as np
import random
import cmath
import math
import copy
import time

#generate two large odd numbers of a specific bit size
def generateLargeOdd(bitSize):
    a = random.getrandbits(128) | 1
    #print(a)
    return a

#Primality check using Fermat Theorem to identify base 2 pseudoprimes
def checkPseudoprimeFermat(num):
    base = 2
    if (checkPrimeModularExponentiation(num-1, base, num)) == 1:
        return True #returns true if prime
    return False #returns false if composite

#Primality check using Modular Exponentiation to the base 2
def checkPrimeModularExponentiation(num2, base, num):
    res = 1
    base%=num
    if (base == 0) :
        res = 0
        return res
    while (num2 > 0) :
        #print("base:", base)
        if ((int(num2) & 1) == 1) :
            res = (res * base) % num
        base = (base * base) % num
        num2 = int(num2) >> 1 # num2 = num/2
    return res #if res is 1 or n-1 it is prime

#Helper function for Miller Rabin test
def millerHelper(d, n):
    a = 2 + random.randint(1, n - 4)
    #print("base:", a)
    x = checkPrimeModularExponentiation(d, a, n)
    if (x == 1 or x == n - 1):
        return True
    while (d != n - 1):
        x = (x * x) % n
        d *= 2
        if (x == 1):
            return False
        if (x == n - 1):
            return True
    return False

#Primality check using Miller Rabin test
def checkPrimeMillerRabin(n):
    k = 4 #no. of iterations
    if (n <= 1 or n == 4):
        return False
    if (n <= 3):
        return True
    d = n - 1
    while (d % 2 == 0):
        d //= 2
    for i in range(k): #Iterating k times
        if not millerHelper(d, n):
            return False
    return True

#Primality check using Trial Division
def checkPrimeTrialDivision(a):
    for i in range(2, math.ceil(pow(a, 0.5))):
        if a%i == 0:
            return False
    return True

#Find relative prime of a number
def relativePrime(p,q):
    phi = (p-1)*(q-1)
    for e in range(3, phi, 2):
        if phi%e != 0:
            return e

#Extended Euclid
def egcd(a, b):
    s = 0; old_s = 1
    t = 1; old_t = 0
    r = b; old_r = a
    while r != 0:
        quotient = old_r // r
        old_r, r = r, old_r - quotient * r
        old_s, s = s, old_s - quotient * s
        old_t, t = t, old_t - quotient * t
    return old_r, old_s, old_t # return gcd, x, y

#To find modular multiplicative inverse of e
def modularMultiplicativeInverse(e, p, q):
    phi = (p-1)*(q-1)
    gcd, x, y = egcd(e, phi)
    if x < 0:
        x += phi
    return x

def rsaEncrypt(M, e, n):
    C = []
    for m in M:
        C_inner = []
        for i in m:
            ex = checkPrimeModularExponentiation(e, i, n)
            #ex = (pow(m, e))%n
            C_inner.append(ex)
        C.append(C_inner)
    return C

def rsaDecrypt(C, d, n):
    MD = []
    for c in C:
        MD_inner = []
        for i in c:
            de = checkPrimeModularExponentiation(d, i, n)
            #de = (pow(c, d))%n
            MD_inner.append(chr(de))
        MD_in = "".join(MD_inner)
        MD.append(MD_in)
    return MD

def rsaHelper(M):
    count = 0
    #a = 6967
    M_original = copy.deepcopy(M)
    M_ascii = []
    for i in range(len(M)):
        M[i] = str(M[i])
        ma = [ord(c) for c in M[i]]
        M_ascii.append(ma)
    largePrimes = []
    while(count<2):
        oddNum = generateLargeOdd(128)
        if count==1 and oddNum == largePrimes[0]: continue
        if checkPrimeModularExponentiation(oddNum-1, 2, oddNum)!=1:
            continue
        if not checkPseudoprimeFermat(oddNum):
            if not checkPrimeMillerRabin(oddNum):
                continue #continue searching if number is composite
        #if not checkPrimeTrialDivisionLoop(oddNum):
            #continue
        count+=1
        largePrimes.append(oddNum)

    a, b = largePrimes[0], largePrimes[1]
    #print("The two large prime numbers: ", a, b)
    #print("Original message M: ", M)
    n = a*b
    e = relativePrime(a, b)
    #print("e: ",e)
    d = modularMultiplicativeInverse(e, a, b)
    #print("d: ", d)
    C = rsaEncrypt(M_ascii, e, n)
    #print("CypherText C: ", C)
    MD = rsaDecrypt(C, d, n)
    #print("Decrypted message MD: ",MD)

    return MD

if __name__ == '__main__':

    VC = [5,7, 8, 10]
    #VC = [0j, (2+1.1102230246251565e-16j), (1.4997597826618576e-32-2.4492935982947064e-16j), (2+4.440892098500626e-16j)]
    VCD = rsaHelper(VC)
    print(np.allclose(VC, VCD))


'''
linear_algebra.py by Dominic Reichl, @domreichl
module for math_for_machine_learning.py that introduces vectors and matrices

key concepts:
1.1. vectors: magnitude, direction, addition
1.2. vector multiplication: scalar, dot product, cross product
2.1. matrices: addition, subtraction, negation, transposition, multiplication, inverse
2.2. systems of equations
3.1. eigenvectors and eigenvalues
'''

'''
PART 1: VECTORS

A vector is a numberic element that has magnitude and direction.
Vectors describe spatial lines and planes; they enable calculations
that explore multi-dimensional relationships.
'''

def vector_properties(x, y):
    '''
        Takes the elements of a vector with 2 dimensions as input.
        Plots the vector and returns its magnitude and direction.
    '''

    print('-'*30 + '\nVector Properties\n' + '-'*30)

    import numpy as np # for building an array
    import matplotlib.pyplot as plt # for plotting the vector
    from math import sqrt, atan, degrees, log # for calculating vector properties

    v = np.array([x, y]) # define vector as a 2d array
    print('Vector:', v)

    # calculate vector magnitude
    vMag = sqrt(x**2 + y**2) # raw equation
    vMagAlt = np.linalg.norm(v) # numpy equation
    vMag == vMagAlt # True
    print('Magnitude:', vMag)

    # calculate vector direction (vector angle as inverse tangent)
    if x > 0: vAtan = degrees(atan(y/x))
    elif x < 0 and y > 0: vAtan = degrees(atan(y/x)) + 180
    elif x < 0 and y < 0: vAtan = degrees(atan(y/x)) - 180
    vAtanAlt = np.degrees(np.arctan2(y, x)) # numpy equation (saves 2 lines of code)
    vAtan == vAtanAlt # True
    print('Direction:', vAtan)

    # plot vector as arrow in a coordinate system (quiver plot)
    plt.grid()
    plt.quiver([0],[0], *v, angles='xy', scale_units='xy', scale=1, color='green')
    plt.xlim(-10, 10); plt.ylim(-10, 10)
    plt.show()

    return vMag, vAtan

def vector_addition(v1, v2):
    '''
        Takes as input two 2d vectors as lists.
        Plots the vectors and their sum, then returns the latter.
    '''

    print('-'*30 + '\nVector Addition\n' + '-'*30)

    import numpy as np # for building arrays
    import matplotlib.pyplot as plt # for plotting vectors

    v1 = np.array(v1) # first vector as array
    v2 = np.array(v2) # second vector as array
    vSum = v1 + v2 # vector addition
    vectors = np.array([v1, v2, vSum])
    print('Vector 1:', v1)
    print('Vector 2:', v2)
    print('Vector sum:', vSum)

    # plot vectors as arrows (quiver plot)
    plt.grid()
    plt.quiver([0],[0], vectors[:,0], vectors[:,1], angles='xy', scale_units='xy', scale=1, color=['g', 'b', 'r'])
    plt.xlim(-10, 10); plt.ylim(-10, 10)
    plt.show()

    return vSum

def vector_multiplication(x, y):
    '''
        Takes as input 2d or 3d vector(s) as list(s) and/or number(s) as integer(s).
        Returns scalar, dot, or cross product as output.
    '''

    print('-'*30 + '\nVector Multiplication\n' + '-'*30)

    import numpy as np

    if type(x) == list and type(y) == int:
        product = np.array(x) * y # scalar product
        print('Vector:', np.array(x))
        print('Multiplier:', y)
        print('Scalar product:', product)
        return product
    elif type(x) == int and type(y) == list:
        product = x * np.array(y) # scalar product
        print('Multiplier:', y)
        print('Vector:', np.array(y))
        print('Scalar product:', product)
        return product
    elif type(x) == list and type(y) == list:
        v1 = np.array(x)
        v2 = np.array(y)
        print('Vector 1:', v1)
        print('Vector 2:', v2)
        if len(x) == 2:
            product = v1 @ v2 # dot product; alternative: np.dot(v1,v2)
            print('Dot product:', product)
            return product
        elif len(x) == 3:
            product = np.cross(v1,v2) # cross product
            print('Cross product:', product)
            return product

'''
PART 2: MATRICES

A matrix is an array of numbers that are arranged in rows and columns.
For example, a 2x3 matrix has two rows and three columns.
'''

def matrix_operations(A, B, operation='add'):
    '''
        Takes as input 2 matrices as nested lists and an operation.
        Possible operations:
            - addition ('add'; default)
            - subtraction ('sub')
            - negation ('neg')
            - transposition ('t')
            - multiplication ('mul')
            - division ('div'; inverse multiplication)
        Returns result of operation.
    '''

    print('-'*30 + '\nMatrix Operation\n' + '-'*30)

    import numpy as np

    A = np.matrix(A) # .matrix allows multiplication with * and inverse with .I
    B = np.matrix(B)

    if operation == 'add': return print(A+B) # sum of matrices
    if operation == 'sub': return print(A-B) # difference of matrices
    if operation == 'neg': return print(-A,-B) # negation of matrices
    if operation == 'transpose': return print(A.T,B.T) # transposition of matrices
    if operation == 'mul': # matrix multiplication if allowed by shapes
        if np.shape(A)[0] == np.shape(B)[1] and np.shape(A)[1] == np.shape(B)[0]:
            return print(A*B) # dot product; alternatives: A@B; np.dot(A,B)
        else:
            return print('These matrices cannot be multiplied.')
    if operation == 'div': # matrix inverse multiplication if allowed by shapes
        if np.shape(A)[0] == np.shape(B)[0] and np.shape(A)[1] == np.shape(B)[1]:
            return print(A*B.I) # dot product; alternative: A@np.linalg.inv(B)
        else:
            return print('These matrices cannot be divided.')

def solve_system_of_equations(xy, results):
    '''
        Takes as input 2 matrices as nested lists representing equations.
        For example, 'xy' might be [[2,4],[6,2]] for equations
        2x+4y and 6x+2y, and 'results' might be [[18,34]].
        Returns values for x and y that solve the equation system.
    '''

    print('\n' + '-'*30 + '\nSolving System of Equations\n' + '-'*30)

    import numpy as np

    xy = np.matrix(xy)
    results = np.matrix(results)

    XY = xy.I @ results # inverse multiplication
    x = float(XY[0][0])
    y = float(XY[1][0])

    print('x is', x, '\ny is', y)
    return x,y # return values for x and y as floats in a tuple

'''
PART 3: EIGENVECTORS AND EIGENVALUES

An eigenvalue is a scalar multiplier that produces an eigenvector
via linear transformation of a non-zero vector.
'''

def eigenvalues_eigenvectors(A):
    '''
        Takes as input a matrix as a nested list.
        Returns eigenvalues and eigenvectors.
    '''

    print('\n' + '-'*30 + '\nEigenvectors, Eigenvalues\n' + '-'*30)

    import numpy as np

    A = np.array(A) # turn list into array
    eVals, eVecs = np.linalg.eig(A) # get eigenvalues and eigenvectors

    print('Matrix:\n', A)
    print('Lambdas:', eVals[0], 'and', eVals[1])
    print('Vectors:', eVecs[:,0], 'and', eVecs[:,1])

    return eVals, eVecs


import numpy as np

class univariate_Linear_Regression:
    # initialize slope and intercept
    def __init__(self):
        self.m = 0.0  # slope
        self.b = 0.0  # intercept

# sum of square deviation for single variable
    def ss_x(self, x):
        return sum((x-np.mean(x))**2)

    # sum of square deviation for two variables x and y
    def ss_xy(self, x, y):
        x_mean = np.mean(x)			# mean of x
        y_mean = np.mean(y)			# mean of y
        return sum((x-x_mean)*(y-y_mean))

    # Train our regression model based on shape and size
    def train(self, x, y):

        # verify the features and labels are of same size
        assert(len(x) == len(y))

        # calculate the slope
        ss_x = self.ss_x(x)
        ss_xy = self.ss_xy(x, y)
        self.m = ss_xy/ss_x

        # calculate the intercept
        self.b = (np.mean(y)) - (self.m)*(np.mean(x))

    # return the predicted values based on feature and weights
    def predict(self, x):
        predictions = np.zeros(len(x))
        for i in range(len(x)):
            predictions[i] = self.m * x[i] + self.b         # Y = mx + b
        return predictions

# Dataset to train model
x = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
y = np.array([1.1, 1.9, 2.8, 4, 5.2, 5.8, 6.9, 8.1, 9, 9.9])

# Initialize our model
reg = univariate_Linear_Regression()

# Train our model with the data
reg.train(x, y)

# Make a prediction
print(reg.predict(x))


import numpy as np
import matplotlib.pyplot as plt
import random
import scipy.stats as stat
from scipy.optimize import curve_fit

def p(x,norm):                           #probability density function (PDF) for the small angle Rutherford distribution
    p = (norm*(2*x))/(1+x**2)**2
    return p

def c_inv(t):                            #inverse cumulative pdf c^-1(t)=x
    c=np.sqrt(t/(1-t))
    return c

N=10**6                             #number of extractions

#random sampling of Rutherford distribution
x=[]
for i in range (0, N):
    n=random.random()           #extract 10^6 random numb. between 0 and 1
    x.append(c_inv(n))
x=np.array(x)                #calculate the inverse of the cumulative in the random point previosly generated

#fit histogram with Rutherford ditribution
a=0
b=20
n_bins=1000                         #number of bins

hist, bins=np.histogram(x, bins=n_bins , density=False, range=(a,b))
z=np.array([0.5 * (bins[i] + bins[i+1]) for i in range(len(bins)-1)]) #bins center
fit, cov=curve_fit(p, xdata=z, ydata=hist, p0=(10**4))
norm_fit = fit
norm_err = np.sqrt(np.diag(cov))

#compute chi square
chi2, p_value = stat.chisquare(hist, p(z,norm_fit))
dof=n_bins-1
chi2_red=chi2/dof

#plot the rutherford formula histogram and its best fit curve
y=np.linspace(0,100, 10000)
plt.figure()
plt.hist(x, color='orange', bins=n_bins , histtype='step', range=(a,b))
plt.plot(y, p(y, norm_fit), 'k')
plt.xlabel('$x$', fontsize=12)
plt.ylabel('$Counts$', fontsize=12)
plt.yscale('log')
plt.xlim(0, 15, 3)
plt.ylim(1, 3*10**4)
plt.xticks(np.arange(0,16, 3), fontsize=10)
plt.yticks(fontsize=10)
props = dict(boxstyle='round', facecolor='orange', alpha=0.5)
plt.text(7, 5*10**3, '$\\chi^2$/ d.o.f = %.2f / %u = %.2f' %(chi2, dof, chi2_red), fontsize=10, bbox=props)


import numpy as np

def jacobi(A, B, x_init, epsilon=0.001, max_iterations=500):
    D = np.diag(np.diag(A))
    LU = A - D
    x = x_init
    for i in range(max_iterations):
        D_inv = np.diag(1 / np.diag(D)) #inverse diagonal
        x_new = np.dot(D_inv, B - np.dot(LU, x))
        if np.linalg.norm(x_new - x) < epsilon:
            return x_new
        x = x_new
        print(i, x)

a = np.array([
    [11, 2.6, 0.4, -3.3],
    [-4.1, 22, 2.2, -3.5],
    [-1.2, -15.7, 33, -0.1],
    [-7.7, -6.3, 4.8, 44]
])
b = np.array([51.27, -64.66, -104.96, -27.12])

x_init = np.zeros(len(b)) #starting vector
print("a:",a,"\n b:" ,b, "\n")
x = jacobi(a, b, x_init)
print("\nfinal:", x)


import numpy as np
import math

class BaseRegression(object):

    def __init__(self, n_iter, lr):
        self.n_iter = n_iter
        self.lr = lr
        self.w = []
        self.errors = []

    def init_weight(self, n_feat):
        bound = 1 / n_feat
        self.w = np.random.uniform(-bound, bound, (n_feat, ))

    def fit(self, X, y):

        X = np.insert(X, 0, 1, axis=1)
        self.init_weight(n_feat=X.shape[1])

        for it in range(self.n_iter):
            y_pred = X.dot(self.w)

            mse = np.mean(0.5 * (y_pred - y)**2)
            self.errors.append(mse)

            grad = (y_pred - y).dot(X)
            self.w -= self.lr * grad

    def predict(self, X):
        X = np.insert(X, 0, 1, axis=1)
        y_pred = X.dot(self.w)
        return y_pred

class LinearRegression(BaseRegression):

    def __init__(self, n_iter=1000, lr=0.01):
        super(LinearRegression, self).__init__(n_iter, lr)

    def fit(self, X, y):
        super(LinearRegression, self).fit(X, y)


import numpy as np
#import numba

def fft(f, x=None, **kwargs):
    """
    customized fourier transform of function f
    g = int dt f(t) * exp(- i * freq * t)

    Return:
        freq: frequencies where f are evaluated
        g: the fourier transform of f
    """
    nx = len(f)

    if x is None:
        x = np.arange(nx)

    dx = x[1] - x[0]

    g = np.fft.fft(f, **kwargs)
    g = np.fft.fftshift(g)
    g *= dx

    freq = 2. * np.pi * np.fft.fftshift(np.fft.fftfreq(nx, d=dx))

    return freq, g * np.exp(-1j * freq * x[0])

def ifft(f, x=None):
    """
    customized fourier transform of function f
    g = int dt f(t) * exp(i * freq * t)
    return:
        freq: frequencies where f are evaluated
        g: the fourier transform of f
    """
    nx = len(f)

    if x is None:
        x = np.arange(nx)

    dx = x[1] - x[0]

    g = np.fft.ifft(f)
    g = np.fft.ifftshift(g)
    g = g * dx /2./np.pi * len(x)
    freq = 2. * np.pi * np.fft.ifftshift(np.fft.fftfreq(nx, d=dx))

    return freq, g * np.exp(1j * freq * x[0])

def fft2(f, dx=1, dy=1):
    """
    customized FFT for 2D function
    input:
        f: 2d array,
            input array
    return:
        freq: 1d array
            frequencies
        g: 2d array
            fourier transform of f
    """
    nx, ny = f.shape

    g = np.fft.fft2(f)
    g = np.fft.fftshift(g)

    g = g * dx * dy

    freqx = 2. * np.pi * np.fft.fftshift(np.fft.fftfreq(nx, d=dx))
    freqy = 2. * np.pi * np.fft.fftshift(np.fft.fftfreq(nx, d=dy))

    return freqx, freqy, g

def dft(x, f, k):
    '''
    Discrete Fourier transfrom at specified momentum
    '''

    dx = (x[1] - x[0]).real

    g = np.zeros(len(k), dtype=np.complex128)

    for i in range(len(k)):
        g[i] = np.sum(f * np.exp(-1j * k[i] * x)) * dx

    return g

def dft2(x, y, f, kx, ky):
    '''
    Discrete Fourier transfrom at specified momentum
    '''

    dx = x[1] - x[0]
    dy = y[1] - y[0]

    X, Y = np.meshgrid(x, y)
    g = np.zeros((len(kx), len(ky)), dtype=complex)

    for i in range(len(kx)):
        for j in range(len(ky)):
            g[i, j] = np.sum(f * np.exp(-1j * kx[i] * X - 1j * ky[j] * Y)) * dx * dy

    return g


from sympy import *
import sys
sys.path.insert(1, '..')
from tait_bryan_R_utils import *
from rodrigues_R_utils import *
from quaternion_R_utils import *

xsl, ysl, zsl = symbols('xsl ysl zsl')
xtg, ytg, ztg = symbols('xtg ytg ztg')
px, py, pz = symbols('px py pz')
#om, fi, ka = symbols('om fi ka')
#sx, sy, sz = symbols('sx sy sz')
q0, q1, q2, q3 = symbols('q0 q1 q2 q3')
vxx, vxy, vxz = symbols('vxx vxy vxz')
vyx, vyy, vyz = symbols('vyx vyy vyz')
scale_x, scale_y = symbols('scale_x scale_y')

position_symbols = [px, py, pz]
#orientation_symbols = [om, fi, ka]
#orientation_symbols = [sx, sy, sz]
orientation_symbols = [q0, q1, q2, q3]
scale_symbols = [scale_x, scale_y]
all_symbols = position_symbols + orientation_symbols + scale_symbols

point_source_local = Matrix([xsl * scale_x, ysl * scale_y, zsl, 1]).vec()
#RT_wc = matrix44FromTaitBryan(px, py, pz, om, fi, ka)[:-1,:]
#RT_wc = matrix44FromRodrigues(px, py, pz, sx, sy, sz)[:-1,:]
RT_wc = matrix44FromQuaternion(px, py, pz, q0, q1, q2, q3)[:-1,:]
point_source_global = RT_wc * point_source_local
point_on_line_target_global = Matrix([xtg, ytg, ztg])

vxt = Matrix([[vxx, vxy, vxz]]).transpose()
vyt = Matrix([[vyx, vyy, vyz]]).transpose()

target_value = Matrix([0,0]).vec()
model_function = Matrix([vxt.dot(point_source_global - point_on_line_target_global),vyt.dot(point_source_global - point_on_line_target_global)]).vec()
delta = target_value - model_function
delta_jacobian=delta.jacobian(all_symbols)

print(delta)
print(delta_jacobian)

with open("rectangular_object_with_unknown_width_height_quaternion_wc_jacobian.h",'w') as f_cpp:
    f_cpp.write("inline void rectangular_object_with_unknown_width_height_quaternion_wc(Eigen::Matrix<double, 2, 1> &delta, double px, double py, double pz, double q0, double q1, double q2, double q3, double xsl, double ysl, double zsl, double xtg, double ytg, double ztg, double vxx, double vxy, double vxz, double vyx, double vyy, double vyz, double scale_x, double scale_y)\n")
    f_cpp.write("{")
    f_cpp.write("delta.coeffRef(0,0) = %s;\n"%(ccode(delta[0,0])))
    f_cpp.write("delta.coeffRef(1,0) = %s;\n"%(ccode(delta[1,0])))
    f_cpp.write("}")
    f_cpp.write("\n")
    f_cpp.write("inline void rectangular_object_with_unknown_width_height_quaternion_wc_jacobian(Eigen::Matrix<double, 2, 9> &j, double px, double py, double pz, double q0, double q1, double q2, double q3, double xsl, double ysl, double zsl, double xtg, double ytg, double ztg, double vxx, double vxy, double vxz, double vyx, double vyy, double vyz, double scale_x, double scale_y)\n")
    f_cpp.write("{")
    for i in range (2):
        for j in range (9):
            f_cpp.write("j.coeffRef(%d,%d) = %s;\n"%(i,j, ccode(delta_jacobian[i,j])))
    f_cpp.write("}")


# %%
import numpy as np
import matplotlib.pyplot as plt

def grad_gauss_wrt_mu(z, mu, inv_cvm):
    return inv_cvm @ (z - mu)

def grad_gauss_wrt_cvm(z, mu, inv_cvm):
    normalized_z = z - mu
    return -0.5 * (inv_cvm - (inv_cvm @ np.outer(normalized_z, normalized_z) @ inv_cvm))

def search_gradient(f, x0, n_samples, max_iterations, step_size):
    """
    Args:
        f: function to minimize
        x0: initial x value
        n_samples: number of samples to sample from gaussian distribution
        max_iterations: number of iterations of cross entropy method to perform
        step_size: step size for parameter updates

    Returns:
        x_hist: history of x values as gradient descent updates x
        f_hist: history of f(x) values as gradient descent updates x
        iterations: number of iterations to reach tolerance
    """

    x_hist = []
    x_hist.append(x0)

    f_hist = []
    f_hist.append(f(x0))

    iterations = 0

    # Set initial mean and covariance matrix
    mu = x0
    cvm = np.eye(mu.size)

    while True:
        iterations += 1
        inv_cvm = np.linalg.inv(cvm)
        samples = np.random.multivariate_normal(mu, cvm, n_samples)
        sample_means = np.array([grad_gauss_wrt_mu(z, mu, inv_cvm)*f(z) for z in samples])
        sample_cvms = np.array([grad_gauss_wrt_cvm(z, mu, inv_cvm)*f(z) for z in samples])
        grad_J_wrt_mu = np.mean(sample_means, axis=0)
        grad_J_wrt_cvm = np.mean(sample_cvms, axis=0)

        mu -= step_size * grad_J_wrt_mu
        cvm -= step_size * grad_J_wrt_cvm

        x_hist.append(mu)
        f_hist.append(f(mu))

        if iterations > max_iterations:
            break

    return x_hist, f_hist, iterations

if __name__ == '__main__':

    def f(x: np.ndarray) -> float:
        """
        """
        Q = np.array([[2, 0, 0],
                      [0, 4, 0],
                      [0, 0, 6]])
        b = np.array([2, 2, 2])
        return 0.5 * (x.T @ Q @ x) - (b.T @ x)

    # random seed
    np.random.seed(8)

    x0 = x_init = np.full(3, 2.)
    n_samples = 100
    iterations = 400
    step_size = 0.001

    x_hist, f_hist, iterations = search_gradient(f, x0, n_samples, iterations, step_size)

    x_hist = np.array(x_hist)
    f_hist = np.array(f_hist)
    min_idx = np.argmin(f_hist)

    print(f'min x value: {x_hist[min_idx]}')
    print(f'min f value: {f_hist[min_idx]}')
    print(f'iterations: {iterations-1}')

    # plot f_hist
    plt.plot(f_hist)
    plt.xlabel('iteration')
    plt.ylabel('$f(x)$')
    plt.title('Search gradient')
    plt.show()


import cmath
import copy
import numpy
import math
import scipy.linalg

class HarmonicOscillator(object):
    def __init__(self, m, w, order, shift):
        self.m = m
        self.w = w
        self.order = order
        # self.norm = (self.w / math.pi) ** 0.25 # not necessary but we just include...
        self.norm = 1.0 # not necessary but we just include...
        self.xavg = shift
        # self.eshift = self.xavg**2 * self.w**2 / 2.0
#-------------------------
    def update_shift(self,shift): # X : lattice configuration
        self.xavg = shift.copy()
#-------------------------
    def value(self,X): # X : lattice configuration
        result = numpy.prod(self.norm * numpy.exp(- (self.m * self.w / 2.0) * (X-self.xavg) * (X-self.xavg)))
        return result
#-------------------------
    def gradient(self,X): # grad / value
        # grad = (-self.w * (X-self.xavg)) * self.value(X)
        grad = - self.m * self.w * (X-self.xavg)
        return grad
#-------------------------
    def laplacian(self,X): # laplacian / value
        # lap = self.w * self.w * (X-self.xavg) * (X-self.xavg) * self.value(X) - self.w * self.value(X)
        lap = self.m * self.m * self.w * self.w * (X-self.xavg) * (X-self.xavg) - self.w * self.m
        return lap
#-------------------------
    def local_energy(self, X):

        nsites = X.shape[0]

        ke   = - 0.5 * numpy.sum(self.laplacian(X)) / self.m
        pot  = 0.5 * self.m * self.w * self.w * numpy.sum(X * X)

        eloc = ke+pot - 0.5 * self.w * nsites # No zero-point energy

        return eloc

class HarmonicOscillatorMomentum(object):
    def __init__(self, m, w, order, shift):
        self.m = m
        self.w = w
        self.order = order
        self.norm = 1.0 # not necessary but we just include...
        self.pavg = shift
#-------------------------
    def value(self,P): # P : lattice momentum
        result = numpy.prod(self.norm * numpy.exp(- (1.0 / (2.0 * self.m * self.w)) * (P-self.pavg) * (P-self.pavg)))
        return result
#-------------------------
    def gradient(self,P): # grad / value
        grad = - (1.0/(self.m * self.w)) * (P-self.pavg)
        return grad
#-------------------------
    def laplacian(self,P): # laplacian / value
        lap = (1.0/(self.m * self.w))**2 * (P-self.pavg) * (P-self.pavg) - (1.0/(self.m * self.w))
        return lap
#-------------------------
    def local_energy(self, P):

        nsites = P.shape[0]

        ke   = (1.0/(2.0*self.m)) * numpy.sum (P * P)
        pot  = - 0.5 * self.m * self.w * self.w * numpy.sum(self.laplacian(P))
        eloc = ke+pot - 0.5 * self.w * nsites # No zero-point energy

        return eloc


# Copyright (c) 2020 Dimitrios-Georgios Akestoridis
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import numpy as np

def em(z_matrix):
    """Python implementation of the EM weighting method.

    For more information, see the following publications:
      * C.-L. Hwang and K. Yoon, Multiple attribute decision making,
        ser. Lecture Notes in Economics and Mathematical Systems.
        Springer-Verlag Berlin Heidelberg, 1981, vol. 186,
        ISBN: 9783540105589.
      * H. Deng, C.-H. Yeh, and R. J. Willis, "Inter-company comparison using
        modified TOPSIS with objective weights," Computers & Operations
        Research, vol. 27, no. 10, pp. 963--973, 2000.
        DOI: 10.1016/S0305-0548(99)00069-6.
    """
    # Make sure that the decision matrix is a float64 NumPy array
    z_matrix = np.array(z_matrix, dtype=np.float64)

    # Make sure that the decision matrix is normalized
    if (np.sum(np.less(z_matrix, 0.0)) > 0
            or np.sum(np.greater(z_matrix, 1.0)) > 0):
        raise ValueError("The decision matrix must be normalized "
                         "in order to apply the EM weighting method")

    # Make sure that the columns of the decision matrix sum to 1
    if not np.all(np.isclose(np.sum(z_matrix, axis=0),
                             np.ones(z_matrix.shape[1]))):
        raise ValueError("The columns of the decision matrix must sum to 1 "
                         "in order to apply the EM weighting method")

    # Compute the normalization constant
    k_constant = 1.0 / np.log(z_matrix.shape[0])

    # Compute the entropy of each criterion
    e_vector = np.zeros(z_matrix.shape[1], dtype=np.float64)
    for j in range(z_matrix.shape[1]):
        tmp_sum = 0.0
        for i in range(z_matrix.shape[0]):
            if z_matrix[i, j] > 0.0:
                tmp_sum += z_matrix[i, j] * np.log(z_matrix[i, j])
        e_vector[j] = -k_constant * tmp_sum

    # The importance of each criterion corresponds to
    # its normalized degree of divergence
    return (1.0 - e_vector) / np.sum(1.0 - e_vector)


import numpy as np
import math

class GradientDescentRegression(object):
    def __init__(self, order, alpha):
        # variable initializing
        self.order = order
        self.theta = None
        self.num_iteration = None
        self.alpha = alpha

    def fit(self, X_train, y_train, X_test, y_test, num_iteration):
        self.num_iteration = num_iteration
        m = X_train.shape[0]
        # calculated feature
        H = self.calculate_H(X_train)
        xTrans = H.T
        # theta initializing
        self.theta = np.random.normal(size=H.shape[1])
        mse_train, mse_test = [], []
        mod = int(math.log(num_iteration))
        for i in range(0, num_iteration):
            hypothesis = np.dot(H, self.theta)
            loss = hypothesis - y_train
            # avg cost per example
            cost = np.sum(loss ** 2) / (2 * m)
            # avg gradient per example
            gradient = np.dot(xTrans, loss) / m
            if i % mod == 0 or i == num_iteration - 1:
                mse_train.append(cost)
                mse_test.append(self.mse(X_test, y_test))
            # update
            self.theta = self.theta - self.alpha * gradient

        return mse_train, mse_test

    def mse(self, X, y):
        y_pred = self.predict(X)
        return np.sum((y - y_pred) ** 2) / (2 * y_pred.shape[0])

    def calculate_H(self, X):
        H = np.c_[np.ones((X.shape[0], 1)), X]
        for i in range(2, self.order + 1):
            H = np.c_[H, np.power(H[:, 1], i)]
        return H

    def predict(self, X_unseen):
        H_unseen = self.calculate_H(X_unseen)
        return H_unseen.dot(self.theta)

    def description(self):
        return "ord=%d, alpha=%g, iter=%d" % (self.order, self.alpha, self.num_iteration)


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Wed Feb 17 12:20:49 2021

@author: atoosa
"""
import numpy as np

# uniform distribution
def uniformDistr(a, b, size):
    data = np.random.uniform(low=a, high=b, size=size)
    return data

def uniformMoments(a, b):
    m1 = (a+b)/2
    m2 = ((b-a)**2)/12
    return m1, m2

# exponential distribution
def expDistr(beta, size):
    data = np.random.exponential(scale=beta, size=size)
    return data

def expMoments(beta):
    m1 = beta
    m2 = beta ** 2
    return m1, m2

# normal distribution
def normalDistr(mue, sigma, size):
    data = np.random.normal(loc=mue, scale=sigma, size=size)
    return data

def normalMoments(mue, sigma):
    m1 = mue
    m2 = sigma ** 2
    return m1, m2

# power-law distribution
# https://stats.stackexchange.com/questions/173242/random-sample-from-power-law-distribution
# https://arxiv.org/pdf/0706.1062.pdf : equation D4
# using inverse of CDF
def powerlawDistr(gamma, a, size):
    r = np.random.uniform(low=0, high=1, size=size)
    data = a * (1-r)**(-1/(gamma-1))
    return  data

def powerlawMoments(gamma, a):
    if gamma>2:
        m1 = a * ((1-gamma)/(2-gamma))
    else:
        m1 = np.inf
    if gamma>3:
        m2 = a**2 * (gamma-1) * ((1/(gamma-3))-((gamma-1)/((gamma-2)**2)))
    else:
        m2 = np.inf

    return m1, m2

np.random.seed(10)

# uniform
# calculate sample moments
print("uniform:")
a = 1
b = 10
for N in [1, 10, 100, 1000, 10000]:
    print("N: " + str(N))
    sample_m1 = []
    sample_m2 = []
    for K in range(0, 30):
        data = uniformDistr(a, b, N)
        sample_m1.append(np.mean(data))
        sample_m2.append(np.var(data))
    m1 = np.mean(sample_m1)
    m2 = np.mean(sample_m2)
    print("sample moments: "+ str(m1) + " ,"+ str(m2))

# calculate moments from formula
true_m1, true_m2  = uniformMoments(a, b)
print("calculated moments: "+ str(true_m1) + " ,"+ str(true_m2))

# expoonential
# calculate sample moments
print("exponential:")
beta = 1
for N in [1, 10, 100, 1000, 10000]:
    print("N: " + str(N))
    sample_m1 = []
    sample_m2 = []
    for K in range(0, 30):
        data = expDistr(beta, N)
        sample_m1.append(np.mean(data))
        sample_m2.append(np.var(data))
    m1 = np.mean(sample_m1)
    m2 = np.mean(sample_m2)
    print("sample moments: "+ str(m1) + " ,"+ str(m2))

# calculate moments from formula
true_m1, true_m2  = expMoments(beta)
print("calculated moments: "+ str(true_m1) + " ,"+ str(true_m2))

# normal
# calculate sample moments
print("normal:")
mue = 1
sigma = 10
for N in [1, 10, 100, 1000, 10000]:
    print("N: " + str(N))
    sample_m1 = []
    sample_m2 = []
    for K in range(0, 30):
        data = normalDistr(mue, sigma, N)
        sample_m1.append(np.mean(data))
        sample_m2.append(np.var(data))
    m1 = np.mean(sample_m1)
    m2 = np.mean(sample_m2)
    print("sample moments: "+ str(m1) + " ,"+ str(m2))

# calculate moments from formula
true_m1, true_m2  = normalMoments(mue, sigma)
print("calculated moments: "+ str(true_m1) + " ,"+ str(true_m2))

# powerlaw
# calculate sample moments
print("power-law:")
a = 1
gamma = 4
for N in [1, 10, 100, 1000, 10000]:
    print("N: " + str(N))
    sample_m1 = []
    sample_m2 = []
    for K in range(0, 30):
        data = powerlawDistr(gamma, a, N)
        sample_m1.append(np.mean(data))
        sample_m2.append(np.var(data))
    m1 = np.mean(sample_m1)
    m2 = np.mean(sample_m2)
    print("sample moments: "+ str(m1) + " ,"+ str(m2))

# calculate moments from formula
true_m1, true_m2  = powerlawMoments(gamma, a)
print("calculated moments: "+ str(true_m1) + " ,"+ str(true_m2))


import numpy as np
import scipy.io as sio

data_Mat = sio.loadmat('dataset2.mat')

def phi(x, n):
    return np.array([x ** i for i in range(0, n + 1)])

def split_dataset(x, y, split):
    split_x = np.vsplit(x, split)
    split_y = np.vsplit(y, split)
    return split_x, split_y

def gradient_descent(x, y, learning_rate, lam, iters, epsilon):
    theta = np.zeros((x.shape[1], 1))

    x_trans = np.transpose(x)
    for i in range(0, iters):
        x_theta = np.matmul(x, theta)
        xTminusy = np.subtract(x_theta, y)
        partial_deri = np.matmul(x_trans, xTminusy) * 2
        lamb_theta = lam * theta * 2
        partial_deri2 = np.add(partial_deri, lamb_theta)
        partial_deri2 = partial_deri2 * learning_rate
        new_theta = theta - partial_deri2
        diff = new_theta - theta
        if np.linalg.norm(diff, 2) <= epsilon:
            theta = new_theta
            break
        theta = new_theta
    return theta

def calculate_loss(theta, lam, x_holdout, y_holdout):
    x_theta = np.matmul(x_holdout, theta)
    term1 = y_holdout - x_theta
    theta_norm = np.linalg.norm(theta, 2) ** 2
    lam_theta = lam * theta_norm
    temp = np.linalg.norm(term1, 2) ** 2
    return lam_theta + temp

lambda_exp = [0.1, 0.001, 0.0001, 1, 2, 0.00001, 0.000001, 3, 0.01, 0.00000000000001]

def ridge_regress(x_t, y_t):
    master_holdout_error = []
    for j in lambda_exp:
        master_loss = []
        for i in range(len(x_t)):
            x_train_copy = x_t.copy()
            del x_train_copy[i]
            y_copy = y_t.copy()
            del y_copy[i]
            x_train_copy = np.concatenate(x_train_copy)
            y_copy = np.concatenate(y_copy)
            master_loss.append(
                calculate_loss(gradient_descent(x_train_copy, y_copy, 0.00000001, j, 60, 0.001), j, x_t[i], y_t[i]))
        avg = np.average(np.array(master_loss))
        master_holdout_error.append(avg)
    final_lambda = np.argmin(np.array(master_holdout_error))
    return final_lambda

x_train, x_test, y_train, y_test = data_Mat['X_trn'], data_Mat['X_tst'], data_Mat['Y_trn'], data_Mat['Y_tst']
# print(data_Mat)
x_train2 = np.array([phi(x_train[i][0], 2) for i in range(0, len(x_train))])
# print(x_test.shape)
# exit(0)
x_test2 = np.array([phi(x_test[i][0], 2) for i in range(0, len(x_test))])
x_train5 = np.array([phi(x_train[i][0], 5) for i in range(0, len(x_train))])
x_test5 = np.array([phi(x_test[i][0], 5) for i in range(0, len(x_test))])

splitted2_2 = split_dataset(x_train2, y_train, 2)
x_train2_2 = splitted2_2[0]
y_train_2 = splitted2_2[1]

splitted2_N = split_dataset(x_train2, y_train, len(x_train2))
x_train2_N = splitted2_N[0]
y_train_N = splitted2_N[1]

splitted5_2 = split_dataset(x_train5, y_train, 2)
x_train5_2 = splitted5_2[0]
y_train_2 = splitted5_2[1]

splitted5_N = split_dataset(x_train5, y_train, len(x_train5))
x_train5_N = splitted5_N[0]
y_train_N = splitted5_N[1]

lam2_2 = ridge_regress(x_train2_2, y_train_2)

lam2_N = ridge_regress(x_train2_N, y_train_N)

lam5_2 = ridge_regress(x_train5_2, y_train_2)

lam5_N = ridge_regress(x_train5_N, y_train_N)

theta2_2 = gradient_descent(np.concatenate(x_train2_2), np.concatenate(y_train_2), 0.00000001, lambda_exp[lam2_2], 60,
                            0.0010)

theta2_N = gradient_descent(np.concatenate(x_train2_N), np.concatenate(y_train_N), 0.00000001, lambda_exp[lam2_N], 60,
                            0.0010)

theta5_2 = gradient_descent(np.concatenate(x_train5_2), np.concatenate(y_train_2), 0.00000000001, lambda_exp[lam5_2], 70,
                            0.0001)

theta5_N = gradient_descent(np.concatenate(x_train5_N), np.concatenate(y_train_N), 0.00000000001, lambda_exp[lam5_N], 70,
                            0.0001)

print(calculate_loss(theta2_2, lambda_exp[lam2_2], x_test2, y_test))
print(calculate_loss(theta2_2, lambda_exp[lam2_2], np.concatenate(x_train2_2), np.concatenate(y_train_2)))
print(lambda_exp[lam2_2])
print(theta2_2)

print("-------------------------------------")

print(calculate_loss(theta2_N, lambda_exp[lam2_N], x_test2, y_test))
print(calculate_loss(theta2_N, lambda_exp[lam2_N], np.concatenate(x_train2_N), np.concatenate(y_train_N)))
print(lambda_exp[lam2_N])
print(theta2_N)

print("-------------------------------------")

print(calculate_loss(theta5_2, lambda_exp[lam5_2], x_test5, y_test))
print(calculate_loss(theta5_2, lambda_exp[lam5_2], np.concatenate(x_train5_2), np.concatenate(y_train_2)))
print(lambda_exp[lam5_2])
print(theta5_2)

print("-------------------------------------")

print(calculate_loss(theta5_N, lambda_exp[lam5_N], x_test5, y_test))
print(calculate_loss(theta5_N, lambda_exp[lam5_N], np.concatenate(x_train5_N), np.concatenate(y_train_N)))
print(lambda_exp[lam5_N])
print(theta5_N)


import sympy
from typing import Tuple

def sym_no_rot2() -> sympy.Matrix:
    return sympy.Matrix([
        [1, 0],
        [0, 1]
    ])

def sym_hom_no_rot2() -> sympy.Matrix:
    return sympy.Matrix([
        [1, 0, 0],
        [0, 1, 0],
        [0, 0, 1]
    ])

def sym_rot2(angle: sympy.NumberSymbol) -> sympy.Matrix:
    c = sympy.cos(angle)
    s = sympy.sin(angle)
    return sympy.Matrix([
        [c, -s],
        [s, c]
    ])

def sym_rot2_inv(rotmat: sympy.Matrix) -> sympy.Matrix:
    return sympy.transpose(rotmat)

def sym_hom_rot2(angle: sympy.NumberSymbol) -> sympy.Matrix:
    c = sympy.cos(angle)
    s = sympy.sin(angle)
    return sympy.Matrix([
        [c, -s, 0],
        [s, c, 0],
        [0, 0, 1]
    ])

def sym_hom_rot2_inv(homrotmat: sympy.Matrix) -> sympy.Matrix:
    rot2_inv_mat = sympy.transpose(homrotmat[0:2, 0:2])
    return sympy.Matrix([
        [rot2_inv_mat[0, 0], rot2_inv_mat[0, 1], 0],
        [rot2_inv_mat[1, 0], rot2_inv_mat[1, 1], 0],
        [0, 0, 1]
    ])

def sym_no_transl2() -> sympy.Matrix:
    return sympy.Matrix([[0, 0]])

def sym_transl2(tx: sympy.NumberSymbol, ty: sympy.NumberSymbol) -> sympy.Matrix:
    return sympy.Matrix([[tx, ty]])

def sym_no_coltransl2() -> sympy.Matrix:
    return sympy.Matrix([0, 0])

def sym_coltransl2(tx: sympy.NumberSymbol, ty: sympy.NumberSymbol) -> sympy.Matrix:
    return sympy.Matrix([tx, ty])

def sym_transl2_inv(translvec: sympy.Matrix) -> sympy.Matrix:
    return -translvec

def sym_hom_no_transl2() -> sympy.Matrix:
    return sympy.Matrix([
        [1, 0, 0],
        [0, 1, 0],
        [0, 0, 1]
    ])

def sym_hom_transl2(tx: sympy.NumberSymbol, ty: sympy.NumberSymbol) -> sympy.Matrix:
    return sympy.Matrix([
        [1, 0, tx],
        [0, 1, ty],
        [0, 0, 1]
    ])

def sym_hom_transl2_inv(homtranslmat: sympy.Matrix) -> sympy.Matrix:
    transl2_inv_vec = -homtranslmat[0:2, 2:3]
    return sympy.Matrix([
        [1, 0, transl2_inv_vec[0, 0]],
        [0, 1, transl2_inv_vec[1, 0]],
        [0, 0, 1]
    ])

def sym_hom2(rotmat: sympy.Matrix, translvec: sympy.Matrix) -> sympy.Matrix:
    translvec = translvec.reshape(2, 1)
    return sympy.Matrix([
        [rotmat[0, 0], rotmat[0, 1], translvec[0, 0]],
        [rotmat[1, 0], rotmat[1, 1], translvec[1, 0]],
        [0, 0, 1]
    ])

def sym_hom2_inv(hommat: sympy.Matrix) -> sympy.Matrix:
    rot2_inv_mat = sympy.transpose(hommat[0:2, 0:2])
    transl2_inv_vec = sympy.simplify(-rot2_inv_mat * hommat[0:2, 2:3])
    return sympy.Matrix([
        [rot2_inv_mat[0, 0], rot2_inv_mat[0, 1], transl2_inv_vec[0, 0]],
        [rot2_inv_mat[1, 0], rot2_inv_mat[1, 1], transl2_inv_vec[1, 0]],
        [0, 0, 1]
    ])

def sym_dehom2(hommat: sympy.Matrix) -> Tuple[sympy.Matrix, sympy.Matrix]:
    rotmat = sympy.Matrix([
        [hommat[0, 0], hommat[0, 1]],
        [hommat[1, 0], hommat[1, 1]],
    ])
    translvec = sympy.Matrix([[hommat[0, 2], hommat[1, 2]]])
    return rotmat, translvec


import numpy as np

def get_Pnm_n(nmax, m, theta):
    """
    Calculation of associated Legendre functions P(n,m) (Schmidt normalized)
    and its derivative dP(n,m) vrt. theta.

    Input: theta[:] co-latitude (in rad)
           nmax  maximum spherical harmonic degree
    Output: Pnm    ndarray PD with Legendre functions

    P(n,m) ==> Pnm(n,m) and dP(n,m) ==> Pnm(m,n+1)
    """

    costh = np.cos(theta)
    sinth = np.sqrt(1-costh**2)

    Pnm = np.zeros((nmax+1, nmax+2, len(theta)))
    Pnm[0][0] = 1
    Pnm[1][1] = sinth

    rootn = np.sqrt(np.arange(0, 2*nmax**2+1))

#     Recursion relations after Langel "The Main Field" (1987),
# eq. (27) and Table 2 (p. 256)
    for m in np.arange(0, nmax):
#         Pnm_tmp = np.sqrt(m+m+1)*Pnm[m][m]
         Pnm_tmp = rootn[m+m+1]*Pnm[m][m]
         Pnm[m+1][m] = costh*Pnm_tmp
         if m > 0:
#             Pnm[m+1][m+1] = sinth*Pnm_tmp/np.sqrt(m+m+2.)
             Pnm[m+1][m+1] = sinth*Pnm_tmp/rootn[m+m+2]
         for n in np.arange(m+2, nmax+1):
             d = n*n - m*m
             e = n + n - 1
#             Pnm[n][m] = (e*costh*Pnm[n-1][m]-np.sqrt(d-e)*Pnm[n-2][m])/np.sqrt(d)
             Pnm[n][m] = (e*costh*Pnm[n-1][m]-rootn[d-e]*Pnm[n-2][m])/rootn[d]

#    dP(n,m) = Pnm(m,n+1) is the derivative of P(n,m) vrt. theta
    Pnm[0][2] = -Pnm[1][1]
    Pnm[1][2] =  Pnm[1][0]
    for n in np.arange(2, nmax+1):
          l = n + 1
          Pnm[0][l] = -np.sqrt(.5*(n*n+n))*Pnm[n][1]
          Pnm[1][l] = .5*(np.sqrt(2.*(n*n+n))*Pnm[n][0]-np.sqrt((n*n+n-2.))*Pnm[n][2])

          for m in np.arange(2, n):
              Pnm[m][l] = .5*(np.sqrt((n+m)*(n-m+1.))*Pnm[n][m-1] -np.sqrt((n+m+1.)*(n-m))*Pnm[n][m+1])

          Pnm[n][l] = .5*np.sqrt(2.*n)*Pnm[n][n-1]

    return Pnm

def get_Pnm(nmax, theta):
    """
    Calculation of associated Legendre functions P(n,m) (Schmidt normalized)
    and its derivative dP(n,m) vrt. theta.

    Input: theta[:] co-latitude (in rad)
           nmax  maximum spherical harmonic degree
    Output: Pnm    ndarray PD with Legendre functions

    P(n,m) ==> Pnm(n,m) and dP(n,m) ==> Pnm(m,n+1)
    """

    costh = np.cos(theta)
    sinth = np.ravel(np.sqrt(1-np.power(costh,2)))

    Pnm = np.zeros((nmax+1, nmax+2, len(theta)))
    Pnm[0][0] = 1
    Pnm[1][1] = sinth

    rootn = np.sqrt(np.arange(0, 2*nmax**2+1))

#     Recursion relations after Langel "The Main Field" (1987),
# eq. (27) and Table 2 (p. 256)
    for m in np.arange(0, nmax):
#         Pnm_tmp = np.sqrt(m+m+1)*Pnm[m][m]
         Pnm_tmp = rootn[m+m+1]*Pnm[m][m]
         Pnm[m+1][m] = costh*Pnm_tmp
         if m > 0:
#             Pnm[m+1][m+1] = sinth*Pnm_tmp/np.sqrt(m+m+2.)
             Pnm[m+1][m+1] = sinth*Pnm_tmp/rootn[m+m+2]
         for n in np.arange(m+2, nmax+1):
             d = n*n - m*m
             e = n + n - 1
#             Pnm[n][m] = (e*costh*Pnm[n-1][m]-np.sqrt(d-e)*Pnm[n-2][m])/np.sqrt(d)
             Pnm[n][m] = (e*costh*Pnm[n-1][m]-rootn[d-e]*Pnm[n-2][m])/rootn[d]

#    dP(n,m) = Pnm(m,n+1) is the derivative of P(n,m) vrt. theta
    Pnm[0][2] = -Pnm[1][1]
    Pnm[1][2] =  Pnm[1][0]
    for n in np.arange(2, nmax+1):
          l = n + 1
          Pnm[0][l] = -np.sqrt(.5*(n*n+n))*Pnm[n][1]
          Pnm[1][l] = .5*(np.sqrt(2.*(n*n+n))*Pnm[n][0]-np.sqrt((n*n+n-2.))*Pnm[n][2])

          for m in np.arange(2, n):
              Pnm[m][l] = .5*(np.sqrt((n+m)*(n-m+1.))*Pnm[n][m-1] -np.sqrt((n+m+1.)*(n-m))*Pnm[n][m+1])

          Pnm[n][l] = .5*np.sqrt(2.*n)*Pnm[n][n-1]

    return Pnm

def design_SHA(r, theta, phi, nmax):
    """
    Created on Fri Feb  2 09:18:42 2018

    @author: nilos
    A_r, A_theta, A_phi = design_SHA(r, theta, phi, N)

     Calculates design matrices A_i that connects the vector
     of (Schmidt-normalized) spherical harmonic expansion coefficients,
     x = (g_1^0; g_1^1; h_1^1; g_2^0; g_2^1; h_2^1; ... g_N^N; h_N^N)
     and the magnetic component B_i, where "i" is "r", "theta" or "phi":
         B_i = A_i*x
         Input: r[:]      radius vector (in units of the reference radius a)
         theta[:]  colatitude    (in radians)
         phi[:]    longitude     (in radians)
         N         maximum degree/order

     A_r, A_theta, A_phi = design_SHA(r, theta, phi, N, i_e_flag)
     with i_e_flag = 'int' for internal sources (g_n^m and h_n^m)
                     'ext' for external sources (q_n^m and s_n^m)
     """

    cml = np.zeros((nmax+1, len(theta))) # cos(m*phi)
    sml = np.zeros((nmax+1, len(theta))) # sin(m*phi)
    a_r = np.zeros((nmax+1, len(theta)))
    cml[0]= 1
    for m in np.arange(1, nmax+1):
        cml[m]=np.cos(m*phi)
        sml[m]=np.sin(m*phi)
    for n in np.arange(1, nmax+1):
        a_r[n]=r**(-(n+2))

    Pnm = get_Pnm(nmax, theta)
    sinth = Pnm[1][1]

# construct A_r, A_theta, A_phi
    A_r =     np.zeros((nmax*(nmax+2), len(theta)))
    A_theta = np.zeros((nmax*(nmax+2), len(theta)))
    A_phi =   np.zeros((nmax*(nmax+2), len(theta)))

    l = 0
    for n in np.arange(1, nmax+1):
        for m in np.arange(0, n+1):
            A_r[l] =     (n+1.)*Pnm[n][m]   *cml[m] * a_r[n]
            A_theta[l] =       -Pnm[m][n+1] *cml[m] * a_r[n]
            A_phi[l] =        m*Pnm[n][m]   *sml[m] * a_r[n] / sinth
            l=l+1
            if m > 0:
                A_r[l] =     (n+1.)*Pnm[n][m]   * sml[m] * a_r[n]
                A_theta[l] =       -Pnm[m][n+1] * sml[m] * a_r[n]
                A_phi[l] =       -m*Pnm[n][m]   * cml[m] * a_r[n] / sinth
                l=l+1
    return A_r.transpose(), A_theta.transpose(), A_phi.transpose()

def synth_grid(gh, r, theta, phi):
    """
    Created on Fri Feb  2 09:18:42 2018

    @author: nilos
    B_r, B_theta, B_phi = synth_grid(gh, r, theta, phi)

    """
    n_coeff = len(gh)
    nmax = int(np.sqrt(n_coeff+1)-1)
    N_theta   = len(theta)
    N_phi     = len(phi)
    n = np.arange(0, nmax+1)

    r_n       = r**(-(n+2))

    cos_sin_m = np.ones((n_coeff, N_phi))
    sin_cos_m = np.zeros((n_coeff, N_phi))
    T_r     = np.zeros((n_coeff, N_theta))
    T_theta = np.zeros((n_coeff, N_theta))
    T_phi   = np.zeros((n_coeff, N_theta))

    Pnm = get_Pnm(nmax, theta)
    sinth = Pnm[1][1]
    #sinth[-1] = 0.10

    k=0
    for n in np.arange(1, nmax+1):
        T_r[k]     = (n+1.)*r_n[n]*Pnm[n][0]
        T_theta[k] = -r_n[n]*Pnm[0][n+1]
        k = k+1
        for m in np.arange(1, n+1):
            T_r[k]     = (n+1)*r_n[n]*Pnm[n][m]
            T_theta[k] = -r_n[n]*Pnm[m][n+1]
            T_phi[k]   = m*r_n[n]*Pnm[n][m] / sinth
            cos_sin_m[k] = np.cos(m*phi)
            sin_cos_m[k+1] = cos_sin_m[k]
            T_r[k+1]     = T_r[k]
            T_theta[k+1] = T_theta[k]
            T_phi[k+1]   = -T_phi[k]
            cos_sin_m[k+1] = np.sin(m*phi)
            sin_cos_m[k] = cos_sin_m[k+1]
            k = k+2

    tmp = cos_sin_m*gh[:, np.newaxis]
    B_r = np.matmul(T_r.transpose(), tmp)
    B_theta = np.matmul(T_theta.transpose(), tmp)
    B_phi = np.matmul(T_phi.transpose(), sin_cos_m*gh[:, np.newaxis])

    return B_r, B_theta, B_phi

def read_shc(shc_fn,cols='all'):
  """
  Read values of Gauss coefficients (g,h) from column(s) in file.

  File should be ascii file obeying the SHC format.

  Parameters
  ----------
  shc_fn : str
    Path of input SHC ascii file
  cols : list_like
    List of columns to read from file. This should correspond to the
    columns the different times values coefficients will be
    read from. In a standard SHC file the first two columns (0 and 1)
    correspond to the degree (l) and order (m) of the harmonic and
    should not be included in `cols`. As such the default value
    ``cols='all'`` corresponds to ``cols=range(2,2+N_times)``, where
    ``N_times`` is the number of time snapshots in the file.

  Returns
  -------
  Tuple
    Tuple with following values at given indices:

      0. numpy.ndarray of gaussian coefficients with such that
          ``myarray[0]`` gives all coefficients at the first time point,
          given that there are multiple time snapshots. Otherwise
          ``array[0]`` will only contain the first coefficient.
      1. spline order `k` as an integer used to reconstruct model from
          time snapshots.
      2. number of columns as an integer.
      3. time of the temporal snapshots (in fractional years in the
          standard SHC format) as 1D `numpy.ndarray`.

  Notes
  -----
  Missing data values marked as NaN are currently not handled.

  """

  with open(shc_fn) as f:
    headerlen=0
    header_fin=False
    for line in f:
      if header_fin: # finished reading header
        times=np.empty(N_times)
        c=0
        for t in line.split():
          times[c]=float(t)
          c+=1
        break
      else:
        if line.startswith('#'):
          headerlen+=1
        else:
          header_fin=True
          N_min,N_max,N_times,spline_order,N_step = \
            (int(v) for v in line.split()[:5])
  cols=range(2,2+N_times)
#  gh=np.loadtxt(shc_fn,skiprows=headerlen+2)
  gh=np.loadtxt(shc_fn,skiprows=headerlen+2,usecols=cols,unpack=True)
#  if len(gh.shape)==1:
#    gh=np.expand_dims(gh,0)

  # currently not passing on N_min,N_max,N_step
  return gh,spline_order,times

def mauersberger_lowes_spec(gh,r=1):
    """ The Mauersberger-Lowes spatial powerspectrum"""
    ratio=1/r
    N = int(np.sqrt(gh.size+1)-1) # maximum spherical harmonic degree
    R_l=np.empty(N)
    gh_idx=0
    for l in range(1,N+1):
        gh_idx_n=gh_idx+2*l+1
        g_sq=np.sum(gh[gh_idx:gh_idx_n]**2)
        R_l[l-1] = (l+1)*ratio**(2*l+4)*g_sq
        gh_idx=gh_idx_n
    return R_l

def degree_correlation(gh1,gh2,lmax=-1,lmin=1):
    """Correlation per spherical harmonic degree between two models 1 and 2"""
    if lmax<1:
        lmax1,lmin1=get_l_maxmin(len(gh1))
        lmax2,lmin2=get_l_maxmin(len(gh2))
        lmax = min(lmax1,lmax2)
        lmin = min(lmin1,lmin2)
    c12=np.empty(lmax+1-lmin)
    i=0
    for l in range(lmin,lmax+1):
        #m=0
        g12 = gh1[i]*gh2[i]
        g11 = gh1[i]**2
        g22 = gh2[i]**2
        i+=1
        for m in range(1,l+1):
            g12 += gh1[i]*gh2[i]
            g11 += gh1[i]**2
            g22 += gh2[i]**2
            i += 2
        c12[l-lmin] = g12/np.sqrt(g11*g22)
    return c12


import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from ex3 import sum_squared_error, total_sum_squares, SSR, r_squared, mean_squared_error, standard_deviation

# X: the number of disk I/Os
# Y: the processor times
# n: 7 programms

X = np.array([14,16,27,42,39,50,83])
Y = np.array([2,5,7,9,10,13,20])
n = X.shape[0]

m_X = np.mean(X)
ssX = sum(X**2)

# Fit the training data with a 1st order polynomial.
z = np.polyfit(X,Y,1)
# Add a column of ones at the end for the intercept.
Xext = np.vstack((X,np.ones(X.shape[0],)))

# Estimate using the polynomial coefs computed previously.
prediction = np.sum(Xext.T*z,axis=1)

# Compute the error and squared error for the data.
error = Y - prediction
squared_error = error**2

# Put all stuff together in a dataframe.
results = pd.DataFrame(np.column_stack((X,Y,prediction,error,squared_error)))
results.columns = ['Disk I/O', 'CPU time', 'Estimate', 'Error', 'Squared error']
print results

# Plot the data and the learned model.
p = np.poly1d(z)
plt.plot(X,Y,'ro',X,p(X))
plt.xlabel("Disk I/Os")
plt.ylabel("CPU time")
#plt.show()

sse = sum_squared_error(squared_error)
sst = total_sum_squares(prediction, Y)
ssr = SSR(sst,sse)
r_s = r_squared(ssr,sst)
print "\nSum of sqared errors:",sse
print "Total sum of squares:",sst
print "SSR:",ssr
print "R squared:",r_s

mse = mean_squared_error(sse,n)
se = standard_deviation(mse)

print "\nMean squared error:",mse
print "Standard deviation:",se

# Compute b0 and b1 standard devietions.
sb0 = se*np.sqrt((1/n) + (m_X**2)/(ssX - n*(m_X**2)))
sb1 = se/np.sqrt(ssX - n*(m_X**2))
print "\nSb0:",sb0
print "Sb1:",sb1

# Compute the 90% confidence intervals for b0 and b1.
# the 0.95-quantile of a t-variate with 5 degrees of freedom is 2.015
t = 2.015
b0, b1 = z[1],z[0]
(ci_0_low, ci_0_high) = b0 - t * (sb0), b0 + t * (sb0)
(ci_1_low, ci_1_high) = b1 - t * (sb1), b1 + t * (sb1)
print "90% Confidence Interval for b0 is:",(ci_0_low, ci_0_high)
print "90% Confidence Interval for b1 is:",(ci_1_low, ci_1_high)

# Predict the CPU time for 100 disk I/Os
num_io = 100
x_new = [num_io, 1]
prediction = sum(x_new*z)
print "Predicted CPU time for",num_io,"I/Os:",prediction

# Standard deviation for future predictions.
sy_p = se * np.sqrt((1/n) + ((num_io - m_X)**2)/(ssX - n*(m_X**2)))
print "The standard deviation of the predicted mean of a large number of observations is:",sy_p
(ci_mp_low, ci_mp_high) = prediction - t * (sy_p), prediction + t * (sy_p)
print "90% Confidence Interval for the predicted mean is:",(ci_mp_low, ci_mp_high)

# For a single future prediction.
s_sp = se * np.sqrt(1 + (1/n) + ((num_io - m_X)**2)/(ssX - n*(m_X**2)))
print "The standard deviation of the predicted mean for a single prediction is:",s_sp
(ci_p_low, ci_p_high) = prediction - t * (s_sp), prediction + t * (s_sp)
print "90% Confidence Interval for a single prediction is:",(ci_p_low, ci_p_high)

# Some visual tests for the model.
plt.subplot(1,2,1)
plt.plot(X,Y,'ro',X,p(X))
plt.xlabel("Disk I/Os")
plt.ylabel("CPU time")
plt.subplot(1,2,2)
plt.plot(p(X),error,'ro')

plt.show()


import numpy as np
import pandas as pd
import avam.common.file_helper as fh

#region FUNCTIONS
# Feature Normalization
def normalize(x):
    max_val = max(x)
    min_val = min(x)
    avg = np.mean(x)
    normalized = [(i-avg)/(max_val - min_val) for i in x]
    return normalized
# Hypothesis Function
def hypothesis_l(X,theta):
    n = len(theta)
    m = len(X)
    h = np.zeros((m,1))
    for i in range(0,m):
        sm=0
        for j in range(0,n):
            sm += X[i,j] * theta[j,0]
        h[i,0]=sm
    return h

#Cost Function
def cost_l(h,y):
    m = len(h)
    cost = 0
    for i in range(m):
        cost += (h[i] - y[i]) ** 2
    return cost / (2*m)

#Gradient Descent Function
def gradient_descent_l(X,h,y, alpha,theta):
    n = X.shape[1]
    m = X.shape[0]
    new_thetas = np.zeros((n,1))
    for j in range(n):
        th = 0
        for i in range(m):
            th += (h[i,0] - y[i,0]) * X[i,j]
        new_thetas[j,0] = theta[j,0] - (alpha * (1/m) * th)
    return new_thetas
#endregion

def run():
    columns = ['CRIM','ZN','INDUS','CHAS','NOX','RM','AGE','DIS','RAD','TAX','PTRATIO','B','LSTAT','MEDV']
    df = pd.read_csv(fh.getHousingDataFile(), header=None, names=columns, sep='\s+')
    df = df.apply(normalize,axis=0)
    # Create X0 column
    Xd = df.drop(columns=['MEDV'])
    Xd.insert(0, 'X0', 1)
    # numpy array format
    X = Xd.values
    m = len(Xd.index)
    y = df.MEDV.to_numpy().reshape(m,1)
    n = Xd.shape[1]
    alpha = 0.0005
    iterations = 100000
    print('Sample Size : {}, Features : {}'.format(m,n))
    #Initialize Theta
    theta = np.ones((n,1))
    n_thetas = theta
    cost = np.zeros((iterations,1))
    for i in range(iterations):
        hypthesis = hypothesis_l(X,n_thetas)
        cost[i,0] = cost_l(hypthesis,y)
        n_thetas = gradient_descent_l(X,hypthesis,y, alpha,n_thetas)
        # if(i>0 and round(i/100,4)==1.0000):
        print("Iterations : {}".format(i))
    print(n_thetas)


import matplotlib.pyplot as plt
from scipy.optimize import curve_fit
import numpy as np

def func(x, a, b, c):
    return a*(x**b)+c

x_points = np.array([1, 1.2, 2, 2.7, 4.1, 6, 8])
y_points = np.array([0, 10, 50, 100, 200, 500, 1000])

plt.scatter(x_points, y_points, c='blue', label='real data')

#give bigger weight to first and last point in real data ("force" function to fit them more precisely)
sigma = np.ones(len(x_points))
sigma[[0, -1]] = 0.01

popt, _ = curve_fit(func, x_points, y_points, sigma=sigma)
print(popt)

x_show = np.arange(x_points[0], x_points[-1], 0.01)

plt.plot(x_show, func(x_show, *popt), c='red', label='params: a=%5.3f, b=%5.3f, c=%5.3f' % tuple(popt))

plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.show()

diffs = y_points - func(x_points, *popt)
ss_tot = np.sum((y_points-np.mean(y_points))**2)
ss_res = np.sum(diffs**2)
r_to_2 = 1 - (ss_res/ss_tot)
print('R^2 score = %.10f' % r_to_2)


import numpy as np
import matplotlib.pyplot as plt

f=20
x=[0.0, 2.7489635262192884, 3.588031681391703, 4.4050473465254107, 5.1140515212826916, 5.9773635998267531, 8.5692968627530455]

a= [-2.94436678, 2.41022146,  2.00620887,  1.49225344, 2.12876905, 2.51520289,-2.53351283]

#x=np.linspace(0, 3, 4)
#a=np.exp(x)

FPO=0.0
FPN=0.0

n=len(a)-1;
l=np.zeros(n+1, dtype=np.float_)
u=np.zeros(n, dtype=np.float_)
z=np.zeros(n+1, dtype=np.float_)
h=np.zeros(n, dtype=np.float_)
alfa=np.zeros(n+1, dtype=np.float_)
c=np.zeros(n+1, dtype=np.float_)
b=np.zeros(n, dtype=np.float_)
d=np.zeros(n, dtype=np.float_)

for i in range(n):
	h[i]=x[i+1]-x[i]
print "h", h
sA = np.zeros(shape=(n+1,n+1), dtype=np.float_)
for i in range(n-1) :
	for j in range(n-1) :
		if i is j:
			print i
			sA[i+1][i+1]=2*(h[i]+h[i+1])
			sA[i+1][i]=h[i]
			sA[i][i+1]=h[i]
sA[0][0]=2*h[0]
sA[-1][-1]=2*h[-1]
sA[-1][-2]=h[-1]
sA[-2][-1]=h[-1]

print sA
sb = np.zeros(shape=(n+1,1), dtype=np.float_)

for i in range(1,n) :
	sb[i]=(3.0*(a[i+1]-a[i])/h[i]) - (3.0*(a[i]-a[i-1])/h[i-1])

sb[0]=((3.0*(a[1]-a[0]))/h[0])-3.0*FPO
sb[-1]=3.0*FPN-(3.0*(a[n]-a[n-1])/h[n-1])

_b=np.arange(n, dtype=np.float_)
_c=np.linalg.solve(sA, sb)
_d=np.arange(n, dtype=np.float_)

for j in reversed(range(n)):
	_b[j]=((a[j+1]-a[j])/h[j] )-(h[j]*(_c[j+1]+2*_c[j])/3.0)
	_d[j]=(_c[j+1]-_c[j])/(3.0*h[j])

# Graphic
t_out=np.linspace(x[0], x[-1], int((x[-1]-x[0])*f)+1)
tl=len(t_out)
s =np.zeros(tl, dtype=np.float_)

for i in range(n):
	for j in range(tl):
		if(t_out[j]>=x[i] and t_out[j]<x[i+1]):
			s[j]=( a[i]+_b[i]*(t_out[j]-x[i])+_c[i]*(t_out[j]-x[i])**2+_d[i]*(t_out[j]-x[i])**3)
s[-1]=a[-1]+_b[-1]*(t_out[-1]-x[-1]+_c[-1]*(t_out[-1]-x[-1])**2+_d[-1]*(t_out[-1]-x[-1])**3)

plt.plot(t_out, s, 'b-')
plt.plot(x,a,'ro')
plt.show()


import numpy as np

def minkowski_distance(group1, group2, p=2):
    """
    return minkowski_distance between two elements group
    """
    group1 = group1.reshape(-1)
    group2 = group2.reshape(-1)
    dist_sum = 0.
    for (x1, x2) in (zip(group1, group2)):
        abs_diff = abs(x1 - x2)
        dist_sum += abs_diff ** p
    return dist_sum ** (1./p)


import math
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

def asset_price_sim(num_paths, num_time, rho, S_0, V_0, T, kappa, theta, sigma, r):
    dt = T / num_time
    S = np.zeros((num_time + 1, num_paths))  # simulation matrix of asset price
    S[0, :] = S_0  # set first line as the initial asset price
    V = np.zeros((num_time + 1, num_paths))  # simulation matrix of variance(square of volatility)
    V[0, :] = V_0
    # np.random.seed(100)
    for i in range(num_paths):  # num_path is the column of the matrices
        for j in range(1, num_time + 1):  # num_time is the row of the matrices
            # two stochastic drivers for variance V and asset price S
            Zv = np.random.randn(1)
            Zs = rho * Zv + math.sqrt(1 - rho ** 2) * np.random.randn(1)
            # we use the Euler scheme to simulate the variance
            V[j, i] = V[j - 1, i] + kappa * (theta - V[j - 1, i]) * dt \
                      + sigma * math.sqrt(V[j - 1, i]) * math.sqrt(dt) * Zv
            if V[j, i] < 0:
                V[j, i] = abs(V[j, i])
            # we then simulate the asset price
            S[j, i] = S[j - 1, i] * np.exp((r - V[j - 1, i] / 2) * dt
                                           + math.sqrt(V[j - 1, i]) * math.sqrt(dt) * Zs)
    return V, S

def call_option_price(S, K_list, num_time, T, r):
    C = np.zeros((len(K_list), num_time))
    dt = T / num_time
    for i in range(1, num_time + 1):  # different expiry date
        S_T = S[i, :]
        T_temp = T - i * dt
        for j in range(len(K_list)):  # different strike price
            K = K_list[j]
            C[j, i - 1] = np.exp(-r * T_temp) * np.mean(np.maximum(S_T - K, 0))
    return C

def sim_figure(V, S, C, K_list, T, num_time):
    dt = T / num_time
    T_list = np.arange(dt, T + dt, dt).tolist()
    plt.figure(1)
    plt.plot(V)
    plt.ylabel('Stochastic variance V')
    plt.xlabel('time step')
    plt.title('Simulation of variance V')
    plt.savefig('./heston/figures/variance.png')
    plt.figure(2)
    plt.plot(S)
    plt.ylabel('Simulated asset paths')
    plt.xlabel('time step')
    plt.title('Simulation of asset paths')
    plt.savefig('./heston/figures/asset_price.png')
    fig = plt.figure(3)
    ax = Axes3D(fig)
    xx, yy = np.meshgrid(T_list, K_list)
    ax.plot_surface(xx, yy, C, cmap='rainbow')
    ax.set_xlabel('Expiry date T')
    ax.set_ylabel('Strike price K')
    ax.set_zlabel('Option price C')
    ax.set_title('Simulation of option price')
    plt.savefig('./heston/figures/option_price.png')
    plt.show()

def record_sim(V, S, C, K_list, T, num_time):
    dt = T / num_time
    T_list = np.arange(dt, T + dt, dt).tolist()
    pd_V = pd.DataFrame(V)
    pd_S = pd.DataFrame(S)
    pd_C = pd.DataFrame(C, index=K_list, columns=T_list)
    pd_V.to_csv('./heston/data/sim_volatility.csv')
    pd_S.to_csv('./heston/data/sim_asset_price.csv')
    pd_C.to_csv('./heston/data/sim_option_price.csv')

num_paths = 10000
num_time = 50
rho = -.02  # correlation of the bivariate normal distribution
S_0 = 100  # initial asset price
V_0 = 0.2 ** 2  # square of volatility
kappa = 2
theta = 0.2 ** 2  # long run variance
sigma = 0.1  # volatility of volatility
r = 0.05  # risk-free interest rate
T = 1  # longest expiry date
K_list = np.arange(25, 120, 1).tolist()  # list of strike price
V, S = asset_price_sim(num_paths, num_time, rho, S_0, V_0, T, kappa, theta, sigma, r)
C = call_option_price(S, K_list, num_time, T, r)
record_sim(V, S, C, K_list, T, num_time)
sim_figure(V, S, C, K_list, T, num_time)

DQN based Hedging model/heston_option_price_sim.py


# -*- coding: utf-8 -*-
"""HW6_Q1.ipynb

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/1rZxaSXzf_A38opZjtiGZefmBgPYCOGfv
"""

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

"""Assume that you have 4 samples each with dimension 3, described in the data matrix X,

a. Find the sample mean.

b. Zero-center the samples, and find the eigenvalues and eigenvectors of the data covariance matrix Q.
"""

x = [[3,2,1],[2,4,5],[1,2,3],[0,2,5]]
mean = []
sum = 0
for col in range(len(x[0])):
  for row in range(len(x)):
    sum += x[row][col]
  mean.append(sum / len(x))
  sum = 0
# print(mean)
zero_center_mat = x[:]
# print(zero_center_mat)
for col in range(len(x[0])):
  for row in range(len(x)):
    zero_center_mat[row][col] = x[row][col] - mean[col]

Q = np.cov(np.array(zero_center_mat).T)
e, v = np.linalg.eig(Q)
print(f'the covariance matrix is: \n{Q}\n')
print(f'the eigenvalues is \n{e}\n')
print(f'the eigenvectors are \n{v}\n')

"""c. Find the PCA coefficients corresponding to each of the samples in X."""

zero_center_mat = np.array(zero_center_mat)
eig_value = e[0]
eig_vec = v[0]

coef_mat = zero_center_mat.dot(eig_vec.T)
print(f'the PCA coefficients is: \n{coef_mat}')
# print(zero_center_mat)

"""d. Reconstruct the original samples from the top two principal components, and report the reconstruction error for each of the samples."""

vec_mat = v[0:2]
new_sample_mat = zero_center_mat.dot(vec_mat.T)

dis_zero = []
dis_new_zero = []
temp_dis = 0
temp_new_dis = 0
for row in range(len(zero_center_mat)):
  for col in range(len(zero_center_mat[0])):
    temp_dis += zero_center_mat[row][col] ** 2
  dis_zero.append(temp_dis)
  temp_dis = 0

for row in range(len(new_sample_mat)):
  for col in range(len(new_sample_mat[0])):
    temp_new_dis += new_sample_mat[row][col] ** 2
  dis_new_zero.append(temp_new_dis)
  temp_new_dis = 0

err_per_sample = []
for _ in range(len(dis_zero)):
  err_per_sample.append(np.sqrt(abs(dis_zero[_] - dis_new_zero[_])))
print(f'the error are {err_per_sample}')


import numpy as np

class PCA:
    """Principal Component Analysis"""
    def __init__(self, X):
        """
        X: 2D sample matrix (paramter num * sample num). Each column is a sample.
        X doesn't have to be normalized.
        """
        assert len(X.shape) == 2, "PCA input X should be a 2D matrix"
        self.X = X
        self.dimension, sample_size = X.shape
        parameters_mean = [np.mean(x) for x in X]
        normalized_X = (X.T - parameters_mean).T
        covariance_mat = normalized_X.dot(normalized_X.T)/sample_size
        self.eigenvalues, self.eigenvectors = np.linalg.eig(covariance_mat)
        idx = self.eigenvalues.argsort()[::-1]
        self.eigenvalues, self.eigenvectors = self.eigenvalues[idx], self.eigenvectors[:, idx].T

    def reduce_dimension(self, k):
        """
        k: The dimension of space that we are mapping X to.
        """
        assert k < self.dimension, "PCA target dimension should be smaller than the input data dimension"
        P = self.eigenvectors[:k, :]
        return P.dot(self.X)


import numpy as np

class LU:
    matrix = [[]]
    L = [[]]
    U = [[]]
    B = []
    def __init__(self, matrix, vector):
        self.matrix = matrix
        self.B = vector
        self.L = [[{True:1, False:0}[x==y] for y in range(len(self.matrix))] for x in range(len(self.matrix))]

    def solve(self):
        if not len(self.matrix) == len(self.matrix[0]):
            return
        for index in range(len(self.matrix)-1):
            pivote = self.matrix[index][index]
            for row in range(index+1, len(self.matrix)):
                magic_number = self.matrix[row][index]/pivote
                self.L[row][index] = magic_number
                self.matrix[row] = [self.matrix[row][x] - magic_number * self.matrix[index][x] for x in range(len(self.matrix))]
        Linv = np.linalg.inv(np.array(self.L))
        self.Y = Linv.dot(np.array(self.B))
        print("the solution is Y = " + str(self.Y))

    def printL(self):
        print("L:")
        for row in self.L:
            print(row)

    def printMatrix(self):
        print("matrix:")
        for row in self.matrix:
            print(row)

test = LU([[9,1,1],[15,1,1],[1,1,1]],[1,1,1])
test.solve()
test.printL()
test.printMatrix()

test2 = LU([[1,4,-2],[3,-2,5],[2,3,1]] , [3,14,11])
test2.solve()
test2.printL()
test2.printMatrix()


import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import random, math, copy, statistics
from IPython.display import clear_output
random.seed(1)

def popInit(tPop=100):
    pop = []
    for n in range(tPop):
        individuo = [0]*10
        for i in range(len(individuo)):
            individuo[i] = random.choice([i for i in range(-100,101)])
        pop.append([calcFitness(individuo), individuo])
    return pop

def F6(x,y):
    return 0.5 - (((math.sin( (x**2+y**2)**0.5 ))**2 - 0.5)/(1 + ( 0.001 * ( x**2 + y**2 ) ))**2)

def calcFitness(individuo):
    return (F6(individuo[0],individuo[1]) + F6(individuo[2],individuo[3]) + F6(individuo[4],individuo[5]) + F6(individuo[6],individuo[7]) + F6(individuo[8],individuo[9]))

def sortPais(populacao, popFitness):

    fitnessTotal = sum(popFitness)
    soma = 0
    selecao = []

    for i in range(2):
        posi = 0
        valorAleatorio = random.uniform(0,1)

        stop = (fitnessTotal * valorAleatorio)

        for individuo in populacao:
            soma += individuo[0]

            if (soma > stop):
                selecao.append(posi)

            posi += 1

    return selecao[0], selecao[1]

def crUniforme(populacao, popFitness, txCruzamento, mascara):
    posiPai1,posiPai2 = sortPais(pop, popFitness)

    pai1 = populacao[posiPai1][1]
    pai2 = populacao[posiPai2][1]

    if txCruzamento > random.uniform(0,1):

        filho1 = [0]*len(mascara)
        filho2 = [0]*len(mascara)

        for i in range(len(mascara)):
            if mascara[i] == 0:
                filho1[i] = pai1[i]
                filho2[i] = pai2[i]

            elif mascara[i] == 1:
                filho1[i] = pai2[i]
                filho2[i] = pai1[i]

        novosIndividuos = [filho1, filho2]

        for i in range(2):
            fitness = calcFitness(novosIndividuos[i])
            novosIndividuos[i] = [fitness, novosIndividuos[i]]
        return novosIndividuos[0], novosIndividuos[1]

    else:
        pais = [pai1,pai2]
        for i in range(2):
            fitness = calcFitness(pais[i])
            pais[i] = [fitness, pais[i]]
        return pais[0], pais[1]

def criaMascara(pop):
    tamanhoPopulacao = len(pop)
    tamanhoCromossomo = len(pop[0][1])
    mask = [[0]*tamanhoCromossomo for i in range(int((tamanhoPopulacao/2)))]
    for j in range(int(tamanhoPopulacao/2)):
        for i in range(tamanhoCromossomo):
            mask[j][i] = random.randint(0,1)

    return mask

# Função para realizar mutação em um indivíduo
def mutacao(ind, txMutacao):

    posi = 0

    for gene in ind:
        if txMutacao > abs(random.gauss(0,2)):
            ind[posi] = random.choice(np.arange(-100,101))
        posi += 1

def calculaMediaExecucoes(melhoresIndExec, popFitExec):
    mediaMeIndExec = [0]*500
    mediaPopFitExec = [0]*500
    for i in range(len(melhoresIndExec[0])):
        holder = []
        for execucao in melhoresIndExec:
            holder.append(execucao[i])
        mediaMeIndExec[i] = statistics.mean(holder)

        holder = []
        for execucao in popFitExec:
            holder.append(execucao[i])
        mediaPopFitExec[i] = statistics.mean(holder)

    return mediaMeIndExec, mediaPopFitExec

def calculaMediaPopulacoes(popsMeExec, popsFitPopExec):
    mediaPopsMeInd = [0]*500
    mediaPopsFitPop = [0]*500
    for i in range(len(popsMeExec[0])):
        holder = []
        for execucao in popsMeExec:
            holder.append(execucao[i])
        mediaPopsMeInd[i] = statistics.mean(holder)

        holder = []
        for execucao in popsFitPopExec:
            holder.append(execucao[i])
        mediaPopsFitPop[i] = statistics.mean(holder)

    return mediaPopsMeInd, mediaPopsFitPop

tamanhoPopulacao = 100
epocas = 500
txCruzamento = 0.8
txMutacao = 0.01

nPopInicial = 5
nExecucoes = 30

tiposCruzamento = 1

elitismo = 1

popsMeExec = [ [0]*epocas ]*nPopInicial
popsFitPopExec = [ [0]*epocas ]*nPopInicial

for populacaoInicial in range(nPopInicial):
    populacao = popInit(tamanhoPopulacao)

    melhoresIndExec = [0]*nExecucoes
    popFitExec = [0]*nExecucoes
    for ex in range(nExecucoes):

        pop = copy.copy(populacao)
        pop.sort()

        melhoresEpoca = [0] * epocas
        mediaPopFit = [0] * epocas

        mascaras = criaMascara(pop)

        for e in range(epocas):

            fPop = [0]*tamanhoPopulacao
            # Armazena a fitness de todos os indivíduos
            for individuo in range(tamanhoPopulacao):
                fPop[individuo] = pop[individuo][0]

            melhoresEpoca[e] = max(fPop)
            mediaPopFit[e] = statistics.mean(fPop)

            # crossover

            #Cruzamento Cruzamento Uniforme
            novaGeracao = []
            for i in range(50):
                ind1, ind2 = crUniforme(pop, fPop, txCruzamento, mascaras[i])
                novaGeracao.append(ind1)
                novaGeracao.append(ind2)

            # mutacao
            for individuo in novaGeracao:
                mutacao(individuo[1], txMutacao)

            if elitismo == 1:
                for i in range(tamanhoPopulacao-1):
                    pop[i] = novaGeracao[i]
                pop.sort()
            else:
                pop = novaGeracao
                pop.sort()
        #end for e in range(epocas)
        print(f'Execução: {ex+1}')
        print(f'População: {populacaoInicial+1}')
        # clear_output(wait=True)

        melhoresIndExec[ex] = melhoresEpoca
        popFitExec[ex] = mediaPopFit
    #end for ex in range(execucoes)
    popsMeExec[populacaoInicial], popsFitPopExec[populacaoInicial] = calculaMediaExecucoes(melhoresIndExec, popFitExec)

[bestInd,fitnessPopulation] = calculaMediaPopulacoes(popsMeExec, popsFitPopExec)

# plt.figure()
# plt.title('Roleta | Dois Pontos de Corte')

# plt.plot(popsMeExec, 'bo', markersize=2, label='Melhores Indivíduos')
# plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.)

# plt.plot(popsFitPopExec, 'ro', markersize=2, label='Media População')
# plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.)
# plt.show()

y = {'Época': [i for i in range(500)], 'Melhores Indivíduos': bestInd, 'Média Fitness da População': fitnessPopulation}

df = pd.DataFrame(data=y, columns=['Época','Melhores Indivíduos', 'Média Fitness da População'])

grafico1 = px.scatter(df, x='Época', y=['Melhores Indivíduos','Média Fitness da População'],
                      range_x=[-50,550],range_y=[2,5], title='F6',)
# grafico2 = px.scatter(x = [i+1 for i in range(epocas)], y = fitnessPopulation, range_y=[0,5])
# grafico3 = go.Figure(data = grafico1.data + grafico2.data)
grafico1.update_layout(title={
                        'text': "AG F6 Modificada",
                        'y':0.9,
                        'x':0.5,
                        'xanchor': 'center',
                        'yanchor': 'top'},
                        xaxis_title="Época",
                        yaxis_title="Fitness",
                        legend_title="Legenda",
                        font=dict(
                            family="Courier New, monospace",
                            size=18,
                        ))
grafico1.show()