import sympy as sy
from sympy import *

m2_1 = sy.Matrix([ [ 1,  0,  0, -1, 0],
                 [-1,  0, -2,  0, 0],
                 [ 0, -1,  0,  0, -3],
                 [ 0,  0,  0, -2, 1],
                 [ 0, -3,  1,  0, 0] ])

print("determinant of M: ", m2_1.det())

a = sy.symbols('a')
m2_2 = sy.Matrix([ [ a,  5,  9],
                 [ a, -1,  4],
                 [ 9, -7,  a] ])

print("det A = ",m2_2.det())
print("a = ", solve(-6*a**2 - 35*a + 261, a))

m2_3 = sy.Matrix([ [-3,  5, -8],
                 [ 8, -5,  1],
                 [ 5,  1,  5] ])
cf = m2_3.cofactor_matrix()
print("cofactor matrix: ",cf)

def minor_matrix(m):
    r , c = m.shape
    new_m = []

    for i in range(r):
        for j in range(c):
            new_m.append(m.minor(i,j))
    return Matrix(r, c, new_m)

print("Minors: ",minor_matrix(m2_3))

m2_4 = sy.Matrix([ [-4,  9, -5],
                   [-9,  7,  5],
                   [ 9,  7, -6] ])

print(m2_4.det())
print(m2_4.inv())

m2_5 = sy.Matrix([ [-5, -5,   4],
                   [ 4,  5,  -5],
                   [ 1,  1,  -1] ])

print(m2_5.inv())

m2_5b = sy.Matrix([ [-2],
                    [ 4],
                    [ 5] ])

print("x is:", m2_5.inv() * m2_5b)

m2_6_A = sy.Matrix([ [ 4, -12, -37],
                     [-2,   7,  20],
                     [ 1,  -3,  -9] ])

m2_6_B = sy.Matrix([ [-4,  12,  25],
                     [ 2,  -7, -15],
                     [ 1,  -3,  -6] ])

m2_6_C = sy.Matrix([ [ 4, -12,  -37,  0,  0,   0],
                     [-2,   7,   20,  0,  0,   0],
                     [ 1,  -3,   -9,  0,  0,   0],
                     [ 0,   0,    0, -4, 12,  25],
                     [ 0,   0,    0,  2, -7, -15],
                     [ 0,   0,    0,  1, -3,  -6] ])

print("m2_6_A inv:", m2_6_A.inv())
print("m2_6_B inv:", m2_6_B.inv())
print("m2_6_C inv:", m2_6_C.inv())

m2_8_A = sy.Matrix([ [ 2, -4,  1],
                     [-1,  1, -1],
                     [ 1, -2,  0] ])

m2_8_B = sy.Matrix([ [3],
                     [0],
                     [3] ])

print("2_8 X is:", m2_8_A.inv() * m2_8_B)

m2_9_A = sy.Matrix([ [ 1, -2, -5],
                     [-4, -2,  4],
                     [ 3,  2, -5] ])

print("A+A.T: ",m2_9_A + m2_9_A.transpose())
print("A-A.T: ",m2_9_A - m2_9_A.transpose())

m2_10_A = sy.Matrix([ [ -4, -5, -8, -9] ])

m2_10_B = sy.Matrix([ [ 5],
                      [-5],
                      [-2],
                      [ 9] ])

print("B*A= ", m2_10_B*m2_10_A)

m2_11_A = sy.Matrix([ [ 6,  4, -3],
                      [-6, -9, -7],
                      [ 2, -6, -4] ])

m2_11_B = sy.Matrix([ [-3, -1, -1],
                      [ 6,  5, -1],
                      [ 4, -3,  0] ])

print("6A = ", 6*m2_11_A)
print("A-7B = ", m2_11_A-(7*m2_11_B))
print("-3A-3B = ", (-3*m2_11_A)-(3*m2_11_B))

m2_12_A = sy.Matrix([ [ 2,  4,  6],
                      [ 2,  4, -2] ])

m2_12_B = sy.Matrix([ [-3,  4,  7],
                      [-4,  1,  3] ])

print(" (-2B - A ) / 3 = ",((-2*m2_12_B)-m2_12_A)/3)

m2_16_A = sy.Matrix([ [-1,  3,  5],
                      [ 1,  8, -6],
                      [-6,  2, -6] ])
print("det(A): ", m2_16_A.det())

m2_18_B = sy.Matrix([ [ 1,  0,  1],
                      [ 2,  2, -1],
                      [ 2, -2, -1] ])
print("det(B^5): ", (m2_18_B**5).det())

a, b, c, d, e, f = sy.symbols('a, b, c, d, e, f')
m2_21_A = sy.Matrix([ [ a,  1,  d],
                      [ b,  1,  e],
                      [ c,  1,  f] ])

m2_21_B = sy.Matrix([ [ a,  1,  d],
                      [ b,  2,  e],
                      [ c,  3,  f] ])
print("det m2_21_A:", m2_21_A.det())
print("det m2_21_B:", m2_21_B.det())

x1, x2, z1, z2 = sy.symbols('x1, x2, z1, z2')
m2_25_A = sy.Matrix([ [ x1, x2],
                      [ z1, z2] ])
m2_25_B = sy.Matrix([ [ 9*x1 + 5*z1, 9*x2 + 5*z2],
                      [ 8*x1 + 4*z1, 8*x2 + 4*z2] ])
print("det(A): ", m2_25_A.det())
print("det(B): ", m2_25_B.det())

m2_27_A = sy.Matrix([ [-3,  7,  0,  1],
                      [ 8, -7,  0,  0],
                      [ 0, -8,  0,  0],
                      [ 3, -8,  6,  4] ])
print("det(A): ", m2_27_A.det())

m2_28_A = sy.Matrix([ [ 0, -4, -7,  0, 0],
                      [ 9,  4, -1,  0, 0],
                      [ 0, -3,  0,  0, 0],
                      [-3, -5, -1, -3, -2],
                      [-5, -7, -1,  0, -2] ])
print("det(A): ", m2_28_A.det())

m2_29_A = sy.Matrix([ [-4, 20,   3],
                      [ 3,  2,  -1],
                      [14,  2, -13] ])
print("det(A): ", m2_29_A.det())


import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
import sklearn.linear_model as skl
#from sklearn.linear_model import LinearRegression, Ridge, Lasso
def R2(y_data, y_model):
    return 1 - np.sum((y_data - y_model) ** 2) / np.sum((y_data - np.mean(y_data)) ** 2)
def MSE(y_data,y_model):
    n = np.size(y_model)
    return np.sum((y_data-y_model)**2)/n

# A seed just to ensure that the random numbers are the same for every run.
# Useful for eventual debugging.
np.random.seed(3155)

x = np.random.rand(100)
y = 2.0+5*x*x+0.1*np.random.randn(100)

# number of features p (here degree of polynomial
p = 3
#  The design matrix now as function of a given polynomial
X = np.zeros((len(x),p))
X[:,0] = 1.0
X[:,1] = x
X[:,2] = x*x
# We split the data in test and training data
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
scaler = StandardScaler()
scaler.fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)

# matrix inversion to find beta
OLSbeta = np.linalg.inv(X_train.T @ X_train) @ X_train.T @ y_train
print(OLSbeta)
# and then make the prediction
ytildeOLS = X_train @ OLSbeta
print("Training R2 for OLS")
print(R2(y_train,ytildeOLS))
print("Training MSE for OLS")
print(MSE(y_train,ytildeOLS))
ypredictOLS = X_test @ OLSbeta
print("Test R2 for OLS")
print(R2(y_test,ypredictOLS))
print("Test MSE OLS")
print(MSE(y_test,ypredictOLS))

# Repeat now for Ridge regression and various values of the regularization parameter
I = np.eye(p,p)
# Decide which values of lambda to use
nlambdas = 100
MSEPredictLasso = np.zeros(nlambdas)
MSEPredictRidge = np.zeros(nlambdas)
lambdas = np.logspace(-4, 0, nlambdas)
for i in range(nlambdas):
    lmb = lambdas[i]
    # add ridge
    clf_ridge = skl.Ridge(alpha=lmb).fit(X_train, y_train)
    clf_lasso = skl.Lasso(alpha=lmb).fit(X_train, y_train)
    yridge = clf_ridge.predict(X_test)
    ylasso = clf_lasso.predict(X_test)
    MSEPredictLasso[i] = MSE(y_test,ylasso)
    MSEPredictRidge[i] = MSE(y_test,yridge)
#then plot the results
plt.figure()
plt.plot(np.log10(lambdas), MSEPredictRidge, 'r--', label = 'MSE Ridge Test')
plt.plot(np.log10(lambdas), MSEPredictLasso, 'g--', label = 'MSE Lasso Test')
plt.xlabel('log10(lambda)')
plt.ylabel('MSE')
plt.legend()
plt.show()


import numpy as np

from numpy import array, diag, divide, exp, matrix, power, zeros
from numpy.linalg import inv, norm

class logistic_regression_IRLS(object):
    """
    Maximum likelihood approach for logistic regression.
    """
    def __init__(self):
        self.w = None

    def fit(self, X, y, w0=None, erf=None, maxit=None, display=None):
        self.__newton_raphson(X, y, w0, erf, maxit, display)

    def predict(self, X):
        y = X * self.w
        return array((y>0).astype(int).T)[0]

    def __newton_raphson(self, X, y, w0, erf, maxit, display):
        """
        Args:
            X:   nxd matrix.
            y:   nx1 column vector.
            w0:  dx1 column vector.
            erf: error function with two parameters.
                 erf = lambda old,new: ...
        """
        # Pre-process
        X   = matrix(X)
        n,d = X.shape
        y   = matrix(y).reshape(n, 1)
        if w0==None:
            w0 = zeros((d, 1))
        if erf==None:
            erf = lambda old,new: norm(old-new,2)<1e-6
        if maxit==None:
            maxit = 64
        if display==None:
            display = False
        w1 = zeros((d, 1))
        ex = zeros((n, 1))
        # Iterations
        for i in range(maxit):
            ex = exp(X*w0).T
            D  = diag(array(ex/power(1+ex,2))[0])
            w1 = w0 + inv(X.T*D*X)*X.T*(y-(ex/(1+ex)).T)
            if erf(w0, w1):
                if display:
                    print("Convergence @ the %d iteration(s)."%i)
                break
            w0 = w1
        self.w = w1

"""
from sklearn.linear_model import LogisticRegression as Regression

X = np.random.rand(20, 2)-0.5
y = (np.sum(X,1)>0).astype(int)

model_1 = logistic_regression_IRLS()
model_1.fit(X, y)
y_1 = model_1.predict(X)

model_2 = Regression(solver="lbfgs")
model_2.fit(X, y)
y_2 = model_2.predict(X)

dy_1 = norm(y_1-y, 2)
dy_2 = norm(y_2-y, 2)

print(dy_1, dy_2)
"""


import numpy as np
import matplotlib.pyplot as plt
m=10
d_teta1=0.01
teta_1_min=-1
teta_1_max=1

# here we actually have a graph with h_teta=teta1*x
X = np.arange(m)
np.random.seed(42) #use to get the same results every run
rnd = np.random.rand(m)
Y = np.arange(m)*0.5 + rnd # actually we have h = teta1*x where teta1 is actually 0.5

#estimated y -> H
Teta1 = np.arange(teta_1_min,teta_1_max,d_teta1)
J=[]

# mathematically J is (1/2*m)* eT * e
# can i do it without a loop ???
for teta1 in Teta1:
    H = X * teta1
    E = Y - H
    cost = np.dot(E,E)/(2*m)
    J.append(cost)

# compute teta1 for min(J)
min_j_index = np.argmin(J)
teta1 = Teta1[min_j_index]
print("min(J) : {} , tata1 @ min(J) : {}".format(J[min_j_index] , teta1))

h_vec = X * Teta1[min_j_index] # computed linear estimation

# plots
plt.plot(X, Y,'o')
plt.title('data set')
plt.xlabel("X")
plt.ylabel("Y")
plt.show()

plt.plot(Teta1,J)
plt.title('loss function J : sum of (Y-H)^2')
plt.xlabel("teta1")
plt.ylabel("J")
plt.show()

plt.plot(X, h_vec,X, Y,'o')
plt.title('linear estimation H = Teta1 * X.    teta1 is chosen as min(J)')
plt.xlabel("X")
plt.ylabel("H , Y")
plt.show()


import numpy as np
from common.functions import softmax

def cross_entropy_error(y, t):
    """
    y : ソフトマックス関数の出力
        y.shape=(k,)またはy.shape=(N,k)
    t : 正解ラベル(ワンホット表現)
        t.shape=(k,)またはt.shape=(N,k)
    """
    if y.ndim==1:
        t = t.reshape(1,-1)
        y = y.reshape(1,-1)

    batch_size = y.shape[0]
    delta = 1e-7
    return -np.sum(t*np.log(y + delta))/ batch_size

class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None
        self.y = None # ソフトマックス関数の出力を格納するインスタンス変数
        self.t = None # 正解ラベルを格納するインスタンス変数

    def forward(self, x, t):
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t)
        return self.loss

    def backward(self):
        batch_size = self.t.shape[0]
        dx = (self.y-self.t)/batch_size
        return dx

if __name__=="__main__":
    x = np.arange(2*3).reshape(2, 3)
    print("x\n", x)

    t = np.arange(2).reshape(-1, 1)
    print("t\n", t)

    sl = SoftmaxWithLoss()

    loss = sl.forward(x,t)
    print("loss\n", loss)

    dx = sl.backward()
    print("dx\n", dx)


# coding: utf-8

# In[1]:

# Importing numpy and matplotlib
from pylab import *
from scipy.integrate import quad

# In[2]:

rcParams['figure.figsize'] = 12,9
rcParams['font.size'] = 18
rcParams['text.usetex'] = True

# In[3]:

def periodic(a, b):
    interval = b - a
    return lambda f: lambda x: f((x - a) % interval + a)

# In[7]:

def coscos(x):
    return cos(cos(x))
def per_e(x):
    return exp(np.remainder(x,2*pi))

# In[8]:

x = linspace(-2*pi,4*pi,400)
fig1 = figure()
ax = fig1.add_subplot(1,1,1)
ax.set_yscale('log')
grid(True)
plot(x,exp(x))
plot(x,per_e(x))
title("$e^x$ and its periodically extended version")
ylabel(r"$y$ (log)", fontsize = 26)
xlabel(r"$x$ (linear)")
legend([r"$e^x$",r"periodic version of $e^x$"], loc=0)
show()

# In[27]:

fig2 = figure()
grid(True)
plot(x,cos(cos(x)))
plot(x,coscos(x))
title("$\cos(\cos(x))$ and its periodically extended version")
ylabel(r"$y$ (linear)", fontsize = 26)
xlabel(r"$x$ (linear)")
legend([r"$\cos(\cos(x))$",r"periodic version of $\cos(\cos(x))$"], loc=1)
ylim(0.3,1.4)
show()

# In[11]:

def quad_fourier(f,n):
    """
    Find the n even and odd fourier coefficients of f, and the DC
    value, using quad integration.
    Assumes a period from 0 to 2pi.
    """

    # functions to integrate
    u = lambda x,k : f(x)*cos(k*x)
    v = lambda x,k : f(x)*sin(k*x)

    # DC coefficient
    a0 = 1/(2*pi)*quad(f,0,2*pi)[0]

    # find coefficients by integrating
    ret = [a0]
    for k in arange(n)+1:
        ak = 1/pi*quad(u,0,2*pi,args=(k))[0]
        bk = 1/pi*quad(v,0,2*pi,args=(k))[0]
        ret.append(ak)
        ret.append(bk)

    return array(ret)

# In[12]:

ef = quad_fourier(per_e,25)
cf = quad_fourier(coscos,25)

# In[13]:

def plotCoeffs(coeffs, name="Coefficients"):
    """
    Helper function to scatter the given coefficients on a
    semilog and loglog scale
    """

    fig0 = figure()
    ax = fig0.add_subplot(1,1,1)
    ax.set_yscale('log')
    grid(True)
    title("Fourier coefficients of {} (semilog)".format(name))
    ylabel(r"Magnitude of coefficients", fontsize = 26)
    xlabel(r"Index")
    scatter(arange(len(coeffs))+1,abs(coeffs),color='red',s=50)
    show()

    fig1 = figure()
    ax = fig1.add_subplot(1,1,1)
    ax.set_yscale('log')
    ax.set_xscale('log')
    grid(True)
    title("Fourier coefficients of {} (log-log)".format(name))
    ylabel(r"Magnitude of coefficients", fontsize = 26)
    xlabel(r"Index")
    scatter(arange(len(coeffs))+1,abs(coeffs),color='red',s=50)

    return fig0,fig1

# In[14]:

fig3,fig4 = plotCoeffs(ef,r"$e^x$")

# In[15]:

fig5,fig6 = plotCoeffs(cf,"$\cos(\cos(x))$")

# In[16]:

def lstsq_fourier(f,n,steps=400):
    """
    Find the n even and odd fourier coefficients of f, and the DC
    value, using least squares estimation. Defaults to 400 steps.
    Assumes a period from 0 to 2pi.

    Also returns the least squares matrix
    """

    x=linspace(0,2*pi,steps+1)
    x=x[:-1] # drop last term to have a proper periodic integral
    b=f(x) # f has been written to take a vector
    A=zeros((steps,2*n+1)) # allocate space for A
    A[:,0]=1 # col 1 is all ones
    for k in range(1,n+1):
        A[:,2*k-1]=cos(k*x) # cos(kx) column
        A[:,2*k]=sin(k*x) # sin(kx) column

    return lstsq(A,b)[0], A

# In[17]:

s = 400 # number of steps for least square estimation
elc, Aexp = lstsq_fourier(per_e,25,s)
ax = fig3.axes[0]
ax.scatter(arange(len(elc))+1,abs(elc),s=50,color='green')
ax.legend(["Using quad integration", "Using least squares"], loc=3)
fig3

# In[18]:

ax = fig4.axes[0]
ax.scatter(arange(len(elc))+1,abs(elc),color='green',s=50)
ax.legend(["Using quad integration", "Using least squares"],loc=3)
fig4

# In[19]:

clc, Acos = lstsq_fourier(coscos,25,s)
ax = fig5.axes[0]
ax.scatter(arange(len(clc))+1,abs(clc),color='green',s=50)
ax.legend(["Using quad integration", "Using least squares"],loc=1)
fig5

# In[20]:

ax = fig6.axes[0]
ax.scatter(arange(len(clc))+1,abs(clc),color='green',s=50)
ax.legend(["Using quad integration", "Using least squares"],loc=6)
fig6

# In[21]:

print(max(abs(ef-elc)))

# In[22]:

print(max(abs(cf-clc)))

# In[23]:

I = linspace(0,2*pi,s)

# In[24]:

ax = fig1.axes[0]
ax.scatter(I,dot(Aexp,ef),color = 'green')
ax.scatter(I,dot(Aexp,elc),color = 'black',marker='+')
ax.set_ylim(1e-3,1e6)
ax.legend([r"$e^x$",r"periodic version of $e^x$",
           "quad estimated","Least squares estimated"], loc = 4)
fig1

# In[25]:

figure()
grid(True)
xlim(-2,7)
plot(x,per_e(x))
plot(I,dot(Aexp,ef),linewidth=3)
title("$e^x$ and its fourier approximation")
ylabel(r"$y$ (linear)", fontsize = 26)
xlabel(r"$x$ (linear)")
legend([r"Periodic version of $e^x$",r"Fourier estimate of $e^x$"], loc=0)
show()

# In[28]:

ax = fig2.axes[0]
ax.scatter(I,dot(Acos,cf),color = 'green')
ax.scatter(I,dot(Acos,clc),color = 'black')
ax.legend([r"$\cos(\cos(x))$",r"periodic version of $\cos(\cos(x))$"
           ,"quad estimated","Least squares estimated"], loc = 0)
fig2


#!/usr/bin/env python

"""Vandermonde matrix example

Demonstrates matrix computations using the Vandermonde matrix.
  * http://en.wikipedia.org/wiki/Vandermonde_matrix
"""

from sympy import Matrix, pprint, Rational, sqrt, symbols, Symbol, zeros

def symbol_gen(sym_str):
    """Symbol generator

    Generates sym_str_n where n is the number of times the generator
    has been called.
    """
    n = 0
    while True:
        yield Symbol("%s_%d" % (sym_str, n))
        n += 1

def comb_w_rep(n, k):
    """Combinations with repetition

    Returns the list of k combinations with repetition from n objects.
    """
    if k == 0:
        return [[]]
    combs = [[i] for i in range(n)]
    for i in range(k - 1):
        curr = []
        for p in combs:
            for m in range(p[-1], n):
                curr.append(p + [m])
        combs = curr
    return combs

def vandermonde(order, dim=1, syms='a b c d'):
    """Comptutes a Vandermonde matrix of given order and dimension.

    Define syms to give beginning strings for temporary variables.

    Returns the Matrix, the temporary variables, and the terms for the polynomials
    """
    syms = syms.split()
    if len(syms) < dim:
        new_syms = []
        for i in range(dim - len(syms)):
            new_syms.append(syms[i%len(syms)] + str(i/len(syms)))
        syms.extend(new_syms)
    terms = []
    for i in range(order + 1):
        terms.extend(comb_w_rep(dim, i))
    rank = len(terms)
    V = zeros(rank)
    generators = [symbol_gen(syms[i]) for i in range(dim)]
    all_syms = []
    for i in range(rank):
        row_syms = [g.next() for g in generators]
        all_syms.append(row_syms)
        for j,term in enumerate(terms):
            v_entry = 1
            for k in term:
                v_entry *= row_syms[k]
            V[i*rank + j] = v_entry
    return V, all_syms, terms

def gen_poly(points, order, syms):
    """Generates a polynomial using a Vandermonde system"""
    num_pts = len(points)
    if num_pts == 0:
        raise ValueError("Must provide points")
    dim = len(points[0]) - 1
    if dim > len(syms):
        raise ValueError("Must provide at lease %d symbols for the polynomial" % dim)
    V, tmp_syms, terms = vandermonde(order, dim)
    if num_pts < V.shape[0]:
        raise ValueError(
            "Must provide %d points for order %d, dimension "\
            "%d polynomial, given %d points" % \
            (V.shape[0], order, dim, num_pts))
    elif num_pts > V.shape[0]:
        print "gen_poly given %d points but only requires %d, "\
            "continuing using the first %d points" % \
            (num_pts, V.shape[0], V.shape[0])
        num_pts = V.shape[0]

    subs_dict = {}
    for j in range(dim):
        for i in range(num_pts):
            subs_dict[tmp_syms[i][j]] = points[i][j]
    V_pts = V.subs(subs_dict)
    V_inv = V_pts.inv()

    coeffs = V_inv.multiply(Matrix([points[i][-1] for i in xrange(num_pts)]))

    f = 0
    for j,term in enumerate(terms):
        t = 1
        for k in term:
            t *= syms[k]
        f += coeffs[j]*t
    return f

def main():
    order = 2
    V, tmp_syms, _ = vandermonde(order)
    print "Vandermonde matrix of order 2 in 1 dimension"
    pprint(V)

    print '-'*79
    print "Computing the determinate and comparing to \sum_{0<i<j<=3}(a_j - a_i)"

    det_sum = 1
    for j in range(order + 1):
        for i in range(j):
            det_sum *= (tmp_syms[j][0] - tmp_syms[i][0])

    print """
    det(V) = %(det)s
    \sum   = %(sum)s
           = %(sum_expand)s
    """ % { "det": V.det(),
             "sum": det_sum,
             "sum_expand": det_sum.expand(),
          }

    print '-'*79
    print "Polynomial fitting with a Vandermonde Matrix:"
    x,y,z = symbols('x,y,z')

    points = [(0,3), (1,2), (2,3)]
    print """
    Quadratic function, represented by 3 points:
       points = %(pts)s
       f = %(f)s
    """ % { "pts" : points,
            "f" : gen_poly(points, 2, [x]),
          }

    points = [(0, 1, 1), (1, 0, 0), (1, 1, 0), (Rational(1, 2), 0, 0),
              (0, Rational(1, 2), 0), (Rational(1, 2), Rational(1, 2), 0)]
    print """
    2D Quadratic function, represented by 6 points:
       points = %(pts)s
       f = %(f)s
    """ % { "pts" : points,
            "f" : gen_poly(points, 2, [x, y]),
          }

    points = [(0, 1, 1, 1), (1, 1, 0, 0), (1, 0, 1, 0), (1, 1, 1, 1)]
    print """
    3D linear function, represented by 4 points:
       points = %(pts)s
       f = %(f)s
    """ % { "pts" : points,
            "f" : gen_poly(points, 1, [x, y, z]),
          }

if __name__ == "__main__":
    main()


## Leetcode problem 207: Course schedule.
import numpy as np
import algorith.clr_book.ch22_elemtary_graph.graph as gr

## numCourses=2; prerequisites=[[1,0]]; return: True
## numCourses=2; prerequisites=[[1,0],[0,1]]; return: False
## numCourses=4; prerequisites=[[0,1],[0,2],[1,3],[2,3]]; return: False

## See here: https://walkccc.github.io/CLRS/Chap22/22.3/
def find_cycle_w_dfs_it(g):
    for u in g.vertices:
        if u.color=="white":
            if find_cycle_w_dfs(g,u):
                return True
    return False

def find_cycle_w_dfs(g,u):
    u.color="grey"
    st=[u]
    while len(st)>0:
        u=st[len(st)-1]
        remains=False
        for v in g.adj[u]:
            if v.color=="grey":
                return True
            elif v.color=="white":
                remains=True
                v.color="grey"
                st.append(v)
                break#Exit the for loop
        if not remains:
            st.pop()
            u.color="black"
    return False

def find_cycle_w_dfs_rec(g):
    for ver in g.vertices:
        if ver.color=="white":
            try:
                find_cycles_dfs_visit(g,ver)
            except:
                return True
    return False

def find_cycles_dfs_visit(g,u):
    u.color="grey"
    if u in g.adj.keys():
        for v in g.adj[u]:
            if v.color=="white":
                find_cycles_dfs_visit(g,v)
            elif v.color=="grey":
                raise Exception("cycle detected!")
    u.color="black"

#########################################
## Tests

def tst():
    prerequisites=[[1,0],[2,0],[3,1],[3,2]]
    numCourses=4
    g=gr.Graph().init_from_edge_list(numCourses,prerequisites)
    print(find_cycle_w_dfs_rec(g)==False)
    prerequisites=[[0,1],[1,0]]
    numCourses=2
    g=gr.Graph().init_from_edge_list(numCourses,prerequisites)
    print(find_cycle_w_dfs_rec(g)==True)
    prerequisites=[[1,0],[0,3],[0,2],[3,2],[2,5],[4,5],[5,6],[2,4]]
    numCourses=7
    g=gr.Graph().init_from_edge_list(numCourses,prerequisites)
    print(find_cycle_w_dfs_it(g))
    #find_cycle_w_dfs_rec(g)


import numpy as np
from scipy.optimize import minimize

from utils import computer_cost, sigmoid, load_data, plot_data

epsilon = 1e-5

def cost_function(theta, X, y):
    m = y.size
    h = sigmoid.sigmoid(X.dot(theta))
    J = -1 * (1 / m) * (np.log(h + epsilon).T.dot(y) + np.log(1 - h + epsilon).T.dot(1 - y))
    return J[0]

def gradient(theta, X, y):
    m = y.size
    h = sigmoid.sigmoid(X.dot(theta.reshape(-1, 1)))
    grad = (1 / m) * X.T.dot(h - y)
    return grad.flatten()

def predict(score1, score2, theta):
    return sigmoid.sigmoid(np.array([1, score1, score2]).dot(theta.reshape(3, -1)).sum())

def main():
    X, y, origin_X = load_data.load_and_process('data1.txt')
    initial_theta = np.zeros(X.shape[1])
    res = minimize(cost_function, initial_theta, args=(X, y), method=None, jac=gradient, options={'maxiter': 400})
    print(res)

    theta = res.x

    data = load_data.load('data1.txt')

    pos_values = data[(data[:, 2] == 1)]
    neg_values = data[(data[:, 2] == 0)]

    plot_data.plot(pos_values[:, 0], pos_values[:, 1], 'score1', 'score2',
                   {
                       'fmt': 'bx',
                       'markersize': 5
                   })

    plot_data.plot(neg_values[:, 0], neg_values[:, 1], 'score1', 'score2',
                   {
                       'fmt': 'yo',
                       'markersize': 5,
                       'show': False
                   })

    score1 = np.linspace(25, 100)
    score2 = []
    for item in score1:
        score2.append(((0.5 - theta[0]) - theta[1] * item) / theta[2])
    score2 = np.array(score2)

    plot_data.plot(score1, score2, 'score1', 'score2',
                   {
                       'fmt': 'r-',
                       'label': 'minimize',
                       'show': False
                   })

    theta_from_cal = np.array([-4.81180027, 0.04528064, 0.03819149]) # 0.001 10 0000
    theta_from_cal_2 = np.array([-15.39517866, 0.12825989, 0.12247929]) # 0.001 100 0000
    theta_from_cal_3 = np.array([-22.21628108, 0.18268725, 0.17763448]) # 0.003 100 0000
    score3 = []
    for item in score1:
        score3.append(((0.5 - theta_from_cal_3[0]) - theta_from_cal_3[1] * item) / theta_from_cal_3[2])
    score3 = np.array(score3)
    plot_data.plot(score1, score3, 'score1', 'score2',
                   {
                       'fmt': 'g-',
                       'label': 'gredient-descent',
                       'legend_loc': 'upper right',
                       'show': True
                   })

    print(predict(45, 85, theta_from_cal_3))

if __name__ == '__main__':
    main()


"""Entropy functions"""
import numpy as np
from numba import jit
from math import factorial, log
from sklearn.neighbors import KDTree
from scipy.signal import periodogram, welch

from .utils import _embed

all = ['perm_entropy', 'spectral_entropy', 'svd_entropy', 'app_entropy',
       'sample_entropy', 'lziv_complexity', 'num_zerocross', 'hjorth_params']

def perm_entropy(x, order=3, delay=1, normalize=False):
    """Permutation Entropy.

    Parameters
    ----------
    x : list or np.array
        One-dimensional time series of shape (n_times)
    order : int
        Order of permutation entropy. Default is 3.
    delay : int
        Time delay (lag). Default is 1.
    normalize : bool
        If True, divide by log2(order!) to normalize the entropy between 0
        and 1. Otherwise, return the permutation entropy in bit.

    Returns
    -------
    pe : float
        Permutation Entropy.

    Notes
    -----
    The permutation entropy is a complexity measure for time-series first
    introduced by Bandt and Pompe in 2002.

    The permutation entropy of a signal :math:`x` is defined as:

    .. math:: H = -\\sum p(\\pi)\\log_2(\\pi)

    where the sum runs over all :math:`n!` permutations :math:`\\pi` of order
    :math:`n`. This is the information contained in comparing :math:`n`
    consecutive values of the time series. It is clear that
    :math:`0 ≤ H (n) ≤ \\log_2(n!)` where the lower bound is attained for an
    increasing or decreasing sequence of values, and the upper bound for a
    completely random system where all :math:`n!` possible permutations appear
    with the same probability.

    The embedded matrix :math:`Y` is created by:

    .. math::
        y(i)=[x_i,x_{i+\\text{delay}}, ...,x_{i+(\\text{order}-1) *
        \\text{delay}}]

    .. math:: Y=[y(1),y(2),...,y(N-(\\text{order}-1))*\\text{delay})]^T

    References
    ----------
    Bandt, Christoph, and Bernd Pompe. "Permutation entropy: a
    natural complexity measure for time series." Physical review letters
    88.17 (2002): 174102.

    Examples
    --------
    Permutation entropy with order 2

    >>> import numpy as np
    >>> import entropy as ent
    >>> import stochastic.processes.noise as sn
    >>> x = [4, 7, 9, 10, 6, 11, 3]
    >>> # Return a value in bit between 0 and log2(factorial(order))
    >>> print(f"{ent.perm_entropy(x, order=2):.4f}")
    0.9183

    Normalized permutation entropy with order 3

    >>> # Return a value comprised between 0 and 1.
    >>> print(f"{ent.perm_entropy(x, normalize=True):.4f}")
    0.5888

    Fractional Gaussian noise with H = 0.5

    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.5, rng=rng).sample(10000)
    >>> print(f"{ent.perm_entropy(x, normalize=True):.4f}")
    0.9998

    Fractional Gaussian noise with H = 0.9

    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.9, rng=rng).sample(10000)
    >>> print(f"{ent.perm_entropy(x, normalize=True):.4f}")
    0.9926

    Fractional Gaussian noise with H = 0.1

    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.1, rng=rng).sample(10000)
    >>> print(f"{ent.perm_entropy(x, normalize=True):.4f}")
    0.9959

    Random

    >>> rng = np.random.default_rng(seed=42)
    >>> print(f"{ent.perm_entropy(rng.random(1000), normalize=True):.4f}")
    0.9997

    Pure sine wave

    >>> x = np.sin(2 * np.pi * 1 * np.arange(3000) / 100)
    >>> print(f"{ent.perm_entropy(x, normalize=True):.4f}")
    0.4463

    Linearly-increasing time-series

    >>> x = np.arange(1000)
    >>> print(f"{ent.perm_entropy(x, normalize=True):.4f}")
    -0.0000
    """
    x = np.array(x)
    ran_order = range(order)
    hashmult = np.power(order, ran_order)
    # Embed x and sort the order of permutations
    sorted_idx = _embed(x, order=order, delay=delay).argsort(kind='quicksort')
    # Associate unique integer to each permutations
    hashval = (np.multiply(sorted_idx, hashmult)).sum(1)
    # Return the counts
    _, c = np.unique(hashval, return_counts=True)
    # Use np.true_divide for Python 2 compatibility
    p = np.true_divide(c, c.sum())
    pe = -np.multiply(p, np.log2(p)).sum()
    if normalize:
        pe /= np.log2(factorial(order))
    return pe

def spectral_entropy(x, sf, method='fft', nperseg=None, normalize=False,
                     axis=-1):
    """Spectral Entropy.

    Parameters
    ----------
    x : list or np.array
        1D or N-D data.
    sf : float
        Sampling frequency, in Hz.
    method : str
        Spectral estimation method:

        * ``'fft'`` : Fourier Transform (:py:func:`scipy.signal.periodogram`)
        * ``'welch'`` : Welch periodogram (:py:func:`scipy.signal.welch`)
    nperseg : int or None
        Length of each FFT segment for Welch method.
        If None (default), uses scipy default of 256 samples.
    normalize : bool
        If True, divide by log2(psd.size) to normalize the spectral entropy
        between 0 and 1. Otherwise, return the spectral entropy in bit.
    axis : int
        The axis along which the entropy is calculated. Default is -1 (last).

    Returns
    -------
    se : float
        Spectral Entropy

    Notes
    -----
    Spectral Entropy is defined to be the Shannon entropy of the power
    spectral density (PSD) of the data:

    .. math:: H(x, sf) =  -\\sum_{f=0}^{f_s/2} P(f) \\log_2[P(f)]

    Where :math:`P` is the normalised PSD, and :math:`f_s` is the sampling
    frequency.

    References
    ----------
    - Inouye, T. et al. (1991). Quantification of EEG irregularity by
      use of the entropy of the power spectrum. Electroencephalography
      and clinical neurophysiology, 79(3), 204-210.

    - https://en.wikipedia.org/wiki/Spectral_density

    - https://en.wikipedia.org/wiki/Welch%27s_method

    Examples
    --------
    Spectral entropy of a pure sine using FFT

    >>> import numpy as np
    >>> import entropy as ent
    >>> sf, f, dur = 100, 1, 4
    >>> N = sf * dur # Total number of discrete samples
    >>> t = np.arange(N) / sf # Time vector
    >>> x = np.sin(2 * np.pi * f * t)
    >>> np.round(ent.spectral_entropy(x, sf, method='fft'), 2)
    0.0

    Spectral entropy of a random signal using Welch's method

    >>> np.random.seed(42)
    >>> x = np.random.rand(3000)
    >>> ent.spectral_entropy(x, sf=100, method='welch')
    6.980045662371389

    Normalized spectral entropy

    >>> ent.spectral_entropy(x, sf=100, method='welch', normalize=True)
    0.9955526198316071

    Normalized spectral entropy of 2D data

    >>> np.random.seed(42)
    >>> x = np.random.normal(size=(4, 3000))
    >>> np.round(ent.spectral_entropy(x, sf=100, normalize=True), 4)
    array([0.9464, 0.9428, 0.9431, 0.9417])

    Fractional Gaussian noise with H = 0.5

    >>> import stochastic.processes.noise as sn
    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.5, rng=rng).sample(10000)
    >>> print(f"{ent.spectral_entropy(x, sf=100, normalize=True):.4f}")
    0.9505

    Fractional Gaussian noise with H = 0.9

    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.9, rng=rng).sample(10000)
    >>> print(f"{ent.spectral_entropy(x, sf=100, normalize=True):.4f}")
    0.8477

    Fractional Gaussian noise with H = 0.1

    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.1, rng=rng).sample(10000)
    >>> print(f"{ent.spectral_entropy(x, sf=100, normalize=True):.4f}")
    0.9248
    """
    x = np.asarray(x)
    # Compute and normalize power spectrum
    if method == 'fft':
        _, psd = periodogram(x, sf, axis=axis)
    elif method == 'welch':
        _, psd = welch(x, sf, nperseg=nperseg, axis=axis)
    psd_norm = psd / psd.sum(axis=axis, keepdims=True)
    se = -(psd_norm * np.log2(psd_norm)).sum(axis=axis)
    if normalize:
        se /= np.log2(psd_norm.shape[axis])
    return se

def svd_entropy(x, order=3, delay=1, normalize=False):
    """Singular Value Decomposition entropy.

    Parameters
    ----------
    x : list or np.array
        One-dimensional time series of shape (n_times)
    order : int
        Order of SVD entropy (= length of the embedding dimension).
        Default is 3.
    delay : int
        Time delay (lag). Default is 1.
    normalize : bool
        If True, divide by log2(order!) to normalize the entropy between 0
        and 1. Otherwise, return the permutation entropy in bit.

    Returns
    -------
    svd_e : float
        SVD Entropy

    Notes
    -----
    SVD entropy is an indicator of the number of eigenvectors that are needed
    for an adequate explanation of the data set. In other words, it measures
    the dimensionality of the data.

    The SVD entropy of a signal :math:`x` is defined as:

    .. math::
        H = -\\sum_{i=1}^{M} \\overline{\\sigma}_i log_2(\\overline{\\sigma}_i)

    where :math:`M` is the number of singular values of the embedded matrix
    :math:`Y` and :math:`\\sigma_1, \\sigma_2, ..., \\sigma_M` are the
    normalized singular values of :math:`Y`.

    The embedded matrix :math:`Y` is created by:

    .. math::
        y(i)=[x_i,x_{i+\\text{delay}}, ...,x_{i+(\\text{order}-1) *
        \\text{delay}}]

    .. math:: Y=[y(1),y(2),...,y(N-(\\text{order}-1))*\\text{delay})]^T

    Examples
    --------
    SVD entropy with order 2

    >>> import numpy as np
    >>> import entropy as ent
    >>> import stochastic.processes.noise as sn
    >>> x = [4, 7, 9, 10, 6, 11, 3]
    >>> # Return a value in bit between 0 and log2(factorial(order))
    >>> print(ent.svd_entropy(x, order=2))
    0.7618909465130066

    Normalized SVD entropy with order 3

    >>> x = [4, 7, 9, 10, 6, 11, 3]
    >>> # Return a value comprised between 0 and 1.
    >>> print(ent.svd_entropy(x, order=3, normalize=True))
    0.6870083043946692

    Fractional Gaussian noise with H = 0.5

    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.5, rng=rng).sample(10000)
    >>> print(f"{ent.svd_entropy(x, normalize=True):.4f}")
    1.0000

    Fractional Gaussian noise with H = 0.9

    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.9, rng=rng).sample(10000)
    >>> print(f"{ent.svd_entropy(x, normalize=True):.4f}")
    0.9080

    Fractional Gaussian noise with H = 0.1

    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.1, rng=rng).sample(10000)
    >>> print(f"{ent.svd_entropy(x, normalize=True):.4f}")
    0.9637

    Random

    >>> rng = np.random.default_rng(seed=42)
    >>> print(f"{ent.svd_entropy(rng.random(1000), normalize=True):.4f}")
    0.8527

    Pure sine wave

    >>> x = np.sin(2 * np.pi * 1 * np.arange(3000) / 100)
    >>> print(f"{ent.svd_entropy(x, normalize=True):.4f}")
    0.1775

    Linearly-increasing time-series

    >>> x = np.arange(1000)
    >>> print(f"{ent.svd_entropy(x, normalize=True):.4f}")
    0.0053
    """
    x = np.array(x)
    mat = _embed(x, order=order, delay=delay)
    W = np.linalg.svd(mat, compute_uv=False)
    # Normalize the singular values
    W /= sum(W)
    svd_e = -np.multiply(W, np.log2(W)).sum()
    if normalize:
        svd_e /= np.log2(order)
    return svd_e

def _app_samp_entropy(x, order, metric='chebyshev', approximate=True):
    """Utility function for `app_entropy`` and `sample_entropy`.
    """
    _all_metrics = KDTree.valid_metrics
    if metric not in _all_metrics:
        raise ValueError('The given metric (%s) is not valid. The valid '
                         'metric names are: %s' % (metric, _all_metrics))
    phi = np.zeros(2)
    r = 0.2 * np.std(x, ddof=0)

    # compute phi(order, r)
    _emb_data1 = _embed(x, order, 1)
    if approximate:
        emb_data1 = _emb_data1
    else:
        emb_data1 = _emb_data1[:-1]
    count1 = KDTree(emb_data1, metric=metric).query_radius(emb_data1, r,
                                                           count_only=True
                                                           ).astype(np.float64)
    # compute phi(order + 1, r)
    emb_data2 = _embed(x, order + 1, 1)
    count2 = KDTree(emb_data2, metric=metric).query_radius(emb_data2, r,
                                                           count_only=True
                                                           ).astype(np.float64)
    if approximate:
        phi[0] = np.mean(np.log(count1 / emb_data1.shape[0]))
        phi[1] = np.mean(np.log(count2 / emb_data2.shape[0]))
    else:
        phi[0] = np.mean((count1 - 1) / (emb_data1.shape[0] - 1))
        phi[1] = np.mean((count2 - 1) / (emb_data2.shape[0] - 1))
    return phi

@jit('f8(f8[:], i4, f8)', nopython=True)
def _numba_sampen(x, order, r):
    """
    Fast evaluation of the sample entropy using Numba.
    """
    n = x.size
    n1 = n - 1
    order += 1
    order_dbld = 2 * order

    # Define threshold
    # r *= x.std()

    # initialize the lists
    run = [0] * n
    run1 = run[:]
    r1 = [0] * (n * order_dbld)
    a = [0] * order
    b = a[:]
    p = a[:]

    for i in range(n1):
        nj = n1 - i

        for jj in range(nj):
            j = jj + i + 1
            if abs(x[j] - x[i]) < r:
                run[jj] = run1[jj] + 1
                m1 = order if order < run[jj] else run[jj]
                for m in range(m1):
                    a[m] += 1
                    if j < n1:
                        b[m] += 1
            else:
                run[jj] = 0
        for j in range(order_dbld):
            run1[j] = run[j]
            r1[i + n * j] = run[j]
        if nj > order_dbld - 1:
            for j in range(order_dbld, nj):
                run1[j] = run[j]

    m = order - 1

    while m > 0:
        b[m] = b[m - 1]
        m -= 1

    b[0] = n * n1 / 2
    a = np.array([float(aa) for aa in a])
    b = np.array([float(bb) for bb in b])
    p = np.true_divide(a, b)
    return -log(p[-1])

def app_entropy(x, order=2, metric='chebyshev'):
    """Approximate Entropy.

    Parameters
    ----------
    x : list or np.array
        One-dimensional time series of shape (n_times).
    order : int
        Embedding dimension. Default is 2.
    metric : str
        Name of the distance metric function used with
        :py:class:`sklearn.neighbors.KDTree`. Default is to use the
        `Chebyshev <https://en.wikipedia.org/wiki/Chebyshev_distance>`_
        distance.

    Returns
    -------
    ae : float
        Approximate Entropy.

    Notes
    -----
    Approximate entropy is a technique used to quantify the amount of
    regularity and the unpredictability of fluctuations over time-series data.
    Smaller values indicates that the data is more regular and predictable.

    The tolerance value (:math:`r`) is set to :math:`0.2 * \\text{std}(x)`.

    Code adapted from the `mne-features <https://mne.tools/mne-features/>`_
    package by Jean-Baptiste Schiratti and Alexandre Gramfort.

    References
    ----------
    Richman, J. S. et al. (2000). Physiological time-series analysis
    using approximate entropy and sample entropy. American Journal of
    Physiology-Heart and Circulatory Physiology, 278(6), H2039-H2049.

    https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.DistanceMetric.html

    Examples
    --------
    Fractional Gaussian noise with H = 0.5

    >>> import numpy as np
    >>> import entropy as ent
    >>> import stochastic.processes.noise as sn
    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.5, rng=rng).sample(10000)
    >>> print(f"{ent.app_entropy(x, order=2):.4f}")
    2.1958

    Same with order = 3 and metric = 'euclidean'

    >>> print(f"{ent.app_entropy(x, order=3, metric='euclidean'):.4f}")
    1.5120

    Fractional Gaussian noise with H = 0.9

    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.9, rng=rng).sample(10000)
    >>> print(f"{ent.app_entropy(x):.4f}")
    1.9681

    Fractional Gaussian noise with H = 0.1

    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.1, rng=rng).sample(10000)
    >>> print(f"{ent.app_entropy(x):.4f}")
    2.0906

    Random

    >>> rng = np.random.default_rng(seed=42)
    >>> print(f"{ent.app_entropy(rng.random(1000)):.4f}")
    1.8177

    Pure sine wave

    >>> x = np.sin(2 * np.pi * 1 * np.arange(3000) / 100)
    >>> print(f"{ent.app_entropy(x):.4f}")
    0.2009

    Linearly-increasing time-series

    >>> x = np.arange(1000)
    >>> print(f"{ent.app_entropy(x):.4f}")
    -0.0010
    """
    phi = _app_samp_entropy(x, order=order, metric=metric, approximate=True)
    return np.subtract(phi[0], phi[1])

def sample_entropy(x, order=2, metric='chebyshev'):
    """Sample Entropy.

    Parameters
    ----------
    x : list or np.array
        One-dimensional time series of shape (n_times).
    order : int
        Embedding dimension. Default is 2.
    metric : str
        Name of the distance metric function used with
        :py:class:`sklearn.neighbors.KDTree`. Default is to use the
        `Chebyshev <https://en.wikipedia.org/wiki/Chebyshev_distance>`_
        distance.

    Returns
    -------
    se : float
        Sample Entropy.

    Notes
    -----
    Sample entropy is a modification of approximate entropy, used for assessing
    the complexity of physiological time-series signals. It has two advantages
    over approximate entropy: data length independence and a relatively
    trouble-free implementation. Large values indicate high complexity whereas
    smaller values characterize more self-similar and regular signals.

    The sample entropy of a signal :math:`x` is defined as:

    .. math:: H(x, m, r) = -\\log\\frac{C(m + 1, r)}{C(m, r)}

    where :math:`m` is the embedding dimension (= order), :math:`r` is
    the radius of the neighbourhood (default = :math:`0.2 * \\text{std}(x)`),
    :math:`C(m + 1, r)` is the number of embedded vectors of length
    :math:`m + 1` having a
    `Chebyshev distance <https://en.wikipedia.org/wiki/Chebyshev_distance>`_
    inferior to :math:`r` and :math:`C(m, r)` is the number of embedded
    vectors of length :math:`m` having a Chebyshev distance inferior to
    :math:`r`.

    Note that if ``metric == 'chebyshev'`` and ``len(x) < 5000`` points,
    then the sample entropy is computed using a fast custom Numba script.
    For other distance metric or longer time-series, the sample entropy is
    computed using a code from the
    `mne-features <https://mne.tools/mne-features/>`_ package by Jean-Baptiste
    Schiratti and Alexandre Gramfort (requires sklearn).

    References
    ----------
    Richman, J. S. et al. (2000). Physiological time-series analysis
    using approximate entropy and sample entropy. American Journal of
    Physiology-Heart and Circulatory Physiology, 278(6), H2039-H2049.

    https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.DistanceMetric.html

    Examples
    --------
    Fractional Gaussian noise with H = 0.5

    >>> import numpy as np
    >>> import entropy as ent
    >>> import stochastic.processes.noise as sn
    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.5, rng=rng).sample(10000)
    >>> print(f"{ent.sample_entropy(x, order=2):.4f}")
    2.1819

    Same with order = 3 and using the Euclidean distance

    >>> print(f"{ent.sample_entropy(x, order=3, metric='euclidean'):.4f}")
    2.6806

    Fractional Gaussian noise with H = 0.9

    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.9, rng=rng).sample(10000)
    >>> print(f"{ent.sample_entropy(x):.4f}")
    1.9078

    Fractional Gaussian noise with H = 0.1

    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.1, rng=rng).sample(10000)
    >>> print(f"{ent.sample_entropy(x):.4f}")
    2.0555

    Random

    >>> rng = np.random.default_rng(seed=42)
    >>> print(f"{ent.sample_entropy(rng.random(1000)):.4f}")
    2.2017

    Pure sine wave

    >>> x = np.sin(2 * np.pi * 1 * np.arange(3000) / 100)
    >>> print(f"{ent.sample_entropy(x):.4f}")
    0.1633

    Linearly-increasing time-series

    >>> x = np.arange(1000)
    >>> print(f"{ent.sample_entropy(x):.4f}")
    -0.0000
    """
    x = np.asarray(x, dtype=np.float64)
    if metric == 'chebyshev' and x.size < 5000:
        return _numba_sampen(x, order=order, r=(0.2 * x.std(ddof=0)))
    else:
        phi = _app_samp_entropy(x, order=order, metric=metric,
                                approximate=False)
        return -np.log(np.divide(phi[1], phi[0]))

@jit("uint32(uint32[:])", nopython=True)
def _lz_complexity(binary_string):
    """Internal Numba implementation of the Lempel-Ziv (LZ) complexity.
    https://github.com/Naereen/Lempel-Ziv_Complexity/blob/master/src/lziv_complexity.py
    - Updated with strict integer typing instead of strings
    - Slight restructuring based on Yacine Mahdid's notebook:
    https://github.com/BIAPT/Notebooks/blob/master/features/Lempel-Ziv%20Complexity.ipynb
    """
    # Initialize variables
    complexity = 1
    prefix_len = 1
    len_substring = 1
    max_len_substring = 1
    pointer = 0

    # Iterate until the entire string has not been parsed
    while prefix_len + len_substring <= len(binary_string):

        # Given a prefix length, find the largest substring
        if (
            binary_string[pointer + len_substring - 1]
            == binary_string[prefix_len + len_substring - 1]
        ):
            len_substring += 1  # increase the length of the substring
        else:

            max_len_substring = max(len_substring, max_len_substring)
            pointer += 1

            # Since all pointers have been scanned, pick largest as the jump size
            if pointer == prefix_len:

                # Increment complexity
                complexity += 1

                # Set prefix length to the max substring size found so far (jump size)
                prefix_len += max_len_substring

                # Reset pointer and max substring size
                pointer = 0
                max_len_substring = 1

            # Reset length of current substring
            len_substring = 1

    # Check if final iteration occurred in the middle of a substring
    if len_substring != 1:
        complexity += 1

    return complexity

def lziv_complexity(sequence, normalize=False):
    """
    Lempel-Ziv (LZ) complexity of (binary) sequence.
    .. versionadded:: 0.1.1
    Parameters
    ----------
    sequence : str or array
        A sequence of character, e.g. ``'1001111011000010'``,
        ``[0, 1, 0, 1, 1]``, or ``'Hello World!'``.
    normalize : bool
        If ``True``, returns the normalized LZ (see Notes).
    Returns
    -------
    lz : int or float
        LZ complexity, which corresponds to the number of different
        substrings encountered as the stream is viewed from the
        beginning to the end. If ``normalize=False``, the output is an
        integer (counts), otherwise the output is a float.
    Notes
    -----
    LZ complexity is defined as the number of different substrings encountered
    as the sequence is viewed from begining to the end.
    Although the raw LZ is an important complexity indicator, it is heavily
    influenced by sequence length (longer sequence will result in higher LZ).
    Zhang and colleagues (2009) have therefore proposed the normalized LZ,
    which is defined by
    .. math:: \\text{LZn} = \\frac{\\text{LZ}}{(n / \\log_b{n})}
    where :math:`n` is the length of the sequence and :math:`b` the number of
    unique characters in the sequence.
    References
    ----------
    * Lempel, A., & Ziv, J. (1976). On the Complexity of Finite Sequences.
      IEEE Transactions on Information Theory / Professional Technical
      Group on Information Theory, 22(1), 75–81.
      https://doi.org/10.1109/TIT.1976.1055501
    * Zhang, Y., Hao, J., Zhou, C., & Chang, K. (2009). Normalized
      Lempel-Ziv complexity and its application in bio-sequence analysis.
      Journal of Mathematical Chemistry, 46(4), 1203–1212.
      https://doi.org/10.1007/s10910-008-9512-2
    * https://en.wikipedia.org/wiki/Lempel-Ziv_complexity
    * https://github.com/Naereen/Lempel-Ziv_Complexity
    Examples
    --------
    >>> from entropy import lziv_complexity
    >>> # Substrings = 1 / 0 / 01 / 1110 / 1100 / 0010
    >>> s = '1001111011000010'
    >>> lziv_complexity(s)
    6
    Using a list of integer / boolean instead of a string:
    >>> # 1 / 0 / 10
    >>> lziv_complexity([1, 0, 1, 0, 1, 0, 1, 0, 1, 0])
    3
    With normalization:
    >>> lziv_complexity(s, normalize=True)
    1.5
    This function also works with characters and words:
    >>> s = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
    >>> lziv_complexity(s), lziv_complexity(s, normalize=True)
    (26, 1.0)
    >>> s = 'HELLO WORLD! HELLO WORLD! HELLO WORLD! HELLO WORLD!'
    >>> lziv_complexity(s), lziv_complexity(s, normalize=True)
    (11, 0.38596001132145313)
    """
    assert isinstance(sequence, (str, list, np.ndarray))
    assert isinstance(normalize, bool)
    if isinstance(sequence, (list, np.ndarray)):
        sequence = np.asarray(sequence)
        if sequence.dtype.kind in 'bfi':
            # Convert [True, False] or [1., 0.] to [1, 0]
            s = sequence.astype("uint32")
        else:
            # Treat as numpy array of strings
            # Map string characters to utf-8 integer representation
            s = np.fromiter(map(ord, "".join(sequence.astype(str))), dtype="uint32")
            # Can't preallocate length (by specifying count) due to string concatenation
    else:
        s = np.fromiter(map(ord, sequence), dtype="uint32")

    if normalize:
        # 1) Timmermann et al. 2019
        # The sequence is randomly shuffled, and the normalized LZ
        # is calculated as the ratio of the LZ of the original sequence
        # divided by the LZ of the randomly shuffled LZ. However, the final
        # output is dependent on the random seed.
        # sl_shuffled = list(s)
        # rng = np.random.RandomState(None)
        # rng.shuffle(sl_shuffled)
        # s_shuffled = ''.join(sl_shuffled)
        # return _lz_complexity(s) / _lz_complexity(s_shuffled)
        # 2) Zhang et al. 2009
        n = len(s)
        base = sum(np.bincount(s) > 0)  # Number of unique characters
        base = 2 if base < 2 else base
        return _lz_complexity(s) / (n / log(n, base))
    else:
        return _lz_complexity(s)

###############################################################################
# OTHER TIME-DOMAIN METRICS
###############################################################################

def num_zerocross(x, normalize=False, axis=-1):
    """Number of zero-crossings.

    .. versionadded: 0.1.3

    Parameters
    ----------
    x : list or np.array
        1D or N-D data.
    normalize : bool
        If True, divide by the number of samples to normalize the output
        between 0 and 1. Otherwise, return the absolute number of zero
        crossings.
    axis : int
        The axis along which to perform the computation. Default is -1 (last).

    Returns
    -------
    nzc : int or float
        Number of zero-crossings.

    Examples
    --------
    Simple examples

    >>> import numpy as np
    >>> import entropy as ent
    >>> ent.num_zerocross([-1, 0, 1, 2, 3])
    1

    >>> ent.num_zerocross([0, 0, 2, -1, 0, 1, 0, 2])
    2

    Number of zero crossings of a pure sine

    >>> import numpy as np
    >>> import entropy as ent
    >>> sf, f, dur = 100, 1, 4
    >>> N = sf * dur # Total number of discrete samples
    >>> t = np.arange(N) / sf # Time vector
    >>> x = np.sin(2 * np.pi * f * t)
    >>> ent.num_zerocross(x)
    7

    Random 2D data

    >>> np.random.seed(42)
    >>> x = np.random.normal(size=(4, 3000))
    >>> ent.num_zerocross(x)
    array([1499, 1528, 1547, 1457])

    Same but normalized by the number of samples

    >>> np.round(ent.num_zerocross(x, normalize=True), 4)
    array([0.4997, 0.5093, 0.5157, 0.4857])

    Fractional Gaussian noise with H = 0.5

    >>> import stochastic.processes.noise as sn
    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.5, rng=rng).sample(10000)
    >>> print(f"{ent.num_zerocross(x, normalize=True):.4f}")
    0.4973

    Fractional Gaussian noise with H = 0.9

    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.9, rng=rng).sample(10000)
    >>> print(f"{ent.num_zerocross(x, normalize=True):.4f}")
    0.2615

    Fractional Gaussian noise with H = 0.1

    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.1, rng=rng).sample(10000)
    >>> print(f"{ent.num_zerocross(x, normalize=True):.4f}")
    0.6451
    """
    x = np.asarray(x)
    # https://stackoverflow.com/a/29674950/10581531
    nzc = np.diff(np.signbit(x), axis=axis).sum(axis=axis)
    if normalize:
        nzc = nzc / x.shape[axis]
    return nzc

def hjorth_params(x, axis=-1):
    """Calculate Hjorth mobility and complexity on given axis.

    .. versionadded: 0.1.3

    Parameters
    ----------
    x : list or np.array
        1D or N-D data.
    axis : int
        The axis along which to perform the computation. Default is -1 (last).

    Returns
    -------
    mobility, complexity : float
        Mobility and complexity parameters.

    Notes
    -----
    Hjorth Parameters are indicators of statistical properties used in signal
    processing in the time domain introduced by Bo Hjorth in 1970. The
    parameters are activity, mobility, and complexity. EntroPy only returns the
    mobility and complexity parameters, since activity is simply the variance
    of :math:`x`, which can be computed easily with :py:func:`numpy.var`.

    The **mobility** parameter represents the mean frequency or the proportion
    of standard deviation of the power spectrum. This is defined as the square
    root of variance of the first derivative of :math:`x` divided by the
    variance of :math:`x`.

    The **complexity** gives an estimate of the bandwidth of the signal, which
    indicates the similarity of the shape of the signal to a pure sine wave
    (where the value converges to 1). Complexity is defined as the ratio of
    the mobility of the first derivative of :math:`x` to the mobility of
    :math:`x`.

    References
    ----------
    - https://en.wikipedia.org/wiki/Hjorth_parameters
    - https://doi.org/10.1016%2F0013-4694%2870%2990143-4

    Examples
    --------
    Hjorth parameters of a pure sine

    >>> import numpy as np
    >>> import entropy as ent
    >>> sf, f, dur = 100, 1, 4
    >>> N = sf * dur # Total number of discrete samples
    >>> t = np.arange(N) / sf # Time vector
    >>> x = np.sin(2 * np.pi * f * t)
    >>> np.round(ent.hjorth_params(x), 4)
    array([0.0627, 1.005 ])

    Random 2D data

    >>> np.random.seed(42)
    >>> x = np.random.normal(size=(4, 3000))
    >>> mob, com = ent.hjorth_params(x)
    >>> print(mob)
    [1.42145064 1.4339572  1.42186993 1.40587512]

    >>> print(com)
    [1.21877527 1.21092261 1.217278   1.22623163]

    Fractional Gaussian noise with H = 0.5

    >>> import stochastic.processes.noise as sn
    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.5, rng=rng).sample(10000)
    >>> np.round(ent.hjorth_params(x), 4)
    array([1.4073, 1.2283])

    Fractional Gaussian noise with H = 0.9

    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.9, rng=rng).sample(10000)
    >>> np.round(ent.hjorth_params(x), 4)
    array([0.8395, 1.9143])

    Fractional Gaussian noise with H = 0.1

    >>> rng = np.random.default_rng(seed=42)
    >>> x = sn.FractionalGaussianNoise(hurst=0.1, rng=rng).sample(10000)
    >>> np.round(ent.hjorth_params(x), 4)
    array([1.6917, 1.0717])
    """
    x = np.asarray(x)
    # Calculate derivatives
    dx = np.diff(x, axis=axis)
    ddx = np.diff(dx, axis=axis)
    # Calculate variance
    x_var = np.var(x, axis=axis)  # = activity
    dx_var = np.var(dx, axis=axis)
    ddx_var = np.var(ddx, axis=axis)
    # Mobility and complexity
    mob = np.sqrt(dx_var / x_var)
    com = np.sqrt(ddx_var / dx_var) / mob
    return mob, com


import numpy as np
import pyLHD

# Maximum Absolute Correlation

def MaxAbsCor(arr):
  """ Calculate the Maximum Absolute Correlation

  Args:
      arr (numpy.ndarray): A design matrix

  Returns:
      [float]: Positive number indicating maximum absolute correlation. Rounded to 3 digits

  Examples:
    >>> example_LHD = pyLHD.rLHD(nrows=5,ncols=3)
    >>> pyLHD.MaxAbsCor(example_LHD)
  """
  p = arr.shape[1]  # number of columns
  corr = []
  for i in range(0, p-1):
    for j in range(i+1, p):
      corr.append(np.corrcoef(arr[:, i], arr[:, j])[0, 1])
  abs_corr_array = np.absolute(np.asarray(corr))
  return np.around(np.amax(abs_corr_array), 3)

# Calculate the Maximum Projection Criterion

def MaxProCriterion(arr):
  """ Calculate the Maximum Projection Criterion

  Args:
      arr (numpy.ndarray): A design matrix

  Returns:
      [float]: Positive number indicating maximum projection criterion

  Examples:
      >>> example_LHD = pyLHD.rLHD(nrows=5,ncols=3)
      >>> pyLHD.MaxProCriterion(example_LHD)
  """
  n = arr.shape[0]
  p = arr.shape[1]

  temp =0

  for i in range(0,n-1):
    for j in range(i+1,n):
      denom = 1
      for k in range(0,p):
        denom = denom*(arr[i,k]-arr[j,k])**2
      temp = temp + 1/denom
  return (2/(n*(n-1) ) *temp)**(1/p)

# Calculate the Inter-site Distance

def dij(arr,i, j, q = 1):
  """ Calculate the Inter-site Distance

  Args:
      arr (numpy.ndarray): A design matrix
      i (int): A positive integer, which stands for the ith row of (arr)
      j (int): A positive integer, which stands for the jth row of (arr)
      q (int, optional): The default is set to be 1, and it could be either 1 or 2.
      If (q) is 1, (dij) is the Manhattan (rectangular) distance. If (q) is 2, (dij) is the Euclidean distance.

  Returns:
      [float]: positive number indicating the distance (rectangular or Euclidean) between the ith and jth row of arr

  Examples:
      # Calculate the inter-site distance of the 2nd and the 4th row of example_LHD
      >>> example_LHD = pyLHD.rLHD(nrows=5,ncols=3)
      >>> pyLHD.dij(example_LHD,i=2,j=4)

      # Calculate the inter-site distance of the 2nd and the 4th row of example_LHD with q=2 (Euclidean)
      >>> example_LHD = pyLHD.rLHD(nrows=5,ncols=3)
      >>> pyLHD.dij(example_LHD,i=2,j=4,q=2)
  """
  p = arr.shape[1]
  distance = np.empty(p)
  for l in range(0,p):
    distance[l] = np.absolute(arr[i,l]-arr[j,l])**q
  return np.sum(distance)**(1/q)

# Calculate the phi_p Criterion

def phi_p(arr,p=15,q=1):
  """ Calculate the phi_p Criterion

  Args:
      arr (numpy.ndarray): A design matrix
      p (int, optional): A positive integer, which is the parameter in the phi_p formula. The default is set to be 15
      If (q) is 1, (dij) is the Manhattan (rectangular) distance. If (q) is 2, (dij) is the Euclidean distance.

  Returns:
      [float]: a positive number indicating phi_p

  Examples:
      # Calculate the phi_p criterion for example_LHD with default settings
      >>> example_LHD = pyLHD.rLHD(nrows=5,ncols=3)
      >>> pyLHD.phi_p(example_LHD)

      # Calculate the phi_p criterion of example_LHD with p=50 and q=2 (Euclidean)
      >>> pyLHD.phi_p(example_LHD,p=50,q=2)
  """
  n = arr.shape[0]
  isd = 0
  for i in range(0,n-1):
    for j in range(i+1,n):
      isd = isd + dij(arr,i=i,j=j,q=q)**(-p)
  return np.sum(isd)**(1/p)

# Calculate the Average Absolute Correlation

def AvgAbsCor(arr):
  """ Calculate the Average Absolute Correlation

  Args:
      arr (numpy.ndarray): A design matrix

  Returns:
      [float]: A positive number indicating the average absolute correlation
      of input matrix

  Examples:
      # Calculate the average absolute correlation of example_LHD
      >>> example_LHD = pyLHD.rLHD(nrows=5,ncols=3)
      >>> pyLHD.AvgAbsCor(example_LHD)
  """
  p = arr.shape[1]
  corr = []
  for i in range(0,p-1):
    for j in range(i+1,p):
      corr.append(np.corrcoef(arr[:,i],arr[:,j])[0,1])
  abs_corr_array = np.absolute(np.asarray(corr))
  return np.around(np.mean(abs_corr_array),3)

# Caluclate the Discrepancy of a given sample

def discrepancy(arr, type='centered_L2'):
  """ Discrepancy of a given sample

  Args:
      arr (numpy.ndarray): A design matrix
      type (str, optional): Type of discrepancy. Defaults to 'centered_L2'. Options include:
      'L2', 'L2_star','centered_L2', 'modified_L2', 'mixture_L2', 'symmetric_L2', 'wrap_around_L2'

  Raises:
      ValueError: Whenever number of rows is less than number of columns

  Returns:
      float: Desired discrepancy type

  Examples:
      # Calculate the centered_L2 discrepancy of example_LHD
      >>> example_LHD = pyLHD.rLHD(nrows=5,ncols=3)
      >>> pyLHD.discrepancy(example_LHD)
      # Calculate the L2 star discrepancy of example_LHD
      >>> pyLHD.discrepancy(example_LHD,type='L2_star)
  """

  if (np.amin(arr) < 0 or np.amax(arr) > 1):
    arr = pyLHD.scale(arr)

  nrows = arr.shape[0]
  ncols = arr.shape[1]

  if nrows < ncols:
    raise ValueError('Make sure number of rows is greater than number of columns')

  sum1 = 0
  sum2 = 0

  if type == 'L2':

    for i in range(nrows):
      sum1 += np.prod(arr[i,:]*(1-arr[i,:]))
      for k in range(nrows):
        q =  1
        for j in range(ncols):
          q = q*(1-np.maximum(arr[i,j],arr[k,j]))*np.minimum(arr[i,j],arr[k,j])
        sum2 += q
    value = np.sqrt(12**(-ncols) - (((2**(1-ncols))/nrows)*sum1) + ((1/nrows**2)*sum2))

  if type == 'L2_star':
    dL2 = 0
    for j in range(nrows):
      for i in range(nrows):
        if i!=j:
          t = []
          for l in range(ncols):
            t.append(1-np.maximum(arr[i,l],arr[j,l]))
          t = (np.prod(t))/(nrows**2)
        else:
          t1 = 1-arr[i,:]
          t1 = np.prod(t1)
          t2 = 1-np.square(arr[i,:])
          t2 = np.prod(t2)
          t = t1/(nrows**2)-((2**(1-ncols))/nrows)*t2

        dL2 += t

    value = np.sqrt(3**(-ncols)+dL2)

  if type == 'centered_L2':

    for i in range(nrows):
      sum1 += np.prod((1+0.5*np.abs(arr[i,:]-0.5)-0.5*((abs(arr[i,:]-0.5))**2)))
      for k in range(nrows):
        sum2 +=  np.prod((1+0.5*np.abs(arr[i,:]-0.5)+0.5*np.abs(arr[k,:]-0.5)-0.5*np.abs(arr[i,:]-arr[k,:])))
    value =  np.sqrt( ( (13/12)**ncols)-((2/nrows)*sum1) + ((1/(nrows**2))*sum2)  )

  if type == 'modified_L2':

    for i in range(nrows):
      p = 1
      p = np.prod((3-(arr[i,:]*arr[i,:])))
      sum1 += p

      for k in range(nrows):
        q = 1
        for j in range(ncols):
          q = q*(2-np.maximum(arr[i,j],arr[k,j]))
        sum2 += q
    value =  np.sqrt(((4/3)**ncols) - (((2**(1-ncols))/nrows)*sum1) + ((1/nrows**2)*sum2))

  if type == 'mixture_L2':
    for i in range(nrows):
      sum1 += np.prod((5/3-0.25*np.abs(arr[i,:]-0.5)-0.25*((np.abs(arr[i,:]-0.5))**2)))
      for k in range(nrows):
        sum2 += np.prod((15/8-0.25*np.abs(arr[i,:]-0.5)-0.25*np.abs(arr[k,:]-0.5)-
                         0.75*np.abs(arr[i,:]-arr[k,:])+0.5*((np.abs(arr[i,:]-arr[k,:]))**2)))
    value = np.sqrt(((19/12)**ncols)-((2/nrows)*sum1) + ((1/nrows**2)*sum2))

  if type == 'symmetric_L2':

    for i in range(nrows):
      sum1 += np.prod( (1+2*arr[i,:]) - (2*arr[i,:]*arr[i,:]))
      for k in range(nrows):
        sum2 += np.prod( (1-np.abs(arr[i,:]-arr[k,:])) )
    value = np.sqrt(((4/3)**ncols) - ((2/nrows)*sum1) + ((2**ncols/nrows**2)*sum2))

  if type == 'wrap_around_L2':
    for i in range(nrows):
      for k in range(nrows):
        sum1 += np.prod((1.5-((np.abs(arr[i,:]-arr[k,:]))*(1-np.abs(arr[i,:]-arr[k,:])))))

    value =  np.sqrt((-((4/3)**ncols) + ((1/nrows**2)*sum1)))

  return value

# Compute the coverage measure

def coverage(arr):
  """ Compute the coverage measure for a design

  Args:
      arr (numpy.ndarray): A design matrix. If design matrix is not within [0,1], the
      origianl design will be scaled to [0,1]
  Raises:
      ValueError: Whenever number of rows is less than number of columns

  Returns:
      float: Coverage measure

  Examples:
      >>> example_LHD = pyLHD.rLHD(nrows=5,ncols=5)
      >>> pyLHD.coverage(example_LHD)
  """
  nrows = arr.shape[0]
  ncols = arr.shape[1]

  if nrows < ncols:
    raise ValueError('Make sure number of rows is greater than number of columns')

  if (np.amin(arr) < 0 or np.amax(arr) > 1):
    x = pyLHD.scale(arr)
  else:
    x = arr

  dist = lambda p1, p2: np.sqrt(((p1-p2)**2).sum())
  dist_mat = np.asarray([[dist(p1, p2) for p2 in x] for p1 in x])
  np.fill_diagonal(dist_mat,10e3)

  Dmin = np.amin(dist_mat,axis=0)
  gammabar = (1/nrows)*np.sum(Dmin)
  sum = 0

  for i in range(nrows):
    sum +=  (Dmin[i]-gammabar)*(Dmin[i]-gammabar)

  cov = (1/gammabar)*((1/nrows)*sum)**(1/2)
  return cov

# Compute the meshratio criterion

def mesh_ratio(arr):
  """ Compute the meshratio criterion for a given design

  Args:
      arr (numpy.ndarray): A design matrix. If design matrix is not within [0,1], the
      origianl design will be scaled to [0,1]

  Raises:
      ValueError: Whenever number of rows is less than number of columns

  Returns:
      float: Calculated meshratio

  Examples:
      >>> example_LHD = pyLHD.rLHD(nrows=5,ncols=5)
      >>> pyLHD.mesh_ratio(example_LHD)
  """

  nrows = arr.shape[0]
  ncols = arr.shape[1]

  if nrows < ncols:
    raise ValueError('Make sure number of rows is greater than number of columns')

  if (np.amin(arr) < 0 or np.amax(arr) > 1):
    x = pyLHD.scale(arr)
  else:
    x = arr

  max_dist = -1.0e30
  min_dist = 1.0e30

  for i in range(nrows-1):
    a = 1.0e30
    b = -1.0e30
    for k in range(nrows):
      if i != k:
        Dist = 0
        for j in range(ncols):
          Dist += (x[i,j] - x[k,j])*(x[i,j]-x[k,j])
        if Dist > b:
          b = Dist
        if Dist < a:
          a = Dist
    if max_dist < a:
      max_dist = a
    if min_dist > a:
      min_dist = a
  ratio = np.sqrt(max_dist/min_dist)
  return ratio

# Calculate maximin criterion

def maximin(arr):
  """ Compute the maximin criterion for a given design. A higher value corresponds
  to a more regular scattering of design points.

  Args:
      arr (numpy.ndarray): A design matrix. If design matrix is not within [0,1], the
      origianl design will be scaled to [0,1]

  Returns:
      float: Calculated maximin criterion

  Examples:
      >>> example_LHD = pyLHD.rLHD(nrows=5,ncols=5)
      >>> pyLHD.maximin(example_LHD)
  """

  if (np.amin(arr) < 0 or np.amax(arr) > 1):
    x = pyLHD.scale(arr)
  else:
    x = arr

  dist = lambda p1, p2: np.sqrt(((p1-p2)**2).sum())
  dist_mat = np.asarray([[dist(p1, p2) for p2 in x] for p1 in x])
  np.fill_diagonal(dist_mat,1e30)
  min = np.amin(dist_mat,axis=0)
  return np.amin(min)


import yfinance as yf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.stats import norm

DEBUG = False

def portfolioValueAtRisk(portfolio: dict, start: str, end: str, conf: float):
    """
    Calculates overall Value at Risk (VaR) for a portfolio of equities.
    @param portfolio: Dictionary of equities, where the key is the ticker and value is its weight in the portfolio (% dollar value)
    @param start: Start date for eod-values
    @param end: End date for eod-values
    @param conf: Confidence level (either .95 or .99, usually)
    """

    values = []
    for ticker, weight in portfolio:
        values.append(weight * getValueAtRisk(ticker, start, end, conf))
    return sum(values)

def historicalValueAtRisk(ticker: str, start: str, end: str, conf: float):
    """
    Calculates Value at Risk (VaR) for a certain equity by checking historical returns
    """
    # Get historical eod values and calculate percent returns per day
    historical_eod = yf.Ticker(ticker).history(start=start, end=end)
    returns = historical_eod['Close'].pct_change()

    returns = np.sort(returns)
    return round(returns[int((1 - conf) * returns.size)], 4)

def getValueAtRisk(ticker: str, start: str, end: str, conf: float):
    """
    Calculates Value at Risk (VaR) for a certain equity
    @param ticker: Equity ticker
    @param start: Start date for eod-values
    @param end: End date for eod-values
    @param conf: Confidence level (either .95 or .99, usually)
    """

    # Get historical eod values and calculate percent returns per day.
    historical_eod = yf.Ticker(ticker).history(start=start, end=end)
    returns = historical_eod['Close'].pct_change()

    # Calculate expected value, standard deviation from historical data
    mu = returns.mean()
    sigma = returns.std()  # TODO: use unbiased estimator (n-1) or not?
    ppf = norm.ppf(1 - conf)  # Get ppf of normal distribution at inverse of confidence level
    return round(mu + ppf*sigma, 4)   # Subtract average returns from the maximum expected loss (ppf * standard deviation)

def simulateValueAtRisk(ticker: str, start: str, end: str, conf: float, days: int, trials: int):
    """
    Calculate Value at Risk (VaR) using Monte-Carlo Simulation
    """
    historical_eod = yf.Ticker(ticker).history(start=start, end=end)
    returns = historical_eod['Close'].pct_change()
    ln_returns = np.log(1 + returns)

    mu = np.array(ln_returns.mean())
    drift = np.array(mu - 0.5 * ln_returns.var())
    sigma = np.array(ln_returns.std())

    mc_returns = np.exp(drift + sigma * norm.ppf(np.random.rand(days, trials)))

    prices = np.zeros_like(mc_returns)
    prices[0] = historical_eod['Close'].iloc[-1]
    for day in range(1, days):
        prices[day] = prices[day - 1] * mc_returns[day]

    if DEBUG:
        print(mu, drift, sigma)
        print(mc_returns)
        print(prices)

    plt.plot(prices)
    plt.show()

    # Get maximum possible loss at confidence level
    last_price = prices[-1]
    mc_change = (last_price - np.array(historical_eod['Close'].iloc[-1])) / np.array(historical_eod['Close'].iloc[-1])

    if DEBUG:
        print(mu, drift, sigma)
        print(mc_returns)
        print(prices)
        print(mc_change)

    return np.sort(mc_change)[int((1 - conf) * trials)]

if __name__=='__main__':
    ticker = input("Enter ticker: ")
    start = input("Enter data start date (YYYY-MM-DD): ")
    end = input("Enter data end date (YYYY-MM-DD): ")
    conf = input("Enter confidence level (.95 or .99): ")
    print(f"Value at Risk (VaR) at {conf} confidence is historically {historicalValueAtRisk(ticker, start, end, float(conf))} and variance-covariance {getValueAtRisk(ticker, start, end, float(conf))}")

    print(f"One-month projection for VaR with Monte-Carlo is {simulateValueAtRisk(ticker, start, end, float(conf), 30, 1000)}.")


#!/usr/bin/env python3

import numpy as np

def create_rotation_matrix(
    x: float,
    y: float,
    z: float,
) -> np.ndarray:
    r"""Create Rotation Matrix

    params:
        x: x-axis rotation float
        y: y-axis rotation float
        z: z-axis rotation float

    return:
        rotation matrix: numpy.ndarray
    """
    # calculate rotation about the x-axis
    R_x = np.array(
        [
            [1.0, 0.0, 0.0],
            [0.0, np.cos(x), -np.sin(x)],
            [0.0, np.sin(x), np.cos(x)],
        ]
    )
    # calculate rotation about the y-axis
    R_y = np.array(
        [
            [np.cos(y), 0.0, np.sin(y)],
            [0.0, 1.0, 0.0],
            [-np.sin(y), 0.0, np.cos(y)],
        ]
    )
    # calculate rotation about the z-axis
    R_z = np.array(
        [
            [np.cos(z), -np.sin(z), 0.0],
            [np.sin(z), np.cos(z), 0.0],
            [0.0, 0.0, 1.0],
        ]
    )

    #return R_z @ R_y @ R_x
    return R_x @ R_y @ R_z


import numpy as np
import matplotlib.pyplot as plt

pi = np.pi
def Fft_series(series):
    """
      The `FFT y[k]` of length N of the length-N sequence `x[n]` is defined as
        $$ y[k]= \sum_{n=0}^{N-1} e^{-2\pi i \frac{kn}{N} } x[n] $$
    """

    N = len(series)
    # real part
    y_r = np.zeros(len(series))
    # image part
    y_i = np.zeros(len(series))

    for k in range(N):
        y_r[k] = sum([(np.exp(-2*pi*1j*k*n/N)*series[n]).real for n in range(N)])
        y_i[k] = sum([(np.exp(-2*pi*1j*k*n/N)*series[n]).imag for n in range(N)])

    # The fft transform must be under the complex-type : real_part + j*image_part
    return y_r + 1j * y_i

def inverse_Fft_series(fft_series):
    """
      The inverse transform of the series
        $$ x[n]= \dfrac{1}{N} \sum_{k=0}^{N-1} e^{2\pi i \frac{kn}{N} } y[k] $$
    """

    N = len(fft_series)
    y_r = np.zeros(len(fft_series))

    for k in range(N):
        y_r[k] = np.mean([(np.exp(2*pi*1j*k*n/N)*fft_series[n]).real for n in range(N)])

    return y_r


#!/usr/bin/env python3

import numpy as np

def computeCostMulti(X, y, theta):
    #COMPUTECOSTMULTI Compute cost for linear regression with multiple variables
    #   J = COMPUTECOSTMULTI(X, y, theta) computes the cost of using theta as the
    #   parameter for linear regression to fit the data points in X and y

    # Initialize some useful values
    m = X.shape[0] # number of training examples

    # You need to return the following variables correctly
    #J = 0;

    # ====================== YOUR CODE HERE ======================
    # Instructions: Compute the cost of a particular choice of theta
    #               You should set J to the cost.

    J = np.sum(np.power(np.subtract(np.dot(X, theta), y), 2.0)) / (2 * m)

    # =========================================================================

    return J


"""
Author : Achintya Gupta
Date Created : 24-09-2020
"""

"""
Problem Statement
------------------------------------------------
Q)  The sum of the primes below 10 is 2 + 3 + 5 + 7 = 17.
    Find the sum of all the primes below two million.
"""

from utils import timing_decorator, eratosthenes_primegen
import numpy as np

@timing_decorator
def primesum_below(N=2e6):
    next_primegen = eratosthenes_primegen()
    next_prime = next(next_primegen)
    total_sum = 0
    while next_prime<N:
        total_sum+=next_prime
        next_prime = next(next_primegen)
    print(f'Sum of all primes below {N} : {total_sum}')

primesum_below()


import numpy as np
np.set_printoptions(precision=4)

x_guess_0 = np.array([1, 1, 1]);
x_guess_0.astype(float)
err_tol = 0.1;
run = 1;
err_max = 1.; #Initial value
err_max_ARRAY = np.array([0]);
while (err_max >= err_tol):
    if(run == 1):
        x_guess_old = x_guess_0;
    else:
        x_guess_old = x_new
    run = run+1;
    x_1_new = (1-3.0*x_guess_old[1] + 5.0*x_guess_old[2])/12;
    x_2_new = (28 - x_1_new - 3*x_guess_old[2])/5;
    x_3_new = (76-3*x_1_new -7*x_2_new)/13;
    #Strain gage problem

    x_new = np.array([x_1_new, x_2_new, x_3_new]);
    print 'New values of [x_1, x_2, x_3]:'
    print x_new
    err_1 = (x_1_new-x_guess_old[0])*100/x_1_new;
    err_2 = (x_2_new-x_guess_old[1])*100/x_2_new;
    err_3 = (x_3_new-x_guess_old[2])*100/x_3_new;
    err = np.array([err_1, err_2, err_3]);
    err_max = np.amax(err)
    print 'Max error: '
    print err_max
    print ''
    if(err_max <= err_tol):
        print ' '
        print'*******************************'
        print 'Converged solution!'
        print x_new
        print '******************************'


import numpy as np
import matplotlib.pyplot as plt

N = 4          # number of time steps
r = 0.06       # interest rate
K = 1.1        # strike
T = 3          # Maturity

dt = T/(N-1)          # time interval
df = np.exp(-r * dt)  # discount factor per time interval

S = np.array([
            [1.00, 1.09, 1.08, 1.34],
            [1.00, 1.16, 1.26, 1.54],
            [1.00, 1.22, 1.07, 1.03],
            [1.00, 0.93, 0.97, 0.92],
            [1.00, 1.11, 1.56, 1.52],
            [1.00, 0.76, 0.77, 0.90],
            [1.00, 0.92, 0.84, 1.01],
            [1.00, 0.88, 1.22, 1.34]])

print('Stock price paths: \n', S)
H = np.maximum(K - S, 0)           # intrinsic values for put option
V = np.zeros_like(H)               # value matrix
V[:,-1] = H[:,-1]
print('Cash-flow matrix at time 3: \n',V)

# Valuation by LS Method
for t in range(N-2, 0, -1):

    good_paths = H[:,t] > 0        # paths where the intrinsic value is positive
                                   # the regression is performed only on these paths

    rg = np.polyfit( S[good_paths, t], V[good_paths, t+1] * df, 2)    # polynomial regression
    C = np.polyval( rg, S[good_paths,t] )                             # evaluation of regression
    print('\n Regression at time', t)
    print('X: ', S[good_paths, t])
    print('Y: ', V[good_paths, t+1] * df)
    print('\nPolynomial regression:')
    print('Conditional expactation function is E[ Y | X ] = ', round(rg[-1], 3), ' + ', round(rg[-2], 3),'* X', ' + ', round(rg[-3], 3), '* X^2' )
    print('\nOptimal early exercise decision at time ',t,' countinuation values: ', C)

    exercise = np.zeros( len(good_paths), dtype=bool)    # initialize
    exercise[good_paths] = H[good_paths,t] > C           # paths where it is optimal to exercise

    V[exercise,t] = H[exercise,t]                        # set V equal to H where it is optimal to exercise
    V[exercise,t+1:] = 0                                 # set future cash flows, for that path, equal to zero
    discount_path = (V[:,t] == 0)                        # paths where we didn't exercise
    V[discount_path,t] = V[discount_path,t+1] * df       # set V[t] in continuation region

V0 = np.mean(V[:,1]) * df  # discounted expectation of V[t=1]

print('\nOption cash flow matrix: \n', V)

print("\n \t\tExample price= ", V0)