# 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


import numpy as np
import pandas as pd
import scipy as sc
from scipy import stats

# Input a pandas series
def ent(data):
    p_data = data.value_counts() / len(data)  # calculates the probabilities
    entropy = sc.stats.entropy(p_data, base=2)  # input probabilities to get the entropy
    # entropy = sc.stats.entropy(p_data)  # input probabilities to get the entropy
    return entropy

data = pd.Series(np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]))
print(ent(data))

data = pd.Series(np.array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]))
print(ent(data))


# -*- 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
import matplotlib.pyplot as plt
a=1
b=100
N=100
X1=np.random.uniform(a,b,N)
plt.plot(X1)
plt.show()
plt.figure()
plt.hist(X1, density=True, histtype='stepfilled', alpha=0.2)
plt.show()

a=1
b=100
N=10000
X2=np.random.uniform(a,b,N)

plt.figure()
plt.plot(X2)
plt.show()

plt.figure()
plt.hist(X2, density=True, histtype='stepfilled', alpha=0.2)
plt.show()


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()


import math
from numpy.core.umath_tests import inner1d
import matplotlib.pyplot as plt
import cartopy
import cartopy.crs as ccrs
import numpy as np
from shapely.geometry.polygon import Polygon
import pygplates
import pandas as pd

########## BASIC CALCULATIONS ##########

def lat_lon_2_cart(lat, lon):
    """
    Convert lat/lon coordinates to cartesian.

    Parameters
    ----------
    lat : array-like
        latitude (-90 to 90)
    lon : array-like
        longitude (-180 to 180)

    Returns
    -------
    cart : tuple
        (x, y, z) cartesian coordinates on the unit sphere
    """
    # convert to radians
    lat = math.radians(lat)
    lon = math.radians(lon)
    # calculations
    x = math.cos(lon) * math.cos(lat)
    y = math.sin(lon) * math.cos(lat)
    z = math.sin(lat)
    cart = (x, y, z)

    return cart

def cart_2_lat_lon(cart):
    """
    Convert cartesian coordinates to lat/lon.

    Parameters
    ----------
    cart : tuple
        (x, y, z) cartesian coordinates on the unit sphere

    Returns
    -------
    lat : array-like
        latitude (-90 to 90)
    lon : array-like
        longitude (-180 to 180)
    """
    # calculations
    lon = math.atan2(cart[1], cart[0])
    lat = math.atan2(cart[2], math.sqrt(cart[0]**2 + cart[1]**2))
    # convert to degrees
    lat = math.degrees(lat)
    lon = math.degrees(lon)

    return lat, lon

def fast_cross(a, b):
    """
    3D matrix cross multiplication.
    source: http://ssb.stsci.edu/doc/stsci_python_x/stsci.sphere.doc/html/_modules/stsci/sphere/great_circle_arc.html

    Parameters
    ----------
    a, b : numpy arrays
        matrices to be cross multiplied

    Returns
    -------
    cp : numpy array
        cross product
    """
    cp = np.empty(np.broadcast(a, b).shape)
    aT = a.T
    bT = b.T
    cpT = cp.T
    cpT[0] = aT[1]*bT[2] - aT[2]*bT[1]
    cpT[1] = aT[2]*bT[0] - aT[0]*bT[2]
    cpT[2] = aT[0]*bT[1] - aT[1]*bT[0]

    return cp

def cross_and_normalize(A, B):
    """
    3D matrix cross multiplication and normalized.
    source: http://ssb.stsci.edu/doc/stsci_python_x/stsci.sphere.doc/html/_modules/stsci/sphere/great_circle_arc.html

    Parameters
    ----------
    A, B : numpy arrays
        matrices to be cross multiplied

    Returns
    -------
    TN : numpy array
        normalized cross product
    """
    T = fast_cross(A, B)
    # normalization
    l = np.sqrt(np.sum(T ** 2, axis=-1))
    l = np.expand_dims(l, 2)
    # might get some divide-by-zeros, but we don't care
    with np.errstate(invalid='ignore'):
        TN = T / l

    return TN

def intersection(A, B, C, D):
    """
    Point of intersection between two great circle arcs.
    source: http://ssb.stsci.edu/doc/stsci_python_x/stsci.sphere.doc/html/_modules/stsci/sphere/great_circle_arc.html

    Parameters
    ----------
    A, B : (*x*, *y*, *z*) triples or Nx3 arrays of triples
        Endpoints of the first great circle arc.
    C, D : (*x*, *y*, *z*) triples or Nx3 arrays of triples
        Endpoints of the second great circle arc.

    Returns
    -------
    T : (*x*, *y*, *z*) triples or Nx3 arrays of triples
        If the given arcs intersect, the intersection is returned. If the arcs do not intersect,
        the triple is set to all NaNs.
    """
    A = np.asanyarray(A)
    B = np.asanyarray(B)
    C = np.asanyarray(C)
    D = np.asanyarray(D)

    A, B = np.broadcast_arrays(A, B)
    C, D = np.broadcast_arrays(C, D)

    ABX = fast_cross(A, B)
    CDX = fast_cross(C, D)
    T = cross_and_normalize(ABX, CDX)
    T_ndim = len(T.shape)

    if T_ndim > 1:
        s = np.zeros(T.shape[0])
    else:
        s = np.zeros(1)
    s += np.sign(inner1d(fast_cross(ABX, A), T))
    s += np.sign(inner1d(fast_cross(B, ABX), T))
    s += np.sign(inner1d(fast_cross(CDX, C), T))
    s += np.sign(inner1d(fast_cross(D, CDX), T))
    if T_ndim > 1:
        s = np.expand_dims(s, 2)

    cross = np.where(s == -4, -T, np.where(s == 4, T, np.nan))

    # If they share a common point, it's not an intersection.  This
    # gets around some rounding-error/numerical problems with the
    # above.
    equals = (np.all(A == C, axis=-1) |
              np.all(A == D, axis=-1) |
              np.all(B == C, axis=-1) |
              np.all(B == D, axis=-1))

    equals = np.expand_dims(equals, 2)

    T = np.where(equals, np.nan, cross)

    return T

########## ZONAL CALCULATIONS ##########

def check_polygon_in_band(polygon, lat_min, lat_max):
    """
    Check to see whether any part of a given polygon is inside a given latitude band.

    Parameters
    ----------
    polygon : pygpplates polygon
    lat_min : float
        the minimum latitude of the latitude band
    lat_max : float
        the maximum latitude of the latitude band

    Returns
    -------
    in_band : boolean
        True if inside, False if outside
    """
    # pull out lat/lon vertices
    lat_lon_array = polygon.to_lat_lon_array()
    lats = lat_lon_array[:,0]

    # check to see if any part of the polygon falls into our latitude band
    in_band = False
    for j in range(len(lats)):
        if lats[j]>lat_min and lats[j]<lat_max:
            in_band = True
            break

    return in_band

def get_area_in_band(polygon, lat_min, lat_max):
    """
    Calculate the area of a given polygon inside a given latitude band.

    Parameters
    ----------
    polygon : pygpplates polygon
    lat_min : float
        the minimum latitude of the latitude band
    lat_max : float
        the maximum latitude of the latitude band

    Returns
    -------
    area : float
        the area of the polygon within the latitude band (in km^2)
    band_polygon : pygplates polygon
        with the parts outside of the latitude band removed
    """
    # pull out lat/lon vertices
    lat_lon_array = polygon.to_lat_lon_array()
    lats = lat_lon_array[:,0]
    lons = lat_lon_array[:,1]

    # storage lists
    bookmarks = []
    lat_add_list = []
    lon_add_list = []

    # iterate through the points
    for i in range(1,len(lats)):
        top_cross = False
        bot_cross = False

        # case where we move into the band from above
        if lats[i-1]>lat_max and lats[i]<lat_max:
            top_cross = True
        # case where we move out of the band from below
        if lats[i-1]<lat_max and lats[i]>lat_max:
            top_cross = True
        # case where we move out of the band from above
        if lats[i-1]>lat_min and lats[i]<lat_min:
            bot_cross = True
        # case where we move into the band from below
        if lats[i-1]<lat_min and lats[i]>lat_min:
            bot_cross = True

        # do calculations if we cross
        if top_cross or bot_cross:

            # convert the endpoints of the polygon segment into cartesian
            A = lat_lon_2_cart(lats[i-1], lons[i-1])
            B = lat_lon_2_cart(lats[i]  , lons[i])

            # get the intersection point (for the top and bottom cases), and convert back to lat/lon
            if top_cross:
                C_top = lat_lon_2_cart(lat_max, min([lons[i],lons[i-1]]))
                D_top = lat_lon_2_cart(lat_max, max([lons[i],lons[i-1]]))
                T = intersection(A, B, C_top, D_top)
            else:
                C_bot = lat_lon_2_cart(lat_min, min([lons[i],lons[i-1]]))
                D_bot = lat_lon_2_cart(lat_min, max([lons[i],lons[i-1]]))
                T = intersection(A, B, C_bot, D_bot)
            lat_add, lon_add = cart_2_lat_lon(T)

            # add to the storage lists
            bookmarks.append(i)
            lat_add_list.append(lat_add)
            lon_add_list.append(lon_add)

    # now insert the stored values into the original arrays
    new_lats = np.insert(lats, bookmarks, lat_add_list)
    new_lons = np.insert(lons, bookmarks, lon_add_list)

    # only keep values below the maximum latitude (with small buffer)
    mask = np.less(new_lats, lat_max+0.1)
    new_lats = new_lats[mask]
    new_lons = new_lons[mask]

    # only keep values above the minimum latitude
    mask = np.greater(new_lats, lat_min-0.1)
    new_lats = new_lats[mask]
    new_lons = new_lons[mask]

    # create a Polygon, if we are left with enough points
    if len(new_lats) >= 3:
        band_polygon = pygplates.PolygonOnSphere(zip(new_lats,new_lons))

        # get the area in km2
        area = band_polygon.get_area() * 6371.009**2

    # if we don't...
    else:
        area = 0
        band_polygon = None

    return area, band_polygon

def get_areas_in_bands(reconstructed_feature_geometries, lat_mins, lat_maxs):
    """
    Get the area of all features in each latitude band.

    Parameters
    ----------
    reconstructed_feature_geometries : list
        list of reconstructed features
    lat_mins : array-like
        array-like of latitude minimums
    lat_maxs : array_like
        array_like of latitude maximums

    Returns
    -------
    areas : array
        list of total area in each latitude band
    area_polygons : list
        list of all polygons for which areas were calculated
    """
    # storage vectors
    areas = np.array([])
    area_polygons = []

    # iterate over each latitude band
    for i in range(len(lat_mins)):

        accumulated_area = 0

        # iterate over each polygon
        for j in range(len(reconstructed_feature_geometries)):

            current_polygon = reconstructed_feature_geometries[j].get_reconstructed_geometry()

            # check if the polygon is in the band
            in_band = check_polygon_in_band(current_polygon, lat_mins[i], lat_maxs[i])

            if in_band:
                # do the calculation
                area, band_polygon = get_area_in_band(current_polygon, lat_mins[i], lat_maxs[i])

                # store results
                accumulated_area = accumulated_area + area
                area_polygons.append(band_polygon)

        # store total area for the band
        areas = np.append(areas, accumulated_area)

    return areas, area_polygons

def get_length_in_band(polyline, lat_min, lat_max):
    """
    Calculate the length of a given polyline inside a given latitude band.

    Parameters
    ----------
    polyline : pygpplates polyline
    lat_min : float
        the minimum latitude of the latitude band
    lat_max : float
        the maximum latitude of the latitude band

    Returns
    -------
    length : float
        the length of the polyline within the latitude band (in km)
    band_polylines : list
        pygplates polylines with the parts outside of the latitude band removed
    """
    # pull out lat/lon vertices
    lat_lon_array = polyline.to_lat_lon_array()
    lats = lat_lon_array[:,0]
    lons = lat_lon_array[:,1]

    # storage lists
    bookmarks = []
    lat_add_list = []
    lon_add_list = []

    # iterate through the points
    for i in range(1,len(lats)):
        top_cross = False
        bot_cross = False

        # case where we move into the band from above
        if lats[i-1]>lat_max and lats[i]<lat_max:
            top_cross = True
        # case where we move out of the band from below
        if lats[i-1]<lat_max and lats[i]>lat_max:
            top_cross = True
        # case where we move out of the band from above
        if lats[i-1]>lat_min and lats[i]<lat_min:
            bot_cross = True
        # case where we move into the band from below
        if lats[i-1]<lat_min and lats[i]>lat_min:
            bot_cross = True

        # do calculations if we cross
        if top_cross or bot_cross:

            # convert the endpoints of the polygon segment into cartesian
            A = lat_lon_2_cart(lats[i-1], lons[i-1])
            B = lat_lon_2_cart(lats[i]  , lons[i])

            # get the intersection point (for the top and bottom cases), and convert back to lat/lon
            if top_cross:
                C_top = lat_lon_2_cart(lat_max, min([lons[i],lons[i-1]]))
                D_top = lat_lon_2_cart(lat_max, max([lons[i],lons[i-1]]))
                T = intersection(A, B, C_top, D_top)
            else:
                C_bot = lat_lon_2_cart(lat_min, min([lons[i],lons[i-1]]))
                D_bot = lat_lon_2_cart(lat_min, max([lons[i],lons[i-1]]))
                T = intersection(A, B, C_bot, D_bot)
            lat_add, lon_add = cart_2_lat_lon(T)

            # add to the storage lists
            bookmarks.append(i)
            lat_add_list.append(lat_add)
            lon_add_list.append(lon_add)

    # now insert the stored values into the original arrays
    new_lats = np.insert(lats, bookmarks, lat_add_list)
    new_lons = np.insert(lons, bookmarks, lon_add_list)

    # mask points above and below the latitude band (with small buffer)
    top_mask = np.less(new_lats, lat_max+0.1)
    bot_mask = np.greater(new_lats, lat_min-0.1)
    mask = top_mask & bot_mask

    # initiate final output variabiles
    length = 0
    band_polylines = []

    # initiate lat lon arrays for the current polyline that is being split out
    split_lats = np.array([])
    split_lons = np.array([])
    hold = False

    # iterate through the mask
    for i in range(len(mask)):
        if mask[i]:
            # begin holding, and store points
            hold = True
            split_lats = np.append(split_lats, new_lats[i])
            split_lons = np.append(split_lons, new_lons[i])

        # i.e. the first time we reach a point outside of the band
        elif hold:
            # create a polyline, if we are left with enough points
            if len(split_lats) >= 2:
                band_polyline = pygplates.PolylineOnSphere(zip(split_lats,split_lons))

                # get the length in km
                length_split = band_polyline.get_arc_length() * pygplates.Earth.mean_radius_in_kms

                # store
                length = length + length_split
                band_polylines.append(band_polyline)

            # reset stuff
            split_lats = np.array([])
            split_lons = np.array([])
            hold = False

    # if the last point was within the band
    if hold:
        # create a polyline, if we are left with enough points
        if len(split_lats) >= 2:
            band_polyline = pygplates.PolylineOnSphere(zip(split_lats,split_lons))

            # get the length in km
            length_split = band_polyline.get_arc_length() * pygplates.Earth.mean_radius_in_kms

            # store
            length = length + length_split
            band_polylines.append(band_polyline)

    return length, band_polylines

def get_lengths_in_bands(reconstructed_feature_geometries, lat_mins, lat_maxs):
    """
    Get the area of all features in each latitude band.

    Parameters
    ----------
    reconstructed_feature_geometries : list
        list of reconstructed features
    lat_mins : array-like
        array-like of latitude minimums
    lat_maxs : array_like
        array_like of latitude maximums

    Returns
    -------
    accumulated_lengths : array
        array of total area in each latitude band
    length_polylines : polylines
        list of all polylines for which areas were calculated
    polyline_attributes : Pandas Dataframe
        Dataframe with names, lengths, latitudes and recon_times for each polyline
    """
    # storage vectors
    lengths = np.array([])
    accumulated_lengths = np.array([])
    length_polylines = []
    names = []
    polyline_lat_min = []
    polyline_lat_max = []
    recon_times = []

    # iterate over each latitude band
    for i in range(len(lat_mins)):

        accumulated_length = 0

        # iterate over each polygon
        for j in range(len(reconstructed_feature_geometries)):

            current_polyline = reconstructed_feature_geometries[j].get_reconstructed_geometry()

            # check if the polygon is in the band
            in_band = check_polygon_in_band(current_polyline, lat_mins[i], lat_maxs[i])

            if in_band:
                # do the calculation
                length, band_polyline = get_length_in_band(current_polyline, lat_mins[i], lat_maxs[i])
                name = reconstructed_feature_geometries[j].get_feature().get_name()
                recon_time = reconstructed_feature_geometries[j].get_reconstruction_time()

                # store results
                accumulated_length = accumulated_length + length
                length_polylines.append(band_polyline)
                names.append(name)
                lengths = np.append(lengths, length)
                polyline_lat_min.append(lat_mins[i])
                polyline_lat_max.append(lat_maxs[i])
                recon_times.append(recon_time)

        # store total area for the band
        accumulated_lengths = np.append(accumulated_lengths, accumulated_length)

        polyline_attributes = pd.DataFrame({'name':names,'length':lengths,'polyline_lat_min':polyline_lat_min,'polyline_lat_max':polyline_lat_max,'recon_time':recon_times})

    return accumulated_lengths, length_polylines, polyline_attributes

def get_points_in_bands(reconstructed_feature_geometries, lat_mins, lat_maxs):
    """
    Get the number of all features in each latitude band.

    Parameters
    ----------
    reconstructed_feature_geometries : list
        list of reconstructed features
    lat_mins : array-like
        array-like of latitude minimums
    lat_maxs : array_like
        array_like of latitude maximums

    Returns
    -------
    points : array
        list of total number of points in each latitude band
    """
    # storage vector
    points = np.zeros(len(lat_mins))

    # iterate over each point
    for i in range(len(reconstructed_feature_geometries)):

        # get the latitude of the point
        current_point = reconstructed_feature_geometries[i].get_reconstructed_geometry()
        current_lat = current_point.to_lat_lon_array()[:,0]

        # put the point where it belongs
        for j in range(len(points)):
            if current_lat>lat_mins[j] and current_lat<lat_maxs[j]:
                points[j] = points[j] + 1

    return points

########## PLOTTING FUNCTIONS ##########

def plot_reconstructed_features(ax, reconstructed_feature, **kwargs):
    """
    Plot reconstructed features output by pygplates.

    Parameters
    ----------
    ax : axis handle
        axis set up by cartopy on which to plot

    reconstructed_feature : reconstructed features
        reconstructed features output by pygplates

    Other Parameters
    ----------------
    **kwargs passed to plt.add_geometries

    Returns
    -------
    None.
    """
    for n in range(len(reconstructed_feature)):

        # pull out lat/lon vertices
        lat_lon_array = reconstructed_feature[n].get_reconstructed_geometry().to_lat_lon_array()
        lats = lat_lon_array[:,0]
        lons = lat_lon_array[:,1]

        # polygons need to be drawn counter clockwise to be drawn properly if they cross the dateline
        if reconstructed_feature[n].get_reconstructed_geometry().get_orientation() == pygplates.PolygonOnSphere.Orientation.clockwise:
            lats = np.flip(lats)
            lons = np.flip(lons)

        # create the polygon and add it to the axis
        # note that ccrs.Geodetic() must be used to make sure that polygons that cross the dateline are drawn properly
        poly = Polygon(zip(lons,lats))
        ax.add_geometries([poly], ccrs.Geodetic(), **kwargs)

def plot_reconstructed_points(ax, reconstructed_feature, **kwargs):
    """
    Plot reconstructed points output by pygplates.

    Parameters
    ----------
    ax : axis handle
        axis set up by cartopy on which to plot

    reconstructed_feature : reconstructed features
        reconstructed features output by pygplates

    Other Parameters
    ----------------
    **kwargs passed to plt.add_geometries

    Returns
    -------
    None.
    """
    for n in range(len(reconstructed_feature)):

        # pull out lat/lon vertices
        lat_lon_array = reconstructed_feature[n].get_reconstructed_geometry().to_lat_lon_array()
        lats = lat_lon_array[:,0]
        lons = lat_lon_array[:,1]

        ax.scatter(lons, lats, transform=ccrs.Geodetic(), **kwargs)

########## OLD FUNCTIONS ##########

def weather_LIPs_OLD(reconstructed_feature_geometries, LIP_fracs, Ts, Rs, t_step, thickness, density):
    """
    A model for weathering LIPs, using weathering equation from Dessert et al. (2003).

    Parameters
    ----------
    reconstructed_feature_geometries : list
        list of reconstructed features
    LIP_fracs : dictionary
        with keys = LIP names, values = LIP fraction remaining
    Ts : dataframe
        of zonal temperature
    Rs : dataframe
        of zonal runoff
    t_step : float
        time since last weathering calculation (million years)
    thickness : float
        LIP thickness (km)
    density : float
        LIP density (kg/km^3)

    Returns
    -------
    None.

    Notes
    -----
    modifies LIP_fracs in place
    """
    # only calculate if there are geometries
    if len(reconstructed_feature_geometries) > 0:
        # the time
        t = int(reconstructed_feature_geometries[0].get_reconstruction_time())

        # only calculate if the t is in our climate model
        T_string = 'T_C_'+str(t)
        if T_string in Ts.columns:

            # iterate over each polygon
            for i in range(len(reconstructed_feature_geometries)):

                # get the name
                current_name = reconstructed_feature_geometries[i].get_feature().get_name()

                # only calculate if there is some LIP left
                if LIP_fracs[current_name] > 0:

                    # get the polygon, ID, area, mass, and centre latitude
                    current_polygon = reconstructed_feature_geometries[i].get_reconstructed_geometry()

                    og_mass = current_polygon.get_area() * (6371.009**2) * thickness * density

                    current_area = (current_polygon.get_area() * 6371.009**2) * LIP_fracs[current_name]
                    current_mass = current_area * thickness * density

                    centre_point = current_polygon.get_interior_centroid()
                    centre_lat = centre_point.to_lat_lon_array()[0][0]

                    # get the T and R at this time and latitude
                    for j in range(len(Ts.index)):
                        if abs(centre_lat)<Ts['lat_maxs'][j] and abs(centre_lat)>Ts['lat_mins'][j]:
                            T = Ts['T_C_'+str(t)][j]
                    for j in range(len(Rs.index)):
                        if centre_lat<Rs['lat_maxs'][j] and centre_lat>Rs['lat_mins'][j]:
                            R = Rs['R_m/yr_'+str(t)][j]

                    # get the weathering rate (t/km^2/yr) - from Dessert et al. (2003)
                    fw = (R * 18.41 * np.exp(0.0533 * T)) / 0.649

                    # weather (kg)
                    weather_mass = fw * current_area * abs(t_step)*1e6 * 1000

                    # update the mass
                    current_mass = current_mass - weather_mass

                    # update the remaining fraction in place
                    if current_mass > 0:
                        LIP_fracs[current_name] = current_mass / og_mass
                    else:
                        LIP_fracs[current_name] = 0

        # if the t is not in our climate model, fill the dictionary with nans
        else:
            # if the dictionary is already filled with nans, we can move on
            if math.isnan(LIP_fracs[reconstructed_feature_geometries[0].get_feature().get_name()]):
                pass
            else:
                for key, value in LIP_fracs.iteritems():
                    LIP_fracs[key] = float('NaN')

def get_LIP_areas_in_bands_OLD(reconstructed_feature_geometries, lat_mins, lat_maxs, LIP_fracs):
    """
    Get the area of all LIP features in each latitude band, accounting for weathering.

    Parameters
    ----------
    reconstructed_feature_geometries : list
        list of reconstructed features
    lat_mins : array-like
        array-like of latitude minimums
    lat_maxs : array_like
        array_like of latitude maximums
    LIP_fracs : dictionary
        with keys = LIP names, values = LIP fraction remaining

    Returns
    -------
    areas : list
        list of total area in each latitude band
    area_polygons : list
        list of all polygons for which areas were calculated
    """
    # only calculate if there are geometries
    if len(reconstructed_feature_geometries) > 0:
        # if our LIP_fracs has nan's
        if math.isnan(LIP_fracs[reconstructed_feature_geometries[0].get_feature().get_name()]):
            areas = [np.nan]*len(lat_mins)
            area_polygons = None

        # if our LIP_fracs has values
        else:
            # storage vectors
            areas = []
            area_polygons = []

            # iterate over each latitude band
            for i in range(len(lat_mins)):

                accumulated_area = 0

                # iterate over each polygon
                for j in range(len(reconstructed_feature_geometries)):

                    current_polygon = reconstructed_feature_geometries[j].get_reconstructed_geometry()
                    current_name = reconstructed_feature_geometries[j].get_feature().get_name()

                    # check if the polygon is in the band
                    in_band = check_polygon_in_band(current_polygon, lat_mins[i], lat_maxs[i])

                    if in_band:
                        # do the calculation
                        area, band_polygon = get_area_in_band(current_polygon, lat_mins[i], lat_maxs[i])

                        # scale the area appropriately
                        area = area * LIP_fracs[current_name]

                        # store results
                        accumulated_area = accumulated_area + area
                        area_polygons.append(band_polygon)

                # store total area for the band
                areas.append(accumulated_area)

    else:
        areas = [0]*len(lat_mins)
        area_polygons = []

    return areas, area_polygons

########## OLD WEATHERING CALCULATIONS ##########

def get_LIP_areas_in_bands(reconstructed_feature_geometries, lat_mins, lat_maxs, thresh=None, halflife=None):
    """
    Get the area of all features in each latitude band, with optional calculations for:
    - features instantly disappearing after `thresh` years
    - features decaying exponentially

    Parameters
    ----------
    reconstructed_feature_geometries : list
        list of reconstructed features
    lat_mins : array-like
        array-like of latitude minimums
    lat_maxs : array_like
        array_like of latitude maximums
    thresh : array-like (optional)
        after this many Myrs, the feature will instantly disappear
    halflife : array-like (optional)
        half life of exponential decay

    Returns
    -------
    areas : array
        list of total area in each latitude band
    area_polygons : list
        list of all polygons for which areas were calculated

    Optional Returns
    ----------------
    areas_thresh : list of arrays
        list of total area in each latitude band, accounting for thresholding method
    areas_decay : list of arrays
        list of total area in each latitude band, accounting for exponential decay method
    """
    # storage vectors
    areas = np.array([])
    area_polygons = []

    # optional storage vectors
    if thresh != None:
        areas_thresh_temp = []
    if halflife != None:
        areas_decay_temp = []
        # convert halflife to decay constant
        lamb = np.log(2)/halflife

    # iterate over each latitude band
    for i in range(len(lat_mins)):

        accumulated_area = 0

        if thresh != None:
            accumulated_area_thresh = np.zeros(len(thresh))
        if halflife != None:
            accumulated_area_decay = np.zeros(len(halflife))

        # iterate over each polygon
        for j in range(len(reconstructed_feature_geometries)):

            if thresh != None or halflife != None:
                # get the begin date, reconstruction date, and feature age
                begin_date, end_date = reconstructed_feature_geometries[j].get_feature().get_valid_time()
                now_date = reconstructed_feature_geometries[j].get_reconstruction_time()
                feature_age = begin_date - now_date

            # get the actual polygon
            current_polygon = reconstructed_feature_geometries[j].get_reconstructed_geometry()

            # check if the polygon is in the band
            in_band = check_polygon_in_band(current_polygon, lat_mins[i], lat_maxs[i])

            if in_band:
                # do the calculation
                area, band_polygon = get_area_in_band(current_polygon, lat_mins[i], lat_maxs[i])

                # store results
                accumulated_area = accumulated_area + area
                area_polygons.append(band_polygon)

                if thresh != None:
                    for k in range(len(thresh)):
                        # check if the polygon is past its due by date
                        if begin_date < (now_date+thresh[k]):
                            still_alive = True
                        else:
                            still_alive = False

                        if still_alive:
                            accumulated_area_thresh[k] = accumulated_area_thresh[k] + area

                if halflife != None:
                    for k in range(len(halflife)):
                        # scale the area based on the exponential decay equation
                        decay_area = area * np.exp(-lamb[k]*feature_age)

                        # store results
                        accumulated_area_decay[k] = accumulated_area_decay[k] + decay_area

        # store total area for the band
        areas = np.append(areas, accumulated_area)

        if thresh != None:
            areas_thresh_temp.append(accumulated_area_thresh)
        if halflife != None:
            areas_decay_temp.append(accumulated_area_decay)

    # basically, flip our outputs so that each array in our list is for a given input thresh/halflife
    if thresh != None:
        areas_thresh = []
        for i in range(len(thresh)):
            this_array = np.array([])
            for j in range(len(areas_thresh_temp)):
                this_array = np.append(this_array, areas_thresh_temp[j][i])
            areas_thresh.append(this_array)
    if halflife != None:
        areas_decay = []
        for i in range(len(halflife)):
            this_array = np.array([])
            for j in range(len(areas_decay_temp)):
                this_array = np.append(this_array, areas_decay_temp[j][i])
            areas_decay.append(this_array)

    # returns
    if thresh == None and halflife == None:
        return areas, area_polygons
    elif thresh != None and halflife == None:
        return areas, area_polygons, areas_thresh
    elif thresh == None and halflife != None:
        return areas, area_polygons, areas_decay
    elif thresh != None and halflife != None:
        return areas, area_polygons, areas_thresh, areas_decay

def initialize_LIP_dict(LIP_feature_collection):
    """
    Initialize the dictionary which contains the LIP fraction remaining for all LIPs.

    Parameters
    ----------
    LIP_feature_collection : feature collection
        feature collection of LIPs

    Returns
    -------
    LIP_fracs : dictionary
        with keys = LIP Ids, values = LIP fraction remaining
    """
    # get the unique ID associated with each LIP geometry
    LIP_Ids = []
    for feature in LIP_feature_collection:
        LIP_Id = feature.get_feature_id().get_string()
        LIP_Ids.append(LIP_Id)

    # create a dictionary: key = LIP Id, value = LIP fraction remaining
    ones = [1]*len(LIP_Ids)
    LIP_fracs = dict(zip(LIP_Ids, ones))

    return LIP_fracs

def weather_LIPs(reconstructed_feature_geometries, LIP_fracs, climate_df, t_step, thickness, density):
    """
    A model for weathering LIPs, using weathering equation from Dessert et al. (2003).

    Parameters
    ----------
    reconstructed_feature_geometries : list
        list of reconstructed features
    LIP_fracs : dictionary
        with keys = LIP Ids, values = LIP fraction remaining
    climate_df : dataframe
        with columns: "lat_mins", "lat_maxs", "lat_mids", "T" (C), "R" (m/yr)
    t_step : float
        time since last weathering calculation (million years)
    thickness : float
        LIP thickness (km)
    density : float
        LIP density (kg/km^3)

    Returns
    -------
    None.

    Notes
    -----
    modifies LIP_fracs in place
    """
    # only calculate if there are geometries
    if len(reconstructed_feature_geometries) > 0:
        # the time
        t = int(reconstructed_feature_geometries[0].get_reconstruction_time())

        # iterate over each polygon
        for i in range(len(reconstructed_feature_geometries)):

            # get the Id
            current_Id = reconstructed_feature_geometries[i].get_feature().get_feature_id().get_string()

            # only calculate if there is some LIP left
            if LIP_fracs[current_Id] > 0:

                # get the polygon, ID, area, mass, and centre latitude
                current_polygon = reconstructed_feature_geometries[i].get_reconstructed_geometry()

                og_mass = current_polygon.get_area() * (6371.009**2) * thickness * density

                current_area = (current_polygon.get_area() * (6371.009**2)) * LIP_fracs[current_Id]
                current_mass = current_area * thickness * density

                centre_point = current_polygon.get_interior_centroid()
                centre_lat = centre_point.to_lat_lon_array()[0][0]

                # get the T and R at this latitude
                for j in range(len(climate_df.index)):
                    if abs(centre_lat)<climate_df['lat_maxs'][j] and abs(centre_lat)>climate_df['lat_mins'][j]:
                        T = climate_df['T'][j]
                        R = climate_df['R'][j]

                # get the weathering rate (t/km^2/yr) - from Dessert et al. (2003)
                fw = (R * 18.41 * np.exp(0.0533 * T)) / 0.649

                # weather (kg)
                weather_mass = fw * current_area * abs(t_step)*1e6 * 1000

                # update the mass
                current_mass = current_mass - weather_mass

                # update the remaining fraction in place
                if current_mass > 0:
                    LIP_fracs[current_Id] = current_mass / og_mass
                else:
                    LIP_fracs[current_Id] = 0


import math
import numpy as np
from utils import normalize_vector, rotate

class Torus:
	def __init__(self, R=1.0, r=0.4, theta_step=75, phi_step=75):
		self.R = R
		self.r = r
		self.theta_step = theta_step
		self.phi_step = phi_step
		self.thetas = np.arange(0, 2 * math.pi, 2 * math.pi / self.theta_step)
		self.phis = np.arange(0, 2 * math.pi, 2 * math.pi / self.phi_step)
		self.points = np.array([self.point_equation(theta, phi) for theta in self.thetas for phi in self.phis])
		self.normals = np.array([self.normal_equation(theta, phi) for theta in self.thetas for phi in self.phis])

	def point_equation(self, theta, phi):
		return np.array([
				(self.R + self.r * math.cos(phi)) * math.cos(theta),
				(self.R + self.r * math.cos(phi)) * math.sin(theta),
				self.r * math.sin(phi)
			])

	def normal_equation(self, theta, phi):
		return normalize_vector(np.array([
				math.cos(theta) * math.cos(phi),
				math.sin(theta) * math.cos(phi),
				math.sin(phi)
			]))

	def rotate(self, rotation_matrix_func, theta):
		self.points, self.normals = rotate(rotation_matrix_func, theta, self.points, self.normals)


import numpy as np
from scipy import signal

def myconv2d(image, filterr):
    m = filterr.shape[0]
    y, x = image.shape
    y = y - m + 1
    x = x - m + 1
    conv = np.zeros((y,x))
    for i in range(y):
        for j in range(x):
            conv[i][j] = np.sum(image[i:i+m, j:j+m]*filterr)
    return conv

image = np.array([[164,188,164,161,195],
                  [178,201,197,150,137],
                  [174,168,181,190,184],
                  [131,179,176,185,198],
                  [92,185,179,133,167]
                 ])
filter1 = np.array([[1,1,1],[1,1,1],[1,1,1]])
filter2 = np.array([[-1,-2,-1],[0,0,0],[1,2,1]])
filter3 = np.array([[-1,-1,-1],[-1,9,-1],[-1,-1,-1]])
#conv1 = signal.convolve2d(image, filter1, mode='valid')
#print(conv1)

conv_f1 = myconv2d(image, filter1)
conv_f2 = myconv2d(image, filter2)
conv_f3 = myconv2d(image, filter3)
print(conv_f1)
print(conv_f2)
print(conv_f3)


#2D Transformations are regularly used in Linear Algebra.

#I have added the codes for reflection, projection, scaling and rotation matrices.

import numpy as np

def scaling(scaling_factor):
	return(scaling_factor*(np.identity(2))) #This returns a scaling matrix

def rotation(angle):
	arr = np.empty([2, 2])
	c = np.cos(angle)
	s = np.sin(angle)
	arr[0][0] = c
	arr[0][1] = -s
	arr[1][0] = s
	arr[1][1] = c

	return arr #This returns a rotation matrix

def projection(angle):
	arr = np.empty([2, 2])
	c = np.cos(angle)
	s = np.sin(angle)
	arr[0][0] = c*c
	arr[0][1] = c*s
	arr[1][0] = c*s
	arr[1][1] = s*s

	return arr #This returns a rotation matrix

def reflection(angle):
	arr = np.empty([2, 2])
	c = np.cos(angle)
	s = np.sin(angle)
	arr[0][0] = (2*c) -1
	arr[0][1] = 2*s*c
	arr[1][0] = 2*s*c
	arr[1][1] = (2*s) -1

	return arr #This returns a reflection matrix


import numpy as np

def estimate_n_trials_needed(inlier_ratio: float, n_sample: int, probability: float = 0.95) -> int:
    """
    Estimate number of trials needed to select only true inliers with RANSAC.

    Parameters
    ----------
    inlier_ratio : number of inliers / number of all points
    n_sample : number of samples needed to fit model
    probability : desired probability of success.

    Returns
    -------
    Estimated number of trials needed.
    """
    return int(np.log(1 - probability) / np.log(1 - inlier_ratio ** n_sample))


# код (n, k) n -- символов в код. слове, k -- символов в сообщении
# пор. мат. G размера k×n
# пров. мат. H размер (n-k)×n
G = [
    [1, 0, 0, 0, 1, 1, 0],
    [0, 1, 0, 0, 1, 0, 1],
    [0, 0, 1, 0, 0, 1, 1],
    [0, 0, 0, 1, 1, 1, 1],
] # (7, 4)

import numpy as np

G = np.array(G)
print(G.T)

# In[0]
H = np.array([
    [1, 1, 0, 1, 1, 0, 0],
    [1, 0, 1, 1, 0, 1, 0],
    [0, 1, 1, 1, 0, 0, 1],
])

# H×G(T) = 0 (mod q)

np.matmul(H,G.T) % 2

# In[0]

def getH(G):
    k, n = G.shape
    H = np.zeros(shape=(n-k, n))
    for i in range(k, n):
        H[i-k, i] = 1
    for i in range(k):
        for j in range(k, n):
            H[j-k, i] = G[i, j]
    return H

# In[0]

def lincode(G, p=2):
    k,n = G.shape
    H = getH(G)
    tbl = {}
    for i in range(n):
        ei = np.zeros((n, 1))
        ei[i, 0] = 1
        si = np.reshape(np.matmul(H, ei) % p, n-k)
        tbl[str(si)] = np.reshape(ei, n)

    def encode(G, m):
        return np.reshape(np.matmul(G.T, np.reshape(np.array(m), (k, 1))) % p, n)

    def decode(G, c):
        s = np.reshape(np.matmul(H, c) % p, n-k)
        if sum(s) == 0:
            return c[:k]
        e = tbl[str(s)]
        c1 = c - e
        return c1[:k]

    return (encode, decode)

encode, decode = lincode(G = np.array([
    [1, 0, 0, 0, 1, 1, 0],
    [0, 1, 0, 0, 0, 1, 1],
    [0, 0, 1, 0, 1, 0, 1],
    [0, 0, 0, 1, 1, 1, 1],
])) # (7, 4)
decode(G,encode(G, [1,0,1,0])+np.array([0,0,0,1,0,0,0]))


import pandas as pd
import numpy as np
import os
import statistics
import math

class QuickQuery:
    def __init__(self):
        pass

    def get_mean(self, col_name, data_dir):
        num_rows = 0
        means = []
        for filename in os.listdir(data_dir):
            df = pd.read_csv(os.path.join(data_dir, filename))
            means.extend(df[col_name])
            num_rows += len(df.iloc[:, 0])
        return sum(means) / len(means), num_rows

    def get_median(self, col_name, data_dir):
        num_rows = 0
        meds = []
        for filename in os.listdir(data_dir):
            df = pd.read_csv(os.path.join(data_dir, filename))
            # meds.append(df[col_name].median())
            meds.extend(df[col_name])
            num_rows += len(df.iloc[:, 0])
        return statistics.median(meds), num_rows

    def get_mode(self, col_name, data_dir):
        num_rows = 0
        modes = []
        for filename in os.listdir(data_dir):
            df = pd.read_csv(os.path.join(data_dir, filename))
            modes.extend(df[col_name].tolist())
            num_rows += len(df.iloc[:, 0])

        return max(set(modes), key=modes.count),  num_rows

    def get_variance(self, col_name, data_dir):
        num_rows = 0
        vars = []
        for filename in os.listdir(data_dir):
            df = pd.read_csv(os.path.join(data_dir, filename))
            vars.extend(df[col_name])
            num_rows += len(df.iloc[:, 0])

        return np.var(vars), num_rows

    def get_stdev(self, col_name, data_dir):
        variance = self.get_variance(col_name, data_dir)
        stdev = math.sqrt(variance[0])

        return stdev, variance[1]

    def switcher(self, stat, col_name, dir ):

        switch = {
            'mean': self.get_mean(col_name, dir),
            'median': self.get_median(col_name, dir),
            'mode': self.get_mode(col_name, dir),
            'variance': self.get_variance(col_name, dir),
            'stdev': self.get_stdev(col_name, dir)
        }

        return switch[stat]

    def descriptive_stats(self, stat, col_name, game_dir=None, health_dir=None, visualize=False):

        if game_dir is not None:
            grtval, num_rows_game = self.switcher(stat, col_name, game_dir)
            # print(f'The {stat} of {col_name} for the selected game data is {grtval:.3f}')
        return num_rows_game, grtval, str(grtval)

        # if health_dir is not None:
        #     hrtval, num_rows_health = self.switcher(stat, col_name, health_dir)
            # print(f'The {stat} of {col_name} for the selected health data is {hrtval:.3f}')


import pandas as pd
import numpy as np
import scipy.stats as ss
from dython.nominal import theils_u, cramers_v
from sklearn.metrics import mean_squared_error
from scipy.spatial.distance import jensenshannon
from joblib import Parallel, delayed
from typing import Dict, Any, List
from scipy.stats import ks_2samp

def mean_absolute_error(y_true: np.ndarray, y_pred: np.ndarray):
    """
    Returns the mean absolute error between y_true and y_pred.

    :param y_true: NumPy.ndarray with the ground truth values.
    :param y_pred: NumPy.ndarray with the ground predicted values.
    :return: Mean absolute error (float).
    """
    return np.mean(np.abs(np.subtract(y_true, y_pred)))

def euclidean_distance(y_true: np.ndarray, y_pred: np.ndarray):
    """
    Returns the euclidean distance between y_true and y_pred.

    :param y_true: NumPy.ndarray with the ground truth values.
    :param y_pred: NumPy.ndarray with the ground predicted values.
    :returns: Mean absolute error (float).
    """
    return np.sqrt(np.sum(np.power(np.subtract(y_true, y_pred), 2)))

def mean_absolute_percentage_error(y_true: np.ndarray, y_pred: np.ndarray):
    """
    Returns the mean absolute percentage error between y_true and y_pred. Throws ValueError if y_true contains zero values.

    :param y_true: NumPy.ndarray with the ground truth values.
    :param y_pred: NumPy.ndarray with the ground predicted values.
    :return: Mean absolute percentage error (float).
    """
    y_true, y_pred = np.array(y_true), np.array(y_pred)
    return np.mean(np.abs((y_true - y_pred) / y_true))

def rmse(y_true: np.ndarray, y_pred: np.ndarray):
    """
    Returns the root mean squared error between y_true and y_pred.

    :param y_true: NumPy.ndarray with the ground truth values.
    :param y_pred: NumPy.ndarray with the ground predicted values.
    :return: root mean squared error (float).
    """
    return np.sqrt(mean_squared_error(y_true, y_pred))

def cosine_similarity(y_true: np.ndarray, y_pred: np.ndarray):
    y_true, y_pred = y_true.reshape(-1), y_pred.reshape(-1)
    return np.sum(y_true * y_pred) / (np.sqrt(np.sum(y_true ** 2)) * np.sqrt(np.sum(y_pred ** 2)))

def column_correlations(dataset_a, dataset_b, categorical_columns, theil_u=True):
    """
    Column-wise correlation calculation between ``dataset_a`` and ``dataset_b``.

    :param dataset_a: First DataFrame
    :param dataset_b: Second DataFrame
    :param categorical_columns: The columns containing categorical values
    :param theil_u: Whether to use Theil's U. If False, use Cramer's V.
    :return: Mean correlation between all columns.
    """
    if categorical_columns is None:
        categorical_columns = list()
    elif categorical_columns == 'all':
        categorical_columns = dataset_a.columns
    assert dataset_a.columns.tolist() == dataset_b.columns.tolist()
    corr = pd.DataFrame(columns=dataset_a.columns, index=['correlation'])

    for column in dataset_a.columns.tolist():
        if column in categorical_columns:
            if theil_u:
                corr[column] = theils_u(dataset_a[column].sort_values(), dataset_b[column].sort_values())
            else:
                corr[column] = cramers_v(dataset_a[column].sort_values(), dataset_b[column].sort_vaues())
        else:
            corr[column], _ = ss.pearsonr(dataset_a[column].sort_values(), dataset_b[column].sort_values())
    corr.fillna(value=np.nan, inplace=True)
    correlation = np.mean(corr.values.flatten())
    return correlation

def js_distance_df(real: pd.DataFrame, fake: pd.DataFrame, numerical_columns: List) -> pd.DataFrame:
    assert real.columns.tolist() == fake.columns.tolist(), f'Colums are not identical between `real` and `fake`. '
    real_iter = real[numerical_columns].iteritems()
    fake_iter = fake[numerical_columns].iteritems()
    distances = Parallel(n_jobs=-1)(
        delayed(jensenshannon_distance)
        (colname, real_col, fake_col) for (colname, real_col), (_, fake_col) in zip(real_iter, fake_iter))

    distances_df = pd.DataFrame(distances)
    # distances_df = distances_df.append({'colname': 'mean', 'js_distance': distances_df.js_distance.mean()})
    return distances_df.set_index('col_name')

def jensenshannon_distance(colname: str, real_col: pd.Series, fake_col: pd.Series, bins=25) -> Dict[str, Any]:
    binned_values_real, bins = pd.cut(real_col, bins=bins, retbins=True)
    binned_probs_real = binned_values_real.value_counts(normalize=True, sort=False)
    binned_probs_fake = pd.cut(fake_col, bins=bins).value_counts(normalize=True, sort=False)
    js_distance = jensenshannon(binned_probs_real, binned_probs_fake)
    return {'col_name': colname, 'js_distance': js_distance}

def kolmogorov_smirnov_test(col_name, real_col, fake_col):
    statistic, p_value = ks_2samp(real_col, fake_col)
    equality = 'identical' if p_value > 0.01 else 'different'
    return {'col_name': col_name, 'statistic': statistic, 'p-value': p_value, 'equality': equality}

def kolmogorov_smirnov_df(real: pd.DataFrame, fake: pd.DataFrame, numerical_columns: List) -> List[Dict[str, Any]]:
    assert real.columns.tolist() == fake.columns.tolist(), f'Colums are not identical between `real` and `fake`. '
    real_iter = real[numerical_columns].iteritems()
    fake_iter = fake[numerical_columns].iteritems()
    distances = Parallel(n_jobs=-1)(
        delayed(kolmogorov_smirnov_test)
        (colname, real_col, fake_col) for (colname, real_col), (_, fake_col) in zip(real_iter, fake_iter))
    distances_df = pd.DataFrame(distances)
    return distances_df.set_index('col_name')


from scipy.special import comb

def independent_alleles(k, N):
    prob_N=0.0
    total_offspring=2**k

    for i in range(N, total_offspring):
        #calculating the success of at least N in among 2**k trials-->binomial distribution
        prob_N=prob_N+comb(total_offspring,i)*((0.25)**i)*((0.75)**(total_offspring-i))

    prob_N=round(prob_N,4)
    return prob_N

def main():
    with open('datasets/rosalind_lia.txt') as input_file:
        k, N = input_file.read().strip().split()

    prob_N=independent_alleles(int(k),int(N))

    print(prob_N)

    with open('solutions/rosalind_lia.txt', 'w') as output_file:
        output_file.write(str(prob_N))

if(__name__=='__main__'):
    main()