# day 6 Numpy Array Function

import numpy as np
import time
from timeit import timeit
np.random.seed(0)

def compute_reciprocals(values):
    output = np.empty(len(values))
    for i in range(len(values)):
        output[i] = 1.0 / values[i]
    return output

# time loop
# value1 = np.random.randint(1, 10, size=5)
# t1 = timeit('compute_reciprocals(value1)', 'from __main__ import compute_reciprocals, value1', number=1)
# print('timeit', t1)
#
# value2 = np.random.randint(1, 100, size=1000000)
# t2 = timeit('compute_reciprocals(value2)', 'from __main__ import compute_reciprocals, value2', number=1)
# print('timeit', t2)
#
# t3 = timeit('1.0 / value2', 'from __main__ import value2', number=1)
# print('timeit', t3)

def array_arithmetic():
    x = np.arange(4)
    print("x     =", x)
    print("x + 5 =", x + 5)
    print("x - 5 =", x - 5)
    print("x * 2 =", x * 2)
    print("x / 2 =", x / 2)
    print("x // 2 =", x // 2)
    print("-x     = ", -x)
    print("x ** 2 = ", x ** 2)
    print("x % 2  = ", x % 2)
    print('-(0.5*x + 1) ** 2', -(0.5*x + 1) ** 2)
    print('add', np.add(x, 2))

    x = np.array([-2, -1, 0, 1, 2])
    print('x=', x)
    print('abs(x)=', abs(x))
    print('np.absolute(x) = ', np.absolute(x))
    print('np.abs(x) = ', np.abs(x))
    x = np.array([3 - 4j, 4 - 3j, 2 + 0j, 0 + 1j])
    print('x=', x)
    print('np.abs(x) = ', np.abs(x))

def trigonometric():
    theta = np.linspace(0, np.pi, 3)
    print("theta      = ", theta)
    print("sin(theta) = ", np.sin(theta))
    print("cos(theta) = ", np.cos(theta))
    print("tan(theta) = ", np.tan(theta))
    x = [-1, 0, 1]
    print("x         = ", x)
    print("arcsin(x) = ", np.arcsin(x))
    print("arccos(x) = ", np.arccos(x))
    print("arctan(x) = ", np.arctan(x))

def logarithms():
    x = [1, 2, 3]
    print("x     =", x)
    print("e^x   =", np.exp(x))
    print("2^x   =", np.exp2(x))
    print("3^x   =", np.power(3, x))

    x = [1, 2, 4, 10]
    print("x        =", x)
    print("ln(x)    =", np.log(x))
    print("log2(x)  =", np.log2(x))
    print("log10(x) =", np.log10(x))

    x = [0, 0.001, 0.01, 0.1]
    print("exp(x) - 1 =", np.expm1(x))
    print("log(1 + x) =", np.log1p(x))

def advanced_feature():
    # write computation results directly to the memory location
    x = np.arange(5)
    y = np.empty(5)
    np.multiply(x, 10, out=y)
    print(y)

    y = np.zeros(10)
    np.power(2, x, out=y[::2])
    print(y)

    x = np.arange(1, 6)
    print('x=', x)
    sum = np.add.reduce(x)
    print('sum=', sum)
    mul = np.multiply.reduce(x)
    print('multiply reduce=', mul)
    sum2 = np.add.accumulate(x)
    mul2 = np.multiply.accumulate(x)
    out = np.multiply.outer(x, x)
    print('add.accumulate=', sum2)
    print('multiply.accumulate=', mul2)
    print('multiply.outer=', out)

from scipy import special

def scipy_special():
    # Gamma functions
    x = [1, 5, 10]
    print("gamma(x)     =", special.gamma(x))
    print("ln|gamma(x)| =", special.gammaln(x))
    print("beta(x, 2)   =", special.beta(x, 2))
    # Error function (integral of Gaussian)
    x = np.array([0, 0.3, 0.7, 1.0])
    print("erf(x)  =", special.erf(x))
    print("erfc(x) =", special.erfc(x))
    print("erfinv(x) =", special.erfinv(x))

if __name__ == '__main__':
    print('Numpy Version', np.__version__)
    array_arithmetic()
    trigonometric()
    logarithms()
    advanced_feature()
    scipy_special()


import numpy as np

def euclidean_distance(p1,p2):
	"""
	returns euclidean distance between matrices
	@params:
		p1, p2: np.ndarray
			matrices to perform operation to.
	"""
	return np.sqrt(np.sum((p1-p2)**2, axis=1))

def entropy(p):
		"""
		Will be our measurement for uncertainty in our construction
		of descision tree
		@params:
			p: float

		"""
		if p == 0:
			return 0
		elif p == 1:
			return 0
		else:
			return -(p * np.log2(p) + (1 - p) * np.log2(1 - p))

def information_gain(left_child, right_child):
		"""
		measurement of how much info we gained when splitting a node
		using our entropy method.
		@def:
			takes in a list of classes from left and right child to return
			the information gain of our curr split
		@params:
			left_child: np.ndarray
				curr left child arr
			right_child: np.ndarray
				curr left child arr
		"""
		parent = left_child + right_child
		p_par = parent.count(1) / len(parent) if len(parent) > 0 else 0
		p_left = left_child.count(1) / len(left_child) if len(left_child) \
		> 0 else 0
		p_right = right_child.count(1) / len(right_child) if len(right_child) \
		> 0 else 0

		infogain_p = self.entropy(p_par)
		infogain_l = self.entropy(p_left)
		infogain_r = self.entropy(p_right)

		return infogain_p - len(left_child) / len(parent) * infogain_l - \
		len(right_child) / len(parent) * infogain_r


# Usando automatically differentiable numpy

import jax.numpy as jnp

pi = jnp.pi

feynmanPyData = {
    'I.10.7' : {
        'string expression' : 'm_0/jnp.sqrt(1-v**2/c**2)',
        'latex expression'  : r'm_0/\sqrt{(1-v^2/c^2)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda m_0,v,c:m_0/jnp.sqrt(1-v**2/c**2))(*args)
    },
    'I.11.19' : {
        'string expression' : 'x1*y1+x2*y2+x3*y3',
        'latex expression'  : r'x1*y1+x2*y2+x3*y3',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda x1,x2,x3,y1,y2,y3:x1*y1+x2*y2+x3*y3)(*args)
    },
    'I.12.1' : {
        'string expression' : 'mu*Nn',
        'latex expression'  : r'mu*Nn',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda mu,Nn:mu*Nn)(*args)
    },
    'I.12.11' : {
        'string expression' : 'q*(Ef+B*v*jnp.sin(theta))',
        'latex expression'  : r'q*{(Ef+B*v*sin{(\theta)})}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda q,Ef,B,v,theta:q*(Ef+B*v*jnp.sin(theta)))(*args)
    },
    'I.12.2' : {
        'string expression' : 'q1*q2*r/(4*pi*epsilon*r**3)',
        'latex expression'  : r'q1*q2*r/{(4*\pi*\epsilon*r^3)}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda q1,q2,epsilon,r:q1*q2*r/(4*pi*epsilon*r**3))(*args)
    },
    'I.12.4' : {
        'string expression' : 'q1*r/(4*pi*epsilon*r**3)',
        'latex expression'  : r'q1*r/{(4*\pi*\epsilon*r^3)}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda q1,epsilon,r:q1*r/(4*pi*epsilon*r**3))(*args)
    },
    'I.12.5' : {
        'string expression' : 'q2*Ef',
        'latex expression'  : r'q2*Ef',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda q2,Ef:q2*Ef)(*args)
    },
    'I.13.12' : {
        'string expression' : 'G*m1*m2*(1/r2-1/r1)',
        'latex expression'  : r'G*m1*m2*{(1/r2-1/r1)}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda m1,m2,r1,r2,G:G*m1*m2*(1/r2-1/r1))(*args)
    },
    'I.13.4' : {
        'string expression' : '1/2*m*(v**2+u**2+w**2)',
        'latex expression'  : r'1/2*m*{(v^2+u^2+w^2)}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda m,v,u,w:1/2*m*(v**2+u**2+w**2))(*args)
    },
    'I.14.3' : {
        'string expression' : 'm*g*z',
        'latex expression'  : r'm*g*z',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda m,g,z:m*g*z)(*args)
    },
    'I.14.4' : {
        'string expression' : '1/2*k_spring*x**2',
        'latex expression'  : r'1/2*k_spring*x^2',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda k_spring,x:1/2*k_spring*x**2)(*args)
    },
    'I.15.10' : {
        'string expression' : 'm_0*v/jnp.sqrt(1-v**2/c**2)',
        'latex expression'  : r'm_0*v/\sqrt{(1-v^2/c^2)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda m_0,v,c:m_0*v/jnp.sqrt(1-v**2/c**2))(*args)
    },
    'I.15.3t' : {
        'string expression' : '(t-u*x/c**2)/jnp.sqrt(1-u**2/c**2)',
        'latex expression'  : r'{(t-u*x/c^2)}/\sqrt{(1-u^2/c^2)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda x,c,u,t:(t-u*x/c**2)/jnp.sqrt(1-u**2/c**2))(*args)
    },
    'I.15.3x' : {
        'string expression' : '(x-u*t)/jnp.sqrt(1-u**2/c**2)',
        'latex expression'  : r'{(x-u*t)}/\sqrt{(1-u^2/c^2)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda x,u,c,t:(x-u*t)/jnp.sqrt(1-u**2/c**2))(*args)
    },
    'I.16.6' : {
        'string expression' : '(u+v)/(1+u*v/c**2)',
        'latex expression'  : r'{(u+v)}/{(1+u*v/c^2)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda c,v,u:(u+v)/(1+u*v/c**2))(*args)
    },
    'I.18.12' : {
        'string expression' : 'r*F*jnp.sin(theta)',
        'latex expression'  : r'r*F*sin{(\theta)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda r,F,theta:r*F*jnp.sin(theta))(*args)
    },
    'I.18.14' : {
        'string expression' : 'm*r*v*jnp.sin(theta)',
        'latex expression'  : r'm*r*v*sin{(\theta)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda m,r,v,theta:m*r*v*jnp.sin(theta))(*args)
    },
    'I.18.4' : {
        'string expression' : '(m1*r1+m2*r2)/(m1+m2)',
        'latex expression'  : r'{(m1*r1+m2*r2)}/{(m1+m2)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda m1,m2,r1,r2:(m1*r1+m2*r2)/(m1+m2))(*args)
    },
    'I.24.6' : {
        'string expression' : '1/2*m*(omega**2+omega_0**2)*1/2*x**2',
        'latex expression'  : r'1/2*m*{(\omega^2+\omega_0^2)}*1/2*x^2',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda m,omega,omega_0,x:1/2*m*(omega**2+omega_0**2)*1/2*x**2)(*args)
    },
    'I.25.13' : {
        'string expression' : 'q/C',
        'latex expression'  : r'q/C',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda q,C:q/C)(*args)
    },
    'I.26.2' : {
        'string expression' : 'jnp.arcsin(n*jnp.sin(theta2))',
        'latex expression'  : r'arcsin{(n*sin{(\theta_2)})}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda n,theta2:jnp.arcsin(n*jnp.sin(theta2)))(*args)
    },
    'I.27.6' : {
        'string expression' : '1/(1/d1+n/d2)',
        'latex expression'  : r'1/{(1/d1+n/d2)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda d1,d2,n:1/(1/d1+n/d2))(*args)
    },
    'I.29.16' : {
        'string expression' : 'jnp.sqrt(x1**2+x2**2-2*x1*x2*jnp.cos(theta1-theta2))',
        'latex expression'  : r'\sqrt{(x1^2+x2^2-2*x1*x2*cos{(\theta_1-\theta_2)})}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda x1,x2,theta1,theta2:jnp.sqrt(x1**2+x2**2-2*x1*x2*jnp.cos(theta1-theta2)))(*args)
    },
    'I.29.4' : {
        'string expression' : 'omega/c',
        'latex expression'  : r'\omega/c',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda omega,c:omega/c)(*args)
    },
    'I.30.3' : {
        'string expression' : 'Int_0*jnp.sin(n*theta/2)**2/jnp.sin(theta/2)**2',
        'latex expression'  : r'Int_0*sin{(n*\theta/2)}^2/sin{(\theta/2)}^2',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda Int_0,theta,n:Int_0*jnp.sin(n*theta/2)**2/jnp.sin(theta/2)**2)(*args)
    },
    'I.30.5' : {
        'string expression' : 'jnp.arcsin(lambd/(n*d))',
        'latex expression'  : r'arcsin{(\lambda/{(n*d)})}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda lambd,d,n:jnp.arcsin(lambd/(n*d)))(*args)
    },
    'I.32.17' : {
        'string expression' : '(1/2*epsilon*c*Ef**2)*(8*pi*r**2/3)*(omega**4/(omega**2-omega_0**2)**2)',
        'latex expression'  : r'{(1/2*\epsilon*c*Ef^2)}*{(8*\pi*r^2/3)}*{(\omega^4/{(\omega^2-\omega_0^2)}^2)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda epsilon,c,Ef,r,omega,omega_0:(1/2*epsilon*c*Ef**2)*(8*pi*r**2/3)*(omega**4/(omega**2-omega_0**2)**2))(*args)
    },
    'I.32.5' : {
        'string expression' : 'q**2*a**2/(6*pi*epsilon*c**3)',
        'latex expression'  : r'q^2*a^2/{(6*\pi*\epsilon*c^3)}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda q,a,epsilon,c:q**2*a**2/(6*pi*epsilon*c**3))(*args)
    },
    'I.34.1' : {
        'string expression' : 'omega_0/(1-v/c)',
        'latex expression'  : r'\omega_0/{(1-v/c)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda c,v,omega_0:omega_0/(1-v/c))(*args)
    },
    'I.34.14' : {
        'string expression' : '(1+v/c)/jnp.sqrt(1-v**2/c**2)*omega_0',
        'latex expression'  : r'{(1+v/c)}/\sqrt{(1-v^2/c^2)}*\omega_0',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda c,v,omega_0:(1+v/c)/jnp.sqrt(1-v**2/c**2)*omega_0)(*args)
    },
    'I.34.27' : {
        'string expression' : '(h/(2*pi))*omega',
        'latex expression'  : r'{(h/{(2*\pi)})}*\omega',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda omega,h:(h/(2*pi))*omega)(*args)
    },
    'I.34.8' : {
        'string expression' : 'q*v*B/p',
        'latex expression'  : r'q*v*B/p',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda q,v,B,p:q*v*B/p)(*args)
    },
    'I.37.4' : {
        'string expression' : 'I1+I2+2*jnp.sqrt(I1*I2)*jnp.cos(delta)',
        'latex expression'  : r'I1+I2+2*\sqrt{(I1*I2)}*cos{(\delta)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda I1,I2,delta:I1+I2+2*jnp.sqrt(I1*I2)*jnp.cos(delta))(*args)
    },
    'I.38.12' : {
        'string expression' : '4*pi*epsilon*(h/(2*pi))**2/(m*q**2)',
        'latex expression'  : r'4*\pi*\epsilon*{(h/{(2*\pi)})}^2/{(m*q^2)}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda m,q,h,epsilon:4*pi*epsilon*(h/(2*pi))**2/(m*q**2))(*args)
    },
    'I.39.1' : {
        'string expression' : '3/2*pr*V',
        'latex expression'  : r'3/2*pr*V',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda pr,V:3/2*pr*V)(*args)
    },
    'I.39.11' : {
        'string expression' : '1/(gamma-1)*pr*V',
        'latex expression'  : r'1/{(\gamma-1)}*pr*V',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda gamma,pr,V:1/(gamma-1)*pr*V)(*args)
    },
    'I.39.22' : {
        'string expression' : 'n*kb*T/V',
        'latex expression'  : r'n*kb*T/V',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda n,T,V,kb:n*kb*T/V)(*args)
    },
    'I.40.1' : {
        'string expression' : 'n_0*jnp.exp(-m*g*x/(kb*T))',
        'latex expression'  : r'n_0*e^{(-m*g*x/{(kb*T)})}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda n_0,m,x,T,g,kb:n_0*jnp.exp(-m*g*x/(kb*T)))(*args)
    },
    'I.41.16' : {
        'string expression' : 'h/(2*pi)*omega**3/(pi**2*c**2*(jnp.exp((h/(2*pi))*omega/(kb*T))-1))',
        'latex expression'  : r'h/{(2*\pi)}*\omega^3/{(\pi^2*c^2*{(e^{({(h/{(2*\pi)})}*\omega/{(kb*T)})}-1)})}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda omega,T,h,kb,c:h/(2*pi)*omega**3/(pi**2*c**2*(jnp.exp((h/(2*pi))*omega/(kb*T))-1)))(*args)
    },
    'I.43.16' : {
        'string expression' : 'mu_drift*q*Volt/d',
        'latex expression'  : r'mu_drift*q*Volt/d',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda mu_drift,q,Volt,d:mu_drift*q*Volt/d)(*args)
    },
    'I.43.31' : {
        'string expression' : 'mob*kb*T',
        'latex expression'  : r'mob*kb*T',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda mob,T,kb:mob*kb*T)(*args)
    },
    'I.43.43' : {
        'string expression' : '1/(gamma-1)*kb*v/A',
        'latex expression'  : r'1/{(\gamma-1)}*kb*v/A',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda gamma,kb,A,v:1/(gamma-1)*kb*v/A)(*args)
    },
    'I.44.4' : {
        'string expression' : 'n*kb*T*jnp.log(V2/V1)',
        'latex expression'  : r'n*kb*T*log{(V2/V1)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda n,kb,T,V1,V2:n*kb*T*jnp.log(V2/V1))(*args)
    },
    'I.47.23' : {
        'string expression' : 'jnp.sqrt(gamma*pr/rho)',
        'latex expression'  : r'\sqrt{(\gamma*pr/\rho)}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda gamma,pr,rho:jnp.sqrt(gamma*pr/rho))(*args)
    },
    'I.48.20' : {
        'string expression' : 'm*c**2/jnp.sqrt(1-v**2/c**2)',
        'latex expression'  : r'm*c^2/\sqrt{(1-v^2/c^2)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda m,v,c:m*c**2/jnp.sqrt(1-v**2/c**2))(*args)
    },
    'I.50.26' : {
        'string expression' : 'x1*(jnp.cos(omega*t)+alpha*jnp.cos(omega*t)**2)',
        'latex expression'  : r'x1*{(cos{(\omega*t)}+\alpha*cos{(\omega*t)}^2)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda x1,omega,t,alpha:x1*(jnp.cos(omega*t)+alpha*jnp.cos(omega*t)**2))(*args)
    },
    'I.6.2' : {
        'string expression' : 'jnp.exp(-(theta/sigma)**2/2)/(jnp.sqrt(2*pi)*sigma)',
        'latex expression'  : r'e^{(-{(\theta/\sigma)}^2/2)}/{(\sqrt{(2*\pi)}*\sigma)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda sigma,theta:jnp.exp(-(theta/sigma)**2/2)/(jnp.sqrt(2*pi)*sigma))(*args)
    },
    'I.6.2a' : {
        'string expression' : 'jnp.exp(-theta**2/2)/jnp.sqrt(2*pi)',
        'latex expression'  : r'e^{(-\theta^2/2)}/\sqrt{(2*\pi)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda theta:jnp.exp(-theta**2/2)/jnp.sqrt(2*pi))(*args)
    },
    'I.6.2b' : {
        'string expression' : 'jnp.exp(-((theta-theta1)/sigma)**2/2)/(jnp.sqrt(2*pi)*sigma)',
        'latex expression'  : r'e^{(-{({(\theta-\theta_1)}/\sigma)}^2/2)}/{(\sqrt{(2*\pi)}*\sigma)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda sigma,theta,theta1:jnp.exp(-((theta-theta1)/sigma)**2/2)/(jnp.sqrt(2*pi)*sigma))(*args)
    },
    'I.8.14' : {
        'string expression' : 'jnp.sqrt((x2-x1)**2+(y2-y1)**2)',
        'latex expression'  : r'\sqrt{({(x2-x1)}^2+{(y2-y1)}^2)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda x1,x2,y1,y2:jnp.sqrt((x2-x1)**2+(y2-y1)**2))(*args)
    },
    'I.9.18' : {
        'string expression' : 'G*m1*m2/((x2-x1)**2+(y2-y1)**2+(z2-z1)**2)',
        'latex expression'  : r'G*m1*m2/{({(x2-x1)}^2+{(y2-y1)}^2+{(z2-z1)}^2)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda m1,m2,G,x1,x2,y1,y2,z1,z2:G*m1*m2/((x2-x1)**2+(y2-y1)**2+(z2-z1)**2))(*args)
    },
    'II.10.9' : {
        'string expression' : 'sigma_den/epsilon*1/(1+chi)',
        'latex expression'  : r'\sigma_den/\epsilon*1/{(1+chi)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda sigma_den,epsilon,chi:sigma_den/epsilon*1/(1+chi))(*args)
    },
    'II.11.17' : {
        'string expression' : 'n_0*(1+p_d*Ef*jnp.cos(theta)/(kb*T))',
        'latex expression'  : r'n_0*{(1+p_d*Ef*cos{(\theta)}/{(kb*T)})}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda n_0,kb,T,theta,p_d,Ef:n_0*(1+p_d*Ef*jnp.cos(theta)/(kb*T)))(*args)
    },
    'II.11.20' : {
        'string expression' : 'n_rho*p_d**2*Ef/(3*kb*T)',
        'latex expression'  : r'n_\rho*p_d^2*Ef/{(3*kb*T)}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda n_rho,p_d,Ef,kb,T:n_rho*p_d**2*Ef/(3*kb*T))(*args)
    },
    'II.11.27' : {
        'string expression' : 'n*alpha/(1-(n*alpha/3))*epsilon*Ef',
        'latex expression'  : r'n*\alpha/{(1-{(n*\alpha/3)})}*\epsilon*Ef',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda n,alpha,epsilon,Ef:n*alpha/(1-(n*alpha/3))*epsilon*Ef)(*args)
    },
    'II.11.28' : {
        'string expression' : '1+n*alpha/(1-(n*alpha/3))',
        'latex expression'  : r'1+n*\alpha/{(1-{(n*\alpha/3)})}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda n,alpha:1+n*alpha/(1-(n*alpha/3)))(*args)
    },
    'II.11.3' : {
        'string expression' : 'q*Ef/(m*(omega_0**2-omega**2))',
        'latex expression'  : r'q*Ef/{(m*{(\omega_0^2-\omega^2)})}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda q,Ef,m,omega_0,omega:q*Ef/(m*(omega_0**2-omega**2)))(*args)
    },
    'II.13.17' : {
        'string expression' : '1/(4*pi*epsilon*c**2)*2*I/r',
        'latex expression'  : r'1/{(4*\pi*\epsilon*c^2)}*2*I/r',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda epsilon,c,I,r:1/(4*pi*epsilon*c**2)*2*I/r)(*args)
    },
    'II.13.23' : {
        'string expression' : 'rho_c_0/jnp.sqrt(1-v**2/c**2)',
        'latex expression'  : r'\rho_{c_0}/\sqrt{(1-v^2/c^2)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda rho_c_0,v,c:rho_c_0/jnp.sqrt(1-v**2/c**2))(*args)
    },
    'II.13.34' : {
        'string expression' : 'rho_c_0*v/jnp.sqrt(1-v**2/c**2)',
        'latex expression'  : r'\rho_{c_0}*v/\sqrt{(1-v^2/c^2)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda rho_c_0,v,c:rho_c_0*v/jnp.sqrt(1-v**2/c**2))(*args)
    },
    'II.15.4' : {
        'string expression' : '-mom*B*jnp.cos(theta)',
        'latex expression'  : r'-mom*B*cos{(\theta)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda mom,B,theta:-mom*B*jnp.cos(theta))(*args)
    },
    'II.15.5' : {
        'string expression' : '-p_d*Ef*jnp.cos(theta)',
        'latex expression'  : r'-p_d*Ef*cos{(\theta)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda p_d,Ef,theta:-p_d*Ef*jnp.cos(theta))(*args)
    },
    'II.2.42' : {
        'string expression' : 'kappa*(T2-T1)*A/d',
        'latex expression'  : r'kappa*{(T2-T1)}*A/d',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda kappa,T1,T2,A,d:kappa*(T2-T1)*A/d)(*args)
    },
    'II.21.32' : {
        'string expression' : 'q/(4*pi*epsilon*r*(1-v/c))',
        'latex expression'  : r'q/{(4*\pi*\epsilon*r*{(1-v/c)})}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda q,epsilon,r,v,c:q/(4*pi*epsilon*r*(1-v/c)))(*args)
    },
    'II.24.17' : {
        'string expression' : 'jnp.sqrt(omega**2/c**2-pi**2/d**2)',
        'latex expression'  : r'\sqrt{(\omega^2/c^2-\pi^2/d^2)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda omega,c,d:jnp.sqrt(omega**2/c**2-pi**2/d**2))(*args)
    },
    'II.27.16' : {
        'string expression' : 'epsilon*c*Ef**2',
        'latex expression'  : r'\epsilon*c*Ef^2',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda epsilon,c,Ef:epsilon*c*Ef**2)(*args)
    },
    'II.27.18' : {
        'string expression' : 'epsilon*Ef**2',
        'latex expression'  : r'\epsilon*Ef^2',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda epsilon,Ef:epsilon*Ef**2)(*args)
    },
    'II.3.24' : {
        'string expression' : 'Pwr/(4*pi*r**2)',
        'latex expression'  : r'Pwr/{(4*\pi*r^2)}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda Pwr,r:Pwr/(4*pi*r**2))(*args)
    },
    'II.34.11' : {
        'string expression' : 'g_*q*B/(2*m)',
        'latex expression'  : r'g_*q*B/{(2*m)}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda g_,q,B,m:g_*q*B/(2*m))(*args)
    },
    'II.34.2' : {
        'string expression' : 'q*v*r/2',
        'latex expression'  : r'q*v*r/2',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda q,v,r:q*v*r/2)(*args)
    },
    'II.34.29a' : {
        'string expression' : 'q*h/(4*pi*m)',
        'latex expression'  : r'q*h/{(4*\pi*m)}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda q,h,m:q*h/(4*pi*m))(*args)
    },
    'II.34.29b' : {
        'string expression' : 'g_*mom*B*Jz/(h/(2*pi))',
        'latex expression'  : r'g_*mom*B*Jz/{(h/{(2*\pi)})}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda g_,h,Jz,mom,B:g_*mom*B*Jz/(h/(2*pi)))(*args)
    },
    'II.34.2a' : {
        'string expression' : 'q*v/(2*pi*r)',
        'latex expression'  : r'q*v/{(2*\pi*r)}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda q,v,r:q*v/(2*pi*r))(*args)
    },
    'II.35.18' : {
        'string expression' : 'n_0/(jnp.exp(mom*B/(kb*T))+jnp.exp(-mom*B/(kb*T)))',
        'latex expression'  : r'n_0/{(e^{(mom*B/{(kb*T)})}+e^{(-mom*B/{(kb*T)})})}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda n_0,kb,T,mom,B:n_0/(jnp.exp(mom*B/(kb*T))+jnp.exp(-mom*B/(kb*T))))(*args)
    },
    'II.35.21' : {
        'string expression' : 'n_rho*mom*jnp.tanh(mom*B/(kb*T))',
        'latex expression'  : r'n_\rho*mom*tanh{(mom*B/{(kb*T)})}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda n_rho,mom,B,kb,T:n_rho*mom*jnp.tanh(mom*B/(kb*T)))(*args)
    },
    'II.36.38' : {
        'string expression' : 'mom*H/(kb*T)+(mom*alpha)/(epsilon*c**2*kb*T)*M',
        'latex expression'  : r'mom*H/{(kb*T)}+{(mom*\alpha)}/{(\epsilon*c^2*kb*T)}*M',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda mom,H,kb,T,alpha,epsilon,c,M:mom*H/(kb*T)+(mom*alpha)/(epsilon*c**2*kb*T)*M)(*args)
    },
    'II.37.1' : {
        'string expression' : 'mom*(1+chi)*B',
        'latex expression'  : r'mom*{(1+chi)}*B',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda mom,B,chi:mom*(1+chi)*B)(*args)
    },
    'II.38.14' : {
        'string expression' : 'Y/(2*(1+sigma))',
        'latex expression'  : r'Y/{(2*{(1+\sigma)})}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda Y,sigma:Y/(2*(1+sigma)))(*args)
    },
    'II.38.3' : {
        'string expression' : 'Y*A*x/d',
        'latex expression'  : r'Y*A*x/d',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda Y,A,d,x:Y*A*x/d)(*args)
    },
    'II.4.23' : {
        'string expression' : 'q/(4*pi*epsilon*r)',
        'latex expression'  : r'q/{(4*\pi*\epsilon*r)}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda q,epsilon,r:q/(4*pi*epsilon*r))(*args)
    },
    'II.6.11' : {
        'string expression' : '1/(4*pi*epsilon)*p_d*jnp.cos(theta)/r**2',
        'latex expression'  : r'1/{(4*\pi*\epsilon)}*p_d*cos{(\theta)}/r^2',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda epsilon,p_d,theta,r:1/(4*pi*epsilon)*p_d*jnp.cos(theta)/r**2)(*args)
    },
    'II.6.15a' : {
        'string expression' : 'p_d/(4*pi*epsilon)*3*z/r**5*jnp.sqrt(x**2+y**2)',
        'latex expression'  : r'p_d/{(4*\pi*\epsilon)}*3*z/r^5*\sqrt{(x^2+y^2)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda epsilon,p_d,r,x,y,z:p_d/(4*pi*epsilon)*3*z/r**5*jnp.sqrt(x**2+y**2))(*args)
    },
    'II.6.15b' : {
        'string expression' : 'p_d/(4*pi*epsilon)*3*jnp.cos(theta)*jnp.sin(theta)/r**3',
        'latex expression'  : r'p_d/{(4*\pi*\epsilon)}*3*cos{(\theta)}*sin{(\theta)}/r^3',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda epsilon,p_d,theta,r:p_d/(4*pi*epsilon)*3*jnp.cos(theta)*jnp.sin(theta)/r**3)(*args)
    },
    'II.8.31' : {
        'string expression' : 'epsilon*Ef**2/2',
        'latex expression'  : r'\epsilon*Ef^2/2',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda epsilon,Ef:epsilon*Ef**2/2)(*args)
    },
    'II.8.7' : {
        'string expression' : '3/5*q**2/(4*pi*epsilon*d)',
        'latex expression'  : r'3/5*q^2/{(4*\pi*\epsilon*d)}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda q,epsilon,d:3/5*q**2/(4*pi*epsilon*d))(*args)
    },
    'III.10.19' : {
        'string expression' : 'mom*jnp.sqrt(Bx**2+By**2+Bz**2)',
        'latex expression'  : r'mom*\sqrt{(Bx^2+By^2+Bz^2)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda mom,Bx,By,Bz:mom*jnp.sqrt(Bx**2+By**2+Bz**2))(*args)
    },
    'III.12.43' : {
        'string expression' : 'n*(h/(2*pi))',
        'latex expression'  : r'n*{(h/{(2*\pi)})}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda n,h:n*(h/(2*pi)))(*args)
    },
    'III.13.18' : {
        'string expression' : '2*E_n*d**2*k/(h/(2*pi))',
        'latex expression'  : r'2*E_n*d^2*k/{(h/{(2*\pi)})}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda E_n,d,k,h:2*E_n*d**2*k/(h/(2*pi)))(*args)
    },
    'III.14.14' : {
        'string expression' : 'I_0*(jnp.exp(q*Volt/(kb*T))-1)',
        'latex expression'  : r'I_0*{(e^{(q*Volt/{(kb*T)})}-1)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda I_0,q,Volt,kb,T:I_0*(jnp.exp(q*Volt/(kb*T))-1))(*args)
    },
    'III.15.12' : {
        'string expression' : '2*U*(1-jnp.cos(k*d))',
        'latex expression'  : r'2*U*{(1-cos{(k*d)})}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda U,k,d:2*U*(1-jnp.cos(k*d)))(*args)
    },
    'III.15.14' : {
        'string expression' : '(h/(2*pi))**2/(2*E_n*d**2)',
        'latex expression'  : r'{(h/{(2*\pi)})}^2/{(2*E_n*d^2)}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda h,E_n,d:(h/(2*pi))**2/(2*E_n*d**2))(*args)
    },
    'III.15.27' : {
        'string expression' : '2*pi*alpha/(n*d)',
        'latex expression'  : r'2*\pi*\alpha/{(n*d)}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda alpha,n,d:2*pi*alpha/(n*d))(*args)
    },
    'III.17.37' : {
        'string expression' : 'beta*(1+alpha*jnp.cos(theta))',
        'latex expression'  : r'beta*{(1+\alpha*cos{(\theta)})}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda beta,alpha,theta:beta*(1+alpha*jnp.cos(theta)))(*args)
    },
    'III.19.51' : {
        'string expression' : '-m*q**4/(2*(4*pi*epsilon)**2*(h/(2*pi))**2)*(1/n**2)',
        'latex expression'  : r'-m*q^4/{(2*{(4*\pi*\epsilon)}^2*{(h/{(2*\pi)})}^2)}*{(1/n^2)}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda m,q,h,n,epsilon:-m*q**4/(2*(4*pi*epsilon)**2*(h/(2*pi))**2)*(1/n**2))(*args)
    },
    'III.21.20' : {
        'string expression' : '-rho_c_0*q*A_vec/m',
        'latex expression'  : r'-\rho_{c_0}*q*A_vec/m',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda rho_c_0,q,A_vec,m:-rho_c_0*q*A_vec/m)(*args)
    },
    'III.4.32' : {
        'string expression' : '1/(jnp.exp((h/(2*pi))*omega/(kb*T))-1)',
        'latex expression'  : r'1/{(e^{({(h/{(2*\pi)})}*\omega/{(kb*T)})}-1)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda h,omega,kb,T:1/(jnp.exp((h/(2*pi))*omega/(kb*T))-1))(*args)
    },
    'III.4.33' : {
        'string expression' : '(h/(2*pi))*omega/(jnp.exp((h/(2*pi))*omega/(kb*T))-1)',
        'latex expression'  : r'{(h/{(2*\pi)})}*\omega/{(e^{({(h/{(2*\pi)})}*\omega/{(kb*T)})}-1)}',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda h,omega,kb,T:(h/(2*pi))*omega/(jnp.exp((h/(2*pi))*omega/(kb*T))-1))(*args)
    },
    'III.7.38' : {
        'string expression' : '2*mom*B/(h/(2*pi))',
        'latex expression'  : r'2*mom*B/{(h/{(2*\pi)})}',
        'expressible by IT' : True,
        'python function'   : lambda args : (lambda mom,B,h:2*mom*B/(h/(2*pi)))(*args)
    },
    'III.8.54' : {
        'string expression' : 'jnp.sin(E_n*t/(h/(2*pi)))**2',
        'latex expression'  : r'sin{(E_n*t/{(h/{(2*\pi)})})}^2',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda E_n,t,h:jnp.sin(E_n*t/(h/(2*pi)))**2)(*args)
    },
    'III.9.52' : {
        'string expression' : '(p_d*Ef*t/(h/(2*pi)))*jnp.sin((omega-omega_0)*t/2)**2/((omega-omega_0)*t/2)**2',
        'latex expression'  : r'(p_d*Ef*t/(h/(2*\pi)))*sin((\omega-\omega_0)*t/2)^2/((\omega-\omega_0)*t/2)^2',
        'expressible by IT' : False,
        'python function'   : lambda args : (lambda p_d,Ef,t,h,omega,omega_0:(p_d*Ef*t/(h/(2*pi)))*jnp.sin((omega-omega_0)*t/2)**2/((omega-omega_0)*t/2)**2)(*args)
    }
}


import numpy as np

def mean_squared_error(y_true, y_pred):
    """ Returns the mean squared error between y_true and y_pred """
    mse = np.mean(np.power(y_true - y_pred, 2))
    return mse

def shuffle_data(X, y, seed=None):
    """ Random shuffle of the samples in X and y """
    if seed:
        np.random.seed(seed)
    idx = np.arange(X.shape[0])
    np.random.shuffle(idx)
    return X[idx], y[idx]

def train_test_split(X, y, test_size=0.5, shuffle=True, seed=None):
    """ Split the data into train and test sets """
    if shuffle:
        X, y = shuffle_data(X, y, seed)
    # Split the training data from test data in the ratio of test_size
    split_i = len(y) - int(len(y) // (1 / test_size))
    X_train, X_test = X[:split_i], X[split_i:]
    y_train, y_test = y[:split_i], y[split_i:]

    return X_train, X_test, y_train, y_test

def accuracy_score(y_true, y_pred):
    """ Compare y_true to y_pred and return the accuracy """
    accuracy = np.sum(y_true == y_pred, axis=0) / len(y_true)
    return accuracy

def make_diagonal(x):
    """ Converts a vector into an diagonal matrix """
    m = np.zeros((len(x), len(x)))
    for i in range(len(m[0])):
        m[i, i] = x[i]
    return m

def standardize(X):
    """ Standardize the dataset X """
    return (X - X.mean(axis=0)) / X.std(axis=0)

def normalize(X, axis=-1, order=2):
    """ Normalize the dataset X """
    l2 = np.atleast_1d(np.linalg.norm(X, order, axis))
    l2[l2 == 0] = 1
    return X / np.expand_dims(l2, axis)

def euclidean_distance(x1, x2):
    """ Calculates the l2 distance between two vectors """
    return np.sqrt(np.sum((x1.reshape(1, x2.shape[1])-x2)**2, axis=1))

def calculate_variance(X):
    """ Return the variance of the features in dataset X """
    mean = np.ones(np.shape(X)) * X.mean(0)
    n_samples = np.shape(X)[0]
    variance = (1 / n_samples) * np.diag((X - mean).T.dot(X - mean))

    return variance

def calculate_std_dev(X):
    """ Calculate the standard deviations of the features in dataset X """
    std_dev = np.sqrt(calculate_variance(X))
    return std_dev

def calculate_covariance_matrix(X, Y=None):
    """ Calculate the covariance matrix for the dataset X """
    if Y is None:
        Y = X
    n_samples = np.shape(X)[0]
    covariance_matrix = (1 / (n_samples-1)) * (X - X.mean(axis=0)).T.dot(Y - Y.mean(axis=0))

    return np.array(covariance_matrix, dtype=float)

def calculate_correlation_matrix(X, Y=None):
    """ Calculate the correlation matrix for the dataset X """
    if Y is None:
        Y = X
    n_samples = np.shape(X)[0]
    covariance = (1 / n_samples) * (X - X.mean(0)).T.dot(Y - Y.mean(0))
    std_dev_X = np.expand_dims(calculate_std_dev(X), 1)
    std_dev_y = np.expand_dims(calculate_std_dev(Y), 1)
    correlation_matrix = np.divide(covariance, std_dev_X.dot(std_dev_y.T))

    return np.array(correlation_matrix, dtype=float)

def calculate_entropy(y):
    value, counts = np.unique(y, return_counts=True)
    norm_counts = counts / counts.sum()
    return - (norm_counts * np.log(norm_counts)).sum()

def get_random_subsets(X, y, n_subsets, replacements=True):
    """ Return random subsets (with replacements) of the data """
    n_samples = np.shape(X)[0]
    # Concatenate x and y and do a random shuffle
    X_y = np.concatenate((X, y.reshape((1, len(y))).T), axis=1)
    np.random.shuffle(X_y)
    subsets = []

    # Uses 50% of training samples without replacements
    subsample_size = int(n_samples // 2)
    if replacements:
        subsample_size = n_samples      # 100% with replacements

    for _ in range(n_subsets):
        idx = np.random.choice(
            range(n_samples),
            size=np.shape(range(subsample_size)),
            replace=replacements)
        X = X_y[idx][:, :-1]
        y = X_y[idx][:, -1]
        subsets.append([X, y])
    return subsets


"""This module contains the algorithms for the linear equations lab.

The materials follow Miranda and Fackler (2004, :cite:`miranda2004applied`) (Chapter 2).
The python code heavily draws on Romero-Aguilar (2020, :cite:`CompEcon`) and
Foster (2019, :cite:`foster2019`).

"""
import numpy as np

eps = np.sqrt(np.spacing(1.0))

def forward_substitution(a, b):
    """Perform forward substitution to solve a system of linear equations.

    Solves a linear equation of type :math:`Ax = b` when for a *lower triangular* matrix
    :math:`A` of dimension :math:`n \\times n` and vector :math:`b` of length :math:`n`.
    The forward subsititution algorithm can be represented as:

    .. math::

       x_i = \\left ( b_i - \\sum_{j=1}^{i-1} a_{ij}x_j \\right )/a_{ii}

    Parameters
    ----------
    a : numpy.ndarray
        Lower triangular matrix of dimension :math:`n \\times n`.
    b : numpy.ndarray
        Vector of length :math:`n`.

    Returns
    -------
    x : numpy.ndarray
        Solution of the linear equations. Vector of length :math:`n`.

    """
    # Test that only lower-triangular matrix passed in.
    msg = "... function only intended for use with lower triangular matrix"
    np.testing.assert_allclose(a, np.tril(a), err_msg=msg)

    # Get number of rows
    n = a.shape[0]

    # Allocating space for the solution vector
    x = np.zeros_like(b, dtype=np.double)

    # Here we perform the forward-substitution.
    # Initializing  with the first row.
    x[0] = b[0] / a[0, 0]

    # Looping over remaining rows.
    for i in range(1, n):
        x[i] = (b[i] - np.dot(a[i, :i], x[:i])) / a[i, i]

    return x

def backward_substitution(a, b):
    """Perform backward substitution to solve a system of linear equations.

    Solves a linear equation of type :math:`Ax = b` when for an *upper triangular* matrix
    :math:`A` of dimension :math:`n \\times n` and vector :math:`b` of length :math:`n`.

    Parameters
    ----------
    a : numpy.ndarray
        Lower triangular matrix of dimension :math:`n \\times n`.
    b : numpy.ndarray
        Vector of length :math:`n`.

    Returns
    -------
    x : numpy.ndarray
        Solution of the linear equations. Vector of length :math:`n`.

    """
    # Test that only uppper triangular matrix passed in.
    msg = "... function only intended for use with upper triangular matrix"
    np.testing.assert_allclose(a, np.triu(a), err_msg=msg)

    # Get number of rows.
    n = a.shape[0]

    x = np.zeros(n)

    for i in range(n - 1, -1, -1):
        tmp = b[i]
        for j in range(n - 1, i, -1):
            tmp -= x[j] * a[i, j]

        x[i] = tmp / a[i, i]

    return x

def solve(a, b):
    """Solve linear equations using L-U factorization.

    Solves a linear equation of type :math:`Ax = b` when for a nonsingular square matrix
    :math:`A` of dimension :math:`n \\times n` and vector :math:`b` of length :math:`n`. Decomposes
    Algorithm decomposes matrix :math:`A` into the product of lower and upper triangular matrices.
    The linear equations can then be solved using a combination of forward and backward
    substitution.

    Two stages of the L-U algorithm:

    1. Factorization using Gaussian elimination: :math:`A=LU` where :math:`L` denotes
    a row-permuted lower triangular matrix. :math:`U` denotes a row-permuted upper
    triangular matrix.

    2. Solution using forward and backward substitution. The factored linear equation of step 1 can
    be expressed as

    .. math::

       Ax = (LU)x = L(Ux) = b

    The forward substitution algorithm solves :math:`Ly = b` for y. The backward substitution
    algorithm then solves :math:`Ux = y` for :math:`x`.

    Parameters
    ----------
    a : numpy.ndarray
        Matrix of dimension :math:`n \\times n`
    b : numpy.ndarray
        Vector of length :math:`n`.

    Returns
    -------
    x : numpy.ndarray
        Solution of the linear equations. Vector of length :math:`n`.

    Example
    -------
    >>> b = np.array([1, 2, 3])
    >>> a = np.array([[4, 0, 0], [0, 2, 0], [0, 0, 2]])
    >>> solve(a, b)
    array([0.25, 1.  , 1.5 ])

    """
    # Step 1: Factorization using a naive implementation of the LU decomposition.
    l, u = naive_lu(a)

    # Step 2: Solution using forward and backward substitution.
    y = forward_substitution(l, b)
    x = backward_substitution(u, y)

    return x

def gauss_seidel(a, b, x0=None, lambda_=1.0, max_iterations=1000, tolerance=eps):
    """Solves linear equation of type :math:`Ax = b` using Gauss-Seidel iterations.

    In the linear equation, :math:`A` denotes a matrix of dimension
    :math:`n \\times n` and :math:`b` denotes a vector of length :math:`n` The solution
    method performs especially well for larger linear equations if matrix :math`A`is
    sparse. The method achieves fairly precise approximations to the solution but
    generally does not produce *exact* solutions.

    Following the notation in Miranda and Fackler (2004, :cite:`miranda2004applied`),
    the linear equations problem can be written as

    .. math::

       Qx = b + (Q -A)x \\Rightarrow x = Q^{-1}b + (I - Q^{-1}A)x

    which suggest the iteration rule

    .. math::

       x^{(k+1)} \\leftarrow Q^{-1}b + (I - Q^{-1}A)x^{(k)}

    which, if convergent, must converge to a solution of the linear equation. For the Gauss-Seidel
    method, :math:`Q` is the upper triangular matrix formed from the upper triangular elements of
    :math:`A`.

    Parameters
    ----------
    a : numpy.ndarray
        Matrix of dimension :math:`n \\times n`
    b : numpy.ndarray
        Vector of length :math:`n`.
    x0 : numpy.ndarray, default None
        Array of starting values. Set to be if None.
    lambda_ : float
        Over-relaxation parameter which may accelerate convergence of the algorithm
        for :math:`1 < \\lambda < 2`.
    max_iterations : int
        Maximum number of iterations.
    tolerance : float
        Convergence tolerance.

    Returns
    -------
    x : numpy.ndarray
        Solution of the linear equations. Vector of length :math:`n`.

    Raises
    ------
    StopIteration
        If maximum number of iterations specified by `max_iterations` is reached.

    """
    if x0 is None:
        x = b.copy()
    else:
        x = x0

    q = np.tril(a)
    for _ in range(max_iterations):
        dx = np.linalg.solve(q, b - a @ x)
        x += lambda_ * dx

        if np.linalg.norm(dx) < tolerance:
            return x

    raise StopIteration

def naive_lu(a):
    """Apply a naive LU factorization.

    LU factorization decomposes a matrix :math:`A` into a lower triangular matrix :math:`L`
    and upper triangular matrix `U`. The naive LU factorization does not apply permutations to
    the resulting matrices and thus only works reliably for diagonal matrices :math:`A`:

    Parameters
    ----------
    a : numpy.ndarray
        Diagonal square matrix.

    Returns
    -------
    l : numpy.ndarray
    u : numpy.ndarray

    """
    n = a.shape[0]
    u = a.copy()
    l = np.eye(n)
    for j in range(n - 1):
        lam = np.eye(n)
        gamma = u[j + 1 :, j] / u[j, j]
        lam[j + 1 :, j] = -gamma
        u = lam @ u

        lam[j + 1 :, j] = gamma
        l = l @ lam

    return l, u


from math import sqrt
import numpy as np

# nice trick numpy
float_formatter = lambda x: "%.2f" % x
np.set_printoptions(formatter={'float_kind':float_formatter})

def euclidean_distance(a, b):
    """
    Calculate the Euclidean distance of two vectors
    """
    distance = 0
    for x, y in zip(a, b):
        distance += (x - y) ** 2

    return sqrt(distance)

def get_distance_matrix(points):
    """
    From a group of points calculate the distance matrix between them
    """
    symbols = "ABCDEFGHIJKLMNOP"

    w = h = len(points)
    distance_matrix = np.ndarray(shape=(w, h))

    print ' ' * 5,
    for x in range(len(points)):
        print ("A%i" % (x+1)).ljust(5, ' '),
    print

    for x, p in enumerate(points):
        print ("A%i" % (x+1)).ljust(5, ' '),
        for y, q in enumerate(points):
            d = euclidean_distance(p, q)
            print ("%.2f" % d).ljust(5, ' '),
            distance_matrix[x, y] = d
        print

    return distance_matrix

points = [
            [0, 10], [2, 2], [2, 4],
            [8, 8], [7, 6], [10, 9]
        ]

points = [
            [2, 10], [5, 8], [1, 2], [2, 5],
            [8, 4], [7, 5], [6, 4], [4, 9]
        ]

distance_matrix = get_distance_matrix(points)


from numpy.linalg import matrix_power
import numpy as np

def snakes_and_ladders_matrix():
    n = 101
    T = np.zeros((n, n))
    for i in range(n - 1):
        for j in range(i + 1, np.min([n, i + 7])):
            T[i, j] = 1 / 6
    for i in range(n - 6, n):
        T[i, i] = (i - n + 7) / 6

    snakes_and_ladders = np.array([
        [98, 78],  [95, 75],  [93, 73],  [87, 24], [64, 60],
        [62, 19],  [56, 53],  [49, 11], [47, 26],  [16, 6],
        [1, 38],  [4, 14], [9, 31],  [21, 42], [28, 84],
        [36, 44], [51, 67],  [71, 91], [80, 100]])

    for src, dst in snakes_and_ladders:
        b = T[:, src].copy()
        T[:, dst] += b
        T[:, src] = 0
    return T

def main():
    T = snakes_and_ladders_matrix()
    Q = T[:-1, :-1]
    I = np.identity(100)
    o = np.ones((100, 1))

    # method 1
    result = np.linalg.solve(I - Q, o)
    print("via solve", result[0, 0])

    # method 2
    N = np.linalg.inv(I - Q)
    sum = np.sum(N, axis=1)
    print("via inver", sum[0])

    # method 3
    Napprx = np.zeros((100, 100))
    for k in range(1000):
        Napprx += matrix_power(Q, k)
    sumapprx = np.sum(Napprx, axis=1)
    print("via apprx", sumapprx[0])

main()


import numpy as np
np.set_printoptions(threshold=np.inf)
import matplotlib.pyplot as plt

def Building_mat(n_x,n_y):
    A = np.zeros((n_x**2,n_y**2),dtype=float)

    for i in range(0, n_x*n_y):
            if (i < n_x): # bottom
                A[i,i] = 1
            elif (i % n_x==0): # left
                A[i,i] = 1
            elif (i % n_x==n_x-1): # right
                A[i,i] = 1
            elif (i >= A.shape[0]-n_x): # top
                A[i,i] = 1
            else:
                A[i,i-n_x] = 1
                A[i,i-1] = 1
                A[i,i] = -4
                A[i,i+1] = 1
                A[i,i+n_x] = 1
    return A

n = 5 #number of grids
L = 1
h = L/(n-1)
x = np.linspace(0,L,n)
y = np.linspace(0,L,n)

rho = np.zeros((n**2,1),dtype=float)
lam = 1
rho[int((n**2-1)/2),0] = lam/h**2
print(rho.shape)
A = Building_mat(n,n)
V = np.matmul(np.linalg.inv(A),-h**2*rho)
# print(rho)
print(V.shape)

plt.figure(figsize=(8,5))
plt.plot(np.linspace(0,n,n),V.reshape(n,n)[int((n-1)/2),::],marker='o',markersize=12,linestyle = '-',linewidth = 3,label='numerical')
# plt.plot(np.linspace(0,n,n),V.reshape(n,n)[int((n-1)/2),::],lw=3,color='r')
plt.legend(loc='best')
plt.show()


import numpy as np

class LossFun:
    def MSE(self,ts,ys):
        batch_size = ys.shape[0]
        if ys.ndim == 1:
            ts = ts.reshape(1,ts.size)
            ys = ys.reshape(1,ys.size)
        return 1/2*(np.sum((ys - ts)**2))  / batch_size

    def CEE(self,ts,ys):
        batch_size = ys.shape[0]
        if ys.ndim == 1:
            ts = ts.reshape(1,ts.size)
            ys = ys.reshape(1,ys.size)
        return -np.sum(ts*np.log(ys[np.arange(batch_size) , ts] + 1e-7) ) / batch_size

if __name__ == "__main__":
    y =  [[0.1 , 0.05 , 2.1 ] , [0.1 , 0.05 , 2.1 ] , [0.1 , 0.05 , 2.1 ] ]
    test = [[0,0,1],[0,1,0],[1,0,0]]
    test1 = [[3,2,1]]
    t1 = np.array(test)
    t2 = np.array(test1)

    #하고 싶은건  e1*log(0.1) + e2*log(0.05) + e3*log(2.1)
    # test1 은 [3,2,1]이다 어떻게?

    onehot = y[test1[0] ]

    loss = LossFun()
    y =  [[0.1 , 0.05 , 2.1 , 2.7] , [0.1 , 0.05 , 2.1 , 2.7] , [0.1 , 0.05 , 2.1 , 2.7] , [0.1 , 0.05 , 2.1 , 2.7]]
    t1 = [[0 , 1 , 0 , 0],[0 , 1 , 0 , 0],[0 , 1 , 0 , 0],[0 , 1 , 0 , 0]]
    t2 = [[0 , 0 , 1 , 0],[0 , 0 , 1 , 0],[0 , 0 , 1 , 0],[0 , 0 , 1 , 0]]

    yd1 = np.array([0.1 , 0.05 , 2.1 , 2.7])
    td1 = np.array([0,1,0,0]               )
    print( loss.CEE(td1,yd1))

    print( "MSE t1 " + str(loss.MSE(np.array(t1) , np.array(y))) )
    print( "MSE t2 " + str(loss.MSE(np.array(t2) , np.array(y))) )
    print( "CEE t1 " + str(loss.CEE(np.array(t1) , np.array(y))) )
    print( "CEE t2 " + str(loss.CEE(np.array(t2) , np.array(y))) )


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ch4Python.py Statistical significance of mean.

import numpy as np
from scipy import stats

y = [3.34,4.97,4.15,5.40,5.21,4.56,3.69,5.86,4.58,6.94,5.57,5.62,6.87]

# Convert daa to vector.
y = np.array(y)
n = len(y)

# Find zero mean version of y.
ymean = np.mean(y)
yzm = y-ymean
varsampley = np.sum(yzm * yzm)/n
print("Sample variance of y = %0.3f." %varsampley) # 1.095

# Unbiased estimate of parent population variance (divide by n-1).
varpopy = np.sum(yzm * yzm)/(n-1)
print("Est of pop variance of y = %0.3f." %varpopy) # 1.187

# Standard error of ymean.
semymean = np.sqrt(varpopy/float(n)) # 0.302
print('Standard error of ymean = %.3f.\n' %  semymean)

# Find t value.
tval = ymean/semymean # 16.997

# One-tailed p-value = Prob(t>tval).
pval = stats.t.sf(np.abs(tval), n-1) # 9.2343e-10.

print('RESULT CALCULATED BY HAND: ')
print('t-statistic (by hand) = %6.3f.' % tval)
print('p-value = %6.4e.\n' % pval)

# Compare the standard library version of t-test.
m = 0 # mean
t, p = stats.ttest_1samp(y, m)
p = p/2 # for one-tailed p-value.
print('\nLIBRARY RESULT FOR COMPARISON: ')
print('t-statistic (by hand) = %6.3f.' % t)
print('p-value = %6.4e.\n' % p)
# END OF FILE.


import numpy as np
import matplotlib.pyplot as plt
#From Strogatz p.133, Fig. 5.2.1
#He_dot = heloves + sheloveshimback
#She_dot = sheloves + helovesherback
def phaseplot(he_loves=5,she_responds=-6,she_loves=2,he_responds=8, s1=2,s2=2):
    plt.close()
    x,y = np.linspace(-10,10,100),np.linspace(-10,10,100)
    He,She = np.meshgrid(x,y)
    A = np.array([[he_loves,she_responds],[she_loves,he_responds]])

    U = A[0,0]*He +A[0,1]*She
    V = A[1,0]*She + A[1,1]*He
    #Eigenvector calculations.
    w,v = np.linalg.eig(A)
    v = np.round(v,2)
    w = np.round(w,2)
    eigvects = np.dot(A,v)

    speed = np.sqrt(U*U + V*V)

    start = [[s1,s2]]

    fig0, ax0 = plt.subplots(1,1,figsize=(8,8))
    ax0.set_aspect('equal')
    strm = ax0.streamplot(x,y, U, V, color=(.75,.90,.93), linewidth=.5)
    strmS = ax0.streamplot(x,y, U, V, start_points=start, color="crimson", linewidth=1)
    ax0.plot(start[0][0],start[0][1],'go')
    ax0.set_xlim(-10,10)
    ax0.set_ylim(-10,10)
    try:
        ax0.plot(x,eigvects[1,0]/eigvects[0,0]*x,'b',alpha=.7)
    except RuntimeError:
        print("Division by zero error: No meaningful eigenvector")
    try:
        ax0.plot(x,eigvects[1,1]/eigvects[0,1]*x,'b',alpha=.7)
    except RuntimeError:
        print("Division by zero error: No meaningful eigenvector")
    ax0.set_title('Linear System', size=14)
    ax0.set_xlabel('Him',size=14)
    ax0.set_ylabel(r'Her',size=14 )
    ax0.text(-9,8,r'$\dot{0}$ = {1}x + {2}y'.format('x',A[0,0],A[0,1]))
    ax0.text(-9,7,'$\dot{0}$ = {1}x +{2} y'.format('y',A[1,0],A[1,1]))
    ax0.text(-9,6,r'$(x_0,y_0)$ = ({0}, {1})'.format(start[0][0],start[0][1]))
    ax0.text(-9,5,'$\lambda$ = {0}, {1} '.format(w[0],w[1]))
    ax0.text(-9,4,'Eigenvectors = {0}, {1}'.format(v[0].T,v[1].T))
    ax0.text(-9,-9,'General solution = c_1{0}e^({1}t) + c_2{2}e^({3}t)$'.format(v[0],w[0],v[1],w[1]))
    plt.grid()
    plt.show()
phaseplot()


# Import
import numpy as np
import matplotlib.pylab as plt

# Global Variables

# Classes

# Functions
def step_function(x):
	return np.array(x > 0, dtype=np.int)

def sigmoid(x):
	return 1 / (1 + np.exp(-x))

def relu(x):
	return np.maximum(0, x)

def leaky_relu(x):
	return np.maximum(0.01*x, x)

def main():
	x = np.arange(-5.0, 5.0, 0.1)
	y1 = step_function(x)
	y2 = sigmoid(x)
	y3 = relu(x)
	y4 = leaky_relu(x)

	plt.plot(x, y1, 'k--', label='step function')
	plt.plot(x, y2, label='sigmoid')
	plt.plot(x, y3, 'y-', label='ReLU')
	plt.plot(x, y4, 'r:', label='leaky ReLU')
	plt.ylim(-0.1, 2.1)
	plt.legend()
	plt.show()

if __name__ == '__main__':
	main()


import numpy as np

def matrix_chain_order(p, n, debug=False):
    '''
    Compute a matrix m which keeps the minimal cost of a matrix
    chain multiplication. It's also maintain another matrix s
    which tracks the sequence of the optimal solution.
    Pay attention in the indexing differences from the pseudocode.
    CLRS p336 - 2ed.
    '''
    m = [[float('inf') for x in range(n)] for x in range(n)] # initialize the matrix with +infinity
    s = [[0 for x in range(n)] for x in range(n)]             # initialize the matrix with 0

    for i in range (n): # fill the main diagonal with zeros
        m[i][i] = 0

    if debug == True:
        print 'l\ti\tn-l\tj\tk\tq = m[i, k] + m[k+1, j] + p[i]*p[k+1]*p[j+1]'
        print '==============================================='
    for l in range(1, n): # l is the chain length
        for i in range (n-l):
            j = i + l

            for k in range (i, j):
                q = m[i][k] + m[k+1][j] + p[i]*p[k+1]*p[j+1]

                if debug:
                    print l, '\t', i,'\t', n-l, '\t', j, '\t', k, '\t', q
                if q < m[i][j]:
                    m[i][j] = q
                    s[i][j] = k

            if i < n-l-1 and debug == True:
                print '...............................................'
        if debug == True:
            print '==============================================='

    return m, s

def print_optimal_parens(s, i, j):
    '''
    Given the optimal sequence of k values in matrix s, and the indices i and j,
    it prints the optimal parenthesization of sequence Ai, Ai+1..Aj recursively.
    CLRS p338 - 2ed.
    '''
    if i == j:
        print 'A' + str(i),
    else:
        print '(',
        print_optimal_parens(s, i, s[i][j])
        print_optimal_parens(s, s[i][j] + 1, j)
        print ')',

def matrix_chain_multiply(A, s, i, j):
    '''
    Given the optimal sequence of k values in matrix s, and the indices i and j,
    and the matrices in A[a1, A2,..., An] it computes the optimal scalar multiplication
    of matrices sequence Ai, Ai+1..Aj recursively.
    The product is built in a divide and coquer manner.
    '''
    if i == j:
        return A[i]
    else:
        a = matrix_chain_multiply(A, s, i, s[i][j])
        b = matrix_chain_multiply(A, s, s[i][j] + 1, j)

    	return np.dot(a, b)

def main():
    #p = [30, 35, 15, 5, 10, 20, 25]
    p = [5, 10, 3, 12, 5, 50, 6]

    debug = True
    m, s = matrix_chain_order(p, len(p)-1, debug)

    if debug:
    	print "Optimal parenthesization"
    	print "==============================================="
    	print "m = "
        for i in range(len(m)):
            print m[i]

        print "s = "
        for i in range(len(s)):
            print s[i]

        print_optimal_parens(s, 0, 5)

    A = []
    for j in range(len(p)-1):
        A.append(np.matrix(np.random.randint(0,10, size=(p[j], p[j+1]))))

    result = matrix_chain_multiply(A, s, 0, 5)
    if debug:
    	print
    	print "==============================================="
    	print "Scalar product result"
    	print "==============================================="
    	print 'A = '
    	for k in range(len(A)):
    		print 'A' + str(A[k].shape[0]) + 'x' + str(A[k].shape[1])
    		print A[k]
    	print 'Scalar product = '
    	print result

if __name__=="__main__":
    main()


import numpy as np
import matplotlib.pyplot as plt

def norm_histogram(hist):
    """
    takes a histogram of counts and creates a histogram of probabilities
    :param hist: list
    :return: list
    """

    i = 0;

    h_sum = 0;

    h_new = [];

    while i < len(hist):
        h_sum =  h_sum + hist[i]

	 i += 1;

    for j in hist:
        a = j / h_sum;

        h_new.append(a);

    return (h_new);

    pass

def compute_j(histo, width):
    """
    takes histogram of counts, uses norm_histogram to convert to probabilties, it then calculates compute_j for one bin width
    :param histo: list
    :param width: float
    :return: float
    """

    q_sum = 0;

    a = 0;

    b = 0;

    h_p = norm_histogram(histo);

    h_sq =[0] * len(h_p);

    while b < len(h_p):
        h_sq[b] = h_p[b] ** 2;

        q_sum = q_sum + (h_sq[b]);

        a += histo[b];

        b += 1;

    j_compute = 2 / ((a - 1) * width) - (((a + 1)/((a - 1) * width))* (q_sum))

    return (j_compute);

    pass

def sweep_n(data, minimum, maximum, min_bins, max_bins):
    """
    find the optimal bin
    calculate compute_j for a full sweep [min_bins to max_bins]
    please make sure max_bins is included in your sweep
    :param data: list
    :param minimum: int
    :param maximum: int
    :param min_bins: int
    :param max_bins: int
    :return: list
    """
    op_h = [];

    low = min_bins;

    while low <= max_bins:
        j_compute = compute_j( plt.hist( data, low, (minimum ,maximum))[0], (maximum - minimum)/ low);

        op_h.append(j_compute);

        low += 1;

    return (op_h);

    pass

def find_min(l):
    """
    generic function that takes a list of numbers and returns smallest number in that list its index.
    return optimal value and the index of the optimal value as a tuple.
    :param l: list
    :return: tuple
    """

    l_min = min(l);

    min_i = l.index(l_min);

    min_t = (l_min,min_i);

    return min_t ;

    pass

if __name__ == '__main__':
    data = np.loadtxt('input.txt')  # reads data from input.txt
    lo = min(data)
    hi = max(data)
    bin_l = 1
    bin_h = 100
    js = sweep_n(data, lo, hi, bin_l, bin_h)
    """
    the values bin_l and bin_h represent the lower and higher bound of the range of bins.
    They will change when we test your code and you should be mindful of that.
    """
    print(find_min(js))


# Clustering

## k-means

### k-means algorithm
in the clustering problem, we are given a training set $\left\{x^{(1)},...,x^{(n)}\right\}$, and want to group the data into a few cohesive "clusters".

here, $x^{(i)} \in \mathbb{R}^{d}$ as usual, but no labels $y^{(i)}$ are given, so this is an unsupervised learning problem.

k-means clustering algorithm:

1. initialize cluster centroids $\mu_{1},...,\mu_{k} \in \mathbb{R}^{d}$ randomly.
2. repeat until convergence:<br>
$\quad$ for each $i$, set:
$$c^{(i)} := \underset{j}{argmin}\left \|x^{(i)} - \mu_{j} \right \|^{2} $$
$\quad$ for each $j$, set:
$$\mu_{j} := \frac{\sum_{i=1}^{n}1\left\{c^{(i)}=j\right\}x^{(i)}}{\sum_{i=1}^{n}1\left\{c^{(i)}=j\right\}}$$

k is the number of clusters we want to find.

### convergence

define the distortion function to be:

$$J(c,\mu) = \sum_{i=1}^{n}\left \|x^{(i)} - \mu_{c^{(i)}} \right \|^{2}$$

thus $J$ measures the sum of squared distances between each training example $x^{(i)}$ and the nearest cluster centroid $\mu_{c^{(i)}}$

k-means is exactly coordinate descent on $J$.

the first step of k-means inner-loop:

$\quad$ minimize $J$ with respect to $c$ while holding $\mu$ fixed.

the second step of k-means inner-loop:

$\quad$ minimize $J$ with respect to $\mu$ while holding $c$ fixed.

but the distortion function $J$ is non-convex, so k-means may converge to local minimal.

to tackle this local-minimal problem, we commonly run k-means many times, and choose the one gives the lowest $J(c, \mu)$

### practice

from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split

X_digits, y_digits = load_digits(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X_digits, y_digits)

from sklearn.linear_model import LogisticRegression

n_labeled = 50
log_reg = LogisticRegression()
log_reg.fit(X_train[: n_labeled], y_train[: n_labeled])

log_reg.score(X_test, y_test)

from sklearn.cluster import KMeans

k=50
kmeans = KMeans(n_clusters=k, random_state=42)
X_digits_dist = kmeans.fit_transform(X_train)
X_digits_dist.shape  # sample distance from each centroid

import numpy as np

representative_digit_idx = np.argmin(X_digits_dist, axis=0)
X_representative_digits = X_train[representative_digit_idx]

y_representative_digits = y_train[representative_digit_idx]

log_reg = LogisticRegression(multi_class="ovr", solver="lbfgs", max_iter=5000, random_state=42)
log_reg.fit(X_representative_digits, y_representative_digits)
log_reg.score(X_test, y_test)

y_train_propagated = np.empty(len(X_train), dtype=np.int32)
for i in range(k):
    y_train_propagated[kmeans.labels_==i] = y_representative_digits[i]

log_reg = LogisticRegression(multi_class="ovr", solver="lbfgs", max_iter=5000, random_state=42)
log_reg.fit(X_train, y_train_propagated)

log_reg.score(X_test, y_test)

percentile_closest = 75

X_cluster_dist = X_digits_dist[np.arange(len(X_train)), kmeans.labels_]  # distance of each sample
for i in range(k):
    in_cluster = (kmeans.labels_ == i)
    cluster_dist = X_cluster_dist[in_cluster]
    cutoff_distance = np.percentile(cluster_dist, percentile_closest)
    above_cutoff = (X_cluster_dist > cutoff_distance)
    X_cluster_dist[in_cluster & above_cutoff] = -1

partially_propagated = (X_cluster_dist != -1)
X_train_partially_propagated = X_train[partially_propagated]
y_train_partially_propagated = y_train_propagated[partially_propagated]

log_reg = LogisticRegression(multi_class="ovr", solver="lbfgs", max_iter=5000, random_state=42)
log_reg.fit(X_train_partially_propagated, y_train_partially_propagated)

log_reg.score(X_test, y_test)

np.mean(y_train_partially_propagated == y_train[partially_propagated])


from fenics import *
import numpy as np
from ufl import nabla_div
import sympy as sym
import matplotlib.pyplot as plt

def solver(f, p_e, mesh, degree=1):
    """
    Solving the Darcy flow equation on a unit square media with pressure boundary conditions.
    """

    # Creating mesh and defining function space
    V = FunctionSpace(mesh, 'P', degree)

    # Defining Dirichlet boundary
    p_L = Constant(1.0)

    def boundary_L(x, on_boundary):
        return on_boundary and near(x[0], 0)

    bc_L = DirichletBC(V, p_L, boundary_L)

    p_R = Constant(0.0)

    def boundary_R(x, on_boundary):
        return on_boundary and near(x[0], 1)

    bc_R = DirichletBC(V, p_R, boundary_R)

    bcs = [bc_L, bc_R]

    # If p = p_e on the boundary, then use:-
    #def boundary(x, on_boundary):
        #return on_boundary

    #bc = DirichletBC(V, p_e, boundary)

    # Defining variational problem
    p = TrialFunction(V)
    v = TestFunction(V)
    d = 2
    I = Identity(d)
    M = Expression('fmax(0.10, exp(-pow(10.0*x[1]-1.0*sin(10.0*x[0])-5.0, 2)))', degree=2, domain=mesh)
    K = M*I
    a = dot(K*grad(p), grad(v))*dx
    L = inner(f, v)*dx

    # Computing Numerical Pressure
    p = Function(V)
    solve(a == L, p, bcs)

    return p

def run_solver():
    "Run solver to compute and post-process solution"

    mesh = UnitSquareMesh(40, 40)

    # Setting up problem parameters and calling solver
    d = 2
    I = Identity(d)
    M = Expression('fmax(0.10, exp(-pow(10.0*x[1]-1.0*sin(10.0*x[0])-5.0, 2)))', degree=2, domain=mesh)
    K = M*I
    p_e = Expression('1 - x[0]*x[0]', degree=2)
    #p_e = Expression('sin(x[0])*sin(x[1])', degree=2) # Another possible choice of p_e to be used with p = p_e on the boundary as BC
    f1 = Expression(('-2*x[0]', '0.0'), degree=1, domain=mesh)
    #f1 = as_vector((cos(x)*sin(y), sin(x)*cos(y))) # Use of Expression preferred here instead of as_vector
    f = nabla_div(dot(-K, f1))
    p = solver(f, p_e, mesh, 1)
    u_bar = -K*grad(p)
    u_bar1 = project(u_bar, VectorFunctionSpace(mesh, 'P', degree=1))

    # Saving and Plotting Numerical solutions for visualization
    xdmf = XDMFFile('Numerical_Pressure_Gradient_V2.xdmf')
    xdmf.write(p)
    xdmf.close()

    xdmf = XDMFFile('Velocity_Profile_V2.xdmf')
    xdmf.write(u_bar1)
    xdmf.close()

def test_solver():
    "Test solver by reproducing p = 1 - x^2"

    # Set up parameters for testing
    p_e = Expression('1 - x[0]*x[0]', degree=2)
    #p_e = Expression('sin(x[0])*sin(x[1])', degree=2)

    # Iterating over mesh sizes and DOF
    E = []
    DOF = []
    for m in range (10, 350, 10):
        mesh = UnitSquareMesh(m, m)
        V = FunctionSpace(mesh, 'P', 1)
        p_e_f = interpolate(p_e, FunctionSpace(mesh, 'P', 2))
        d = 2
        I = Identity(d)
        M = Expression('fmax(0.10, exp(-pow(10.0*x[1]-1.0*sin(10.0*x[0])-5.0, 2)))', degree=2, domain=mesh)
        K = M*I
        f1 = Expression(('-2*x[0]', '0.0'), degree=1, domain=mesh)
        #f1 = as_vector((cos(x)*sin(y), sin(x)*cos(y))) # Use of Expression preferred here instead of as_vector
        f = nabla_div(dot(-K, f1))

        # Compute solution
        p = solver(f, p_e, mesh, degree=1)

        # Computing Error in L2 norm for Pressure
        E1 = errornorm(p_e_f, p, 'L2')
        print('E1 =', E1)
        E.append(E1)
        DOF.append(len(V.dofmap().dofs()))

    print(E)
    print(DOF)
    Ea = np.array(E)
    DOFa = np.array(DOF)
    LogEa = np.log(Ea)
    LogDOFa = np.log(DOFa)

    return (E, DOF)

if __name__ == '__main__':
    run_solver()

    E, DOF = test_solver()

    # Log plot of L2 Error against DOF
    x = np.log(DOF)
    y = np.log(E)
    plt.plot(x,y)
    plt.title('Log L2 Error vs. Log DOF')
    plt.xlabel("Log DOF")
    plt.ylabel("Log L2 Error")
    plt.show()

    # Semilog plot of L2 Error against DOF
    plt.semilogy(DOF, E)
    plt.title('Semilog L2 Error vs DOF')
    plt.xlabel("DOF")
    plt.ylabel("L2 Error")
    plt.show()


"""
Cubic Spline (Polynomial Interpolation by parts)
"""

import numpy as np
import interpolation.methods.linear_system as lys
from bisect import bisect_right

def Ak(Y2d, H, k):
    """
    Calculate Ak for Sk(x)

    Parameters
    ----------
        Y2d : list
            list of y values with the second derived
        H : list
            list of h values from spline
        k : int
            index from Y2d and H

    Returns
    -------
        float
            Ak from cubic spline
    """
    return (Y2d[k] - Y2d[k - 1]) / (6 * H[k - 1])

def Bk(Y2d, k):
    """
    Calculate Bk for Sk(x)

    Parameters
    ----------
        Y2d : list
            list of y values with the second derived
        k : int
            index from Y2d

    Returns
    -------
        float
            Bk from cubic spline
    """
    return Y2d[k] / 2

def Ck(Y2d, H, Y, k):
    """
    Calculate Ck for Sk(x)

    Parameters
    ----------
        Y2d : list
            list of y values with the second derived
        H : list
            list of h values from spline
        Y : list
            list of y values
        k : int
            index from Y2d, Y, H

    Returns
    -------
        float
            Ck from cubic spline
    """
    return (Y[k] - Y[k - 1]) / H[k - 1] + (2 * H[k - 1] * Y2d[k] + Y2d[k - 1] * H[k - 1]) / 6

def Dk(Y, k):
    """
    Calculate Dk for Sk(x)

    Parameters
    ----------
        Y : list
            list of y values
        k : int
            index from Y

    Returns
    -------
        float
            Dk from cubic spline
    """
    return Y[k]

def Hk(X, k):
    """
    Calculate Hk for Sk(x)

    Parameters
    ----------
        X : list
            list of x values
        k : int
            index from X

    Returns
    -------
        float
            Hk from cubic spline
    """
    return X[k] - X[k - 1]

def Sk(Y2d, H, X, Y, k):
    """
    Create Sk function

    Parameters
    ----------
        Y2d : list
            list of y values with the second derived
        H : list
            list of h values from cubic spline
        X : list
            list of x values
        Y : list
            list of y values
        k : int
            index from Y2d, H, X and Y

    Returns
    -------
        function
            Sk from cubic spline
    """
    def f(x):
        x_diff = x - X[k]
        a = Ak(Y2d, H, k) * x_diff**3
        b = Bk(Y2d, k) * x_diff**2
        c = Ck(Y2d, H, Y, k) * x_diff
        d = Dk(Y, k)
        return a + b + c + d
    return f

def cubic_spline(X, Y):
    """
    Polynomal Interpolation by parts using a natural cubic spline

    Parameters
    ----------
        X : list
            list of x values
        Y : list
            list of y values

    Returns
    -------
        function
            function of interpolation by parts (using natural cubic spline)
    """

    # H's
    n = len(X)
    H = [Hk(X, k) for k in range(1, n)]

    # A
    m = n - 2
    A = np.zeros((m, m))

    # Diagonal (A)
    np.fill_diagonal(A, [2 * (H[k] + H[k + 1]) for k in range(m)])
    # Upper and lower diagonal (A)
    for k in range(0, m - 1):
        A[k, k + 1] = H[k + 1]
        A[k + 1, k] = H[k + 1]

    # B
    B = [ 6 * ((Y[k + 2] - Y[k + 1]) / H[k + 1] - (Y[k + 1] - Y[k]) / H[k]) for k in range(m)]

    # Y2d
    Y2d = lys.solve_sys(A, B)
    Y2d = [0, *Y2d, 0]

    # Sk
    S = [Sk(Y2d, H, X, Y, k) for k in range(1, n)]

    def f(x):
        k = bisect_right(X, x) - 1
        if k == n - 1:
            k -= 1
        return S[k](x)

    return f


import numpy as np
from numpy import sin, cos, arctan2, sqrt

def to_rot(r):
    Rx = np.mat([[ 1,         0,           0],
                 [ 0, cos(r[0]), -sin(r[0]) ],
                 [ 0, sin(r[0]),  cos(r[0]) ]])

    Ry = np.mat([[ cos(r[1]), 0,  sin(r[1]) ],
                 [ 0,         1,          0 ],
                 [-sin(r[1]), 0,  cos(r[1]) ]])

    Rz = np.mat([[ cos(r[2]), -sin(r[2]), 0 ],
                 [ sin(r[2]),  cos(r[2]), 0 ],
                 [         0,          0, 1 ]])

    return Rz*Ry*Rx

def to_mat(p, r):
    "Given a position [m] and orientation in RPY [rad], create homogenous transformation matrix"
    R = to_rot(r)

    return np.mat(np.r_[np.c_[R, p], [np.array([0, 0, 0, 1])]])

def from_mat(T):
    "Get position [m] and orientation in RPY [rad] from homogenous transformation matrix"

    p = [T[0,3], T[1,3], T[2,3]]

    r = [arctan2(T[2,1],T[2,2]) , arctan2(-T[2,0],sqrt(T[2,1] ** 2 + T[2,2] ** 2)), arctan2(T[1,0],T[0,0]) ]

    return p, r

def transform_data(p, r, T_frame):
    "Transform gt data to different frame"
    p_new = [0]*len(p)
    r_new = [0]*len(p)

    for i in (range(len(p))):
        T_i = to_mat(p[i, :], r[i, :])
        T_new = T_i.dot(T_frame)
        p_new[i], r_new[i] = from_mat(T_new)

    return np.array(p_new), np.array(r_new)

def transform_data_left(p, r, T_frame):
    "Transform gt data to different frame"
    p_new = [0]*len(p)
    r_new = [0]*len(p)

    for i in (range(len(p))):
        T_i = to_mat(p[i, :], r[i, :])
        T_new = T_frame.dot(T_i)
        p_new[i], r_new[i] = from_mat(T_new)

    return np.array(p_new), np.array(r_new)

def to_own_frame(r,x):
    x_new = np.zeros(x.shape)

    for i in (range(len(x))):
        x_new[i] = x[i].dot(to_rot(r[i]))

    return x_new

def intg(x,t):
    out = [None] * (len(x) + 1)
    for i in range(len(out)):
        dt = t[i + 1] - t[i]
        out[i+1,:] = out[i,:] + x[i,:]*dt

    return out

def diff(x, t):
    out = [None] * (len(x) - 1)
    for i in (range(len(out))):
        dt = t[i + 1] - t[i]
        dx = x[i + 1, :] - x[i, :]
        out[i] = dx / dt

    return out

Course 2 - State Estimation and Localization/Course2FinalProject/data/utils.py


# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# Name:      general.py
# Purpose:   general ath Utility programs
#
# Author:      Indranil Sinharoy
#
# Created:        22/09/2012
# Last Modified:  04/24/2017
# Copyright:      (c) Indranil Sinharoy, 2012 - 2017
# Licence:        MIT License
#-------------------------------------------------------------------------------
"""General math utilities
"""
from __future__ import division, print_function
import numpy as _np
import sympy as _sym

# trig functions
sind = lambda x : _np.sin(_np.deg2rad(x))
cosd = lambda x : _np.cos(_np.deg2rad(x))
tand = lambda x : _np.tan(_np.deg2rad(x))

# inverse trig functions
arcsind = lambda x : _np.rad2deg(_np.arcsin(x))
arccosd = lambda x : _np.rad2deg(_np.arccos(x))
arctand = lambda x : _np.rad2deg(_np.arctan(x))

# docstrings of the trig functions
sind.__doc__ = "Sine of argument in degrees"
cosd.__doc__ = "Cosine of argument in degrees"
tand.__doc__ = "Tangent of argument in degrees"
arcsind.__doc__ = "Inverse sine in degrees"
arccosd.__doc__ = "Inverse cosine in degrees"
arctand.__doc__ = "Inverse tan in degrees"

def factorial(n):
    """return n factorial

    The factorial is defined as n! = n*(n-1)!
    Note: The recursive factoial implementation will break down if n is too large
    """
    if(n==0):
        return 1
    else:
        return n*factorial(n-1)

def fibonacci(n):
    """returns a fibonacci sequence generator object for fibonacci sequence of n
    elements

    Examples
    --------
    >>> fs = fibonacci(2)
    >>> list(fs)
    [1, 1]
    >>> fs = fibonacci(10)
    >>> list(fs)
    [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
    """
    low, high = 0, 1
    count = 0
    while count < n:
        yield high
        low, high = high, low + high
        count +=1

def golden_ratio(n):
    """returns the Golden Ratio to an accuracy of `n` decimal places

    Examples
    --------
    >>> print(golden_ratio(20))
    1.61803398874989484820
    >>> print(golden_ratio(50))
    1.61803398874989484820458683436563811772030917980576
    """
    x = _sym.symbols('x')
    gr = [sol for sol in _sym.solve(x**2 - x - 1) if sol > 0][0]
    return gr.evalf(n + 1)

def nCk(n, k):
    """return the number of possible combinations of `k` objects from a set of `n` objects
    (i.e., order does not matter).

    Other phrases include "`n` choose `k`" or "the number of combinations of `n` things
    taken `k` at a time".

    Defined for positive integers `n>=k` as `nCk(n,k) = n!/(k!*(n-k)!)`

    nCk is often represented as  / n \
                                 |   |
                                 \ k /

    Examples
    --------
    >>> nCk(3, 2)
    3

    See Also
    --------
    itertools.combinations(iterable, r): It returns successive r-length combinations of
    elements in the iterable. For example,
        `list(combinations(['A', 'B', 'C'], 2)) -->
        [('A', 'B'), ('A', 'C'), ('B', 'C')]`
    """
    return factorial(n)/(factorial(k)*factorial(n-k))

def nPk(n,k):
    """return the number of ways to arange `k` items out of `n` items. i.e., the order
    matters

    It is defined for positive integers `n>=k` as `nPk(n,k) = n!/(n-k)!`
    """
    return factorial(n)/factorial(n-k)

def binomial_distribution(n, p):
    """returns the Binomial probability distribution (PMF)

    Parameters
    ----------
    n : integer
        the number of samples or subexperiments or Bernoulli trials
    p : real
        the probability of success in each Bernoulli trial

    Notes
    -----
    The PMF for a binomial RV X is given as:
        p_x[k] = nCk* p^k * (1-p)^(n-k)
    """
    return [nCk(n,k)*(p**k)*((1-p)**(n-k)) for k in range(n+1)]

def cart2pol(X, Y, Z=None):
    """Transform Cartesian coordinates to polar or cylindrical

    cart2pol(X, Y [,Z]) -> rho, theta [,Z]

    Parameters
    ----------
    X  : Cartesian coordinate array
    Y  : Cartesian coordinate array
    Z  : (optional) if present, then the function converts 3D Cartesian coordinates
         to cylindrical coordinates.

    Returns
    -------
    rho   : distance from the origin to a point in the x-y plane
    theta : counterclockwise angular displacement in radians from the positive x-axis
    Z     : height above the x-y plane

    """
    if X.shape != Y.shape:
        raise ValueError("Input error: Expecting X.shape == Y.shape")
    rho = _np.sqrt(X**2.0 + Y**2.0)
    theta = _np.arctan2(Y,X)
    if Z:
        return rho, theta, Z
    else:
        return rho, theta

## Test the functions
def _test_factorial():
    print("test the factorial function")
    assert factorial(5)==120

def _test_nCk():
    print("test combination")
    print(nCk(10,2))
    #print nCk(2500,2)  #Right now this fails ... the recursion is too large...

def _test_cart2pol():
    print("test cart2pol()")
    X,Y = _np.mgrid[-2:2:10j,-2:2:10j]
    # Convert from cartesian to polar choosing the grid manually
    # Logic from http://scien.stanford.edu/pages/labsite/2003/psych221/projects/03/pmaeda/index_files/zernike.m
    # however there is something work
    # FIXME!! what is the correct logic for this. For now I assume that using atan2()
    # gives the correct results.
    rhoExp = _np.sqrt(X**2 + Y**2)
    mask = ((X >= 0) & (Y >= 0)) | ((X >= 0) & (Y < 0))
    thetaExp = _np.where(mask,_np.arctan(Y/(X+1e-15)),_np.pi + _np.arctan(Y/(X+1e-15)))
    rho, theta = cart2pol(X,Y)
    abs_diff_rho = _np.sum(_np.abs(rhoExp - rho))
    abs_diff_theta = _np.sum(_np.abs(thetaExp - theta))
    print(abs_diff_rho)
    print(abs_diff_theta)
    print(thetaExp)
    print(theta)

def _test_fibonacci():
    print("test fibonacci()")
    fs = fibonacci(10)
    fsl = list(fs)
    fsexp = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
    _nt.assert_array_equal(fsl, fsexp)

def _test_goldenRatio():
    print("test goldenRatio()")
    gr15 = 1.618033988749895
    _nt.assert_almost_equal(golden_ratio(15), gr15, decimal=15)
    print(golden_ratio(50))

if __name__ == '__main__':
    import numpy.testing as _nt
    _test_factorial()
    _test_nCk()
    _test_cart2pol()
    _test_fibonacci()
    _test_goldenRatio()


import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
#from sklearn import metrics
import scipy

#Regressao linear com Adaline e Pseudo-Inversa

entrada = np.array([0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0])
target = np.array([2.26, 3.8, 4.43, 5.91, 6.18, 7.26, 8.15, 9.14, 10.87, 11.58, 12.55])

################## TESTE COM ADALINE ############################

def y_liquid(entrada, peso, bias):
    y = entrada * peso + bias
    return y

def training(entrada, saida, alfa, iteracoes):
    bias = np.random.rand(1)-0.5
    peso = np.random.rand(1)-0.5
    EQT = 100
    EQT2 = 50
    precisao = 0.000001
    count = 0

    #for i in range(iteracoes):
    while abs(EQT - EQT2) > precisao:
        EQT = EQT2
        EQT2 = 0
        count +=1
        for i in range(len(entrada)):
            y = y_liquid(entrada[i], peso, bias)
            EQT2 += 0.5*((saida[i] - y)**2)
            peso += entrada[i] * (saida[i] - y) * alfa
            bias += (saida[i] - y) * alfa
    print("Total de iteracoes: " + str(count))
    print("Variação do Erro Quadratico Total: " + str(abs(EQT2-EQT)))
    return peso, bias

def fit(entrada, peso, bias):
    for i in range(len(entrada)):
        y = y_liquid(entrada[i], peso, bias)
        print("Para a entrada {0}, o Y calculado é {1}".format(entrada[i],y))

def metricas():
    EMQ = 0
    NUM = []
    DEN = []
    PN = []
    PD = []
    PD1 = []
    Y = []
    for i in range(len(entrada)):
        y = y_liquid(entrada[i], peso, bias)
        Y.append(y)
        EMQ += (target[i] - y)**2/len(target)
        NUM.append(((target[i] - y)**2))
        DEN.append((target[i] - np.mean(target))**2)
        PN.append((entrada[i] - np.mean(entrada)) * (target[i] - np.mean(target)))
        PD.append((entrada[i] - np.mean(entrada)) ** 2)
        PD1.append((target[i] - np.mean(target)) ** 2)
    Person = sum(PN) /(np.sqrt(sum(PD)*sum(PD1)))
    R2 = 1 - (sum(NUM)/sum(DEN))
    print("Erro Médio Quadrático: " + str(EMQ)) #"EMQ: " + str(metrics.mean_squared_error(target,Y)))
    print("R-square: " + str(R2))               #"R2: " + str(metrics.r2_score(target,Y)))
    print("Coeficiente de Person: " + str(Person) + '\n')

def plot():
    x, y = pd.Series(entrada, name="X"), pd.Series(target, name="Y")
    ax = sns.regplot(x=x, y=y, ci= 68, color = "purple", marker="^")
    plt.show()

###################### TESTE COM PSEUDO-INVERSA #######################

entradaPINV = np.array([[0.0, 1.0], [0.5, 1.0],[1.0, 1.0], [1.5, 1.0], [2.0, 1.0], [2.5, 1.02],
               [3.0, 1.0],[3.5, 1.0], [4.0, 1.0], [4.5, 1.0], [5.0, 1.0]])

def pseudoInversa(entrada, saida):
    coeficientes = np.linalg.pinv(entrada) @ saida
    ycalculado = entrada @ coeficientes
    print("\nPeso: " + str(coeficientes[0]) + "\nBias: " + str(coeficientes[1]))
    EMQa = sum((saida - ycalculado) ** 2) / len(saida)
    R2a = 1 - sum((saida - ycalculado) ** 2) / sum((saida - np.mean(saida)) ** 2)
    print("\nErro Médio Quadrático: " + str(EMQa))
    print("R-square: " + str(R2a) + '\n')
    for i in range(len(saida)):
        print("Para a entrada {0}, o Y calculado é {1}".format(entrada[i,0], ycalculado[i]))

def coefientesFormula():
    entrada2 = np.array(entrada)**2
    b = (sum(entrada2) * sum(target) - sum(entrada*target)*sum(entrada))/(len(entrada) * sum(entrada2) - (sum(entrada))**2)
    a_peso = (len(entrada)*sum(entrada*target) - sum(entrada)*sum(target)) / (len(entrada) * sum(entrada2) - (sum(entrada))**2)
    print("Calculo dos coeficientes pela fórmula\n")
    print("a = {0} e b = {1}".format(a_peso,b))

#def salvarTXT(entrada, target):
#    f = open("dados.txt", 'w')
#    for i in range(len(entrada)):
#        aux, aux2 = entrada[i], target[i]
#        f.write(str(aux) + " " + str(aux2) + "\n")
print("-------------------- TESTE COM ADALINE -----------------\n")
coefientesFormula()
print("==================== TRAINING ====================\n")
peso, bias = training(entrada, target, alfa=0.005, iteracoes = 100)
print("\nPeso adequado encontrado: {0}.\nBias encontrado: {1}".format(peso, bias) + '\n')
print("==================== FIT ==========================\n")
fit(entrada, peso, bias)
print("\nEquação da reta: y = " + str(peso) + " * x + " + str(bias))
print("================= MÉTRICAS DE ERRO ==================\n")
metricas()
print("-------------------- TESTE COM PSEUDO-INVERSA ------------------")
pseudoInversa(entradaPINV, target)
plot()
print("\nFIM")


"""
Erdos-Renyi Model
----------------------
This module contains different functions for generating a random graphs
using Erdos-Renyi models G(n, p) and G(n, m)
"""
import sys, os, math
import numpy as np

sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

import graph

def er_np(n, p, seed=None):
    """
    Creates a random graph following Erdos-Renyi G(n, p) model.

    :param n: number of nodes
    :param p: probability of an edge existing between any pair of nodes
    :param seed: Seed for generating random number. If it is not specified, time stamp is used.
    :return: a Graph object
    """

    G = graph.Graph({})
    for i in range(n):
        G.add_vertex(i)

    # generate n choose 2 random integers
    probabilities = np.random.randint(low=0, high=100, size=n*(n-1)//2)

    for u in range(n):
        for v in range(u+1, n):
            proba = np.random.uniform()
            if float(p) - proba > float(0.0):
                G.add_edge((u, v))

    # edge k has points i = floor((-1 + sqrt(1 + 8k))/2)
    # and j = k - i*(i+1) / 2

    return G

def er_nm(n, m, seed=None):
    """
    Creates a random graph following Erdos-Renyi G(n,m) model.

    :param n: Number of nodes
    :param m: Number of edges
    :param seed: Seed for generating random numbers. If it is not specified, time stamp is used.
    :return: a Graph object
    """

    # create an empty graph with n vertices
    G = graph.Graph({})
    for i in range(n):
        G.add_vertex(i)

    # pick m random pairs without repetition
    edges_to_add = np.random.choice(1+np.arange(n*(n-1)//2), m, replace=False)

    # think about it as a matrix
    # which is lower - right triangular
    for e in edges_to_add:
        i = (-1 + math.sqrt(1 + 8*e))/2.0
        i = int(math.ceil(i))
        j = e - i*(i-1)//2
        G.add_edge((i-1, j-1))

    return G

def compare_edge_count(n, p, seed=None):
    """
    Compares number of edges of G(n,p) and G(n, m) where m = floor(p*C_2^n)

    :param n: Number of nodes
    :param p: Probability of having an edge
    :param seed: Seed for generating random numbers. If it is not specified, time stamp is used
    :return: the ratio suze(E(G(n, p))) / size(E(G(n, m)))
    """
    m = p * n * (n-1) / 2
    m = int(math.floor(m))
    G_np = er_np(n, p, seed=seed)
    G_nm = er_nm(n, m, seed=seed)

    ratio = G_np.number_of_edges() / G_nm.number_of_edges()

    return ratio


"""
==================================
The Tipping Problem - The Hard Way
==================================

 Note: This method computes everything by hand, step by step. For most people,
 the new API for fuzzy systems will be preferable. The same problem is solved
 with the new API `in this example <./plot_tipping_problem_newapi.html>`_.

The 'tipping problem' is commonly used to illustrate the power of fuzzy logic
principles to generate complex behavior from a compact, intuitive set of
expert rules.

Input variables
---------------

A number of variables play into the decision about how much to tip while
dining. Consider two of them:

* ``quality`` : Quality of the food
* ``service`` : Quality of the service

Output variable
---------------

The output variable is simply the tip amount, in percentage points:

* ``tip`` : Percent of bill to add as tip

For the purposes of discussion, let's say we need 'high', 'medium', and 'low'
membership functions for both input variables and our output variable. These
are defined in scikit-fuzzy as follows

"""
import numpy as np
import skfuzzy as fuzz
import matplotlib.pyplot as plt

# Generate universe variables
#   * Quality and service on subjective ranges [0, 10]
#   * Tip has a range of [0, 25] in units of percentage points
x_qual = np.arange(0, 11, 1)
x_serv = np.arange(0, 11, 1)
x_tip = np.arange(0, 26, 1)

# Generate fuzzy membership functions
qual_lo = fuzz.trimf(x_qual, [0, 0, 5])
qual_md = fuzz.trimf(x_qual, [0, 5, 10])
qual_hi = fuzz.trimf(x_qual, [5, 10, 10])
serv_lo = fuzz.trimf(x_serv, [0, 0, 5])
serv_md = fuzz.trimf(x_serv, [0, 5, 10])
serv_hi = fuzz.trimf(x_serv, [5, 10, 10])
tip_lo = fuzz.trimf(x_tip, [0, 0, 13])
tip_md = fuzz.trimf(x_tip, [0, 13, 25])
tip_hi = fuzz.trimf(x_tip, [13, 25, 25])

# Visualize these universes and membership functions
fig, (ax0, ax1, ax2) = plt.subplots(nrows=3, figsize=(8, 9))

ax0.plot(x_qual, qual_lo, 'b', linewidth=1.5, label='Bad')
ax0.plot(x_qual, qual_md, 'g', linewidth=1.5, label='Decent')
ax0.plot(x_qual, qual_hi, 'r', linewidth=1.5, label='Great')
ax0.set_title('Food quality')
ax0.legend()

ax1.plot(x_serv, serv_lo, 'b', linewidth=1.5, label='Poor')
ax1.plot(x_serv, serv_md, 'g', linewidth=1.5, label='Acceptable')
ax1.plot(x_serv, serv_hi, 'r', linewidth=1.5, label='Amazing')
ax1.set_title('Service quality')
ax1.legend()

ax2.plot(x_tip, tip_lo, 'b', linewidth=1.5, label='Low')
ax2.plot(x_tip, tip_md, 'g', linewidth=1.5, label='Medium')
ax2.plot(x_tip, tip_hi, 'r', linewidth=1.5, label='High')
ax2.set_title('Tip amount')
ax2.legend()

# Turn off top/right axes
for ax in (ax0, ax1, ax2):
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    ax.get_xaxis().tick_bottom()
    ax.get_yaxis().tick_left()

plt.tight_layout()

"""
.. image:: PLOT2RST.current_figure

Fuzzy rules
-----------

Now, to make these triangles useful, we define the *fuzzy relationship*
between input and output variables. For the purposes of our example, consider
three simple rules:

1. If the food is bad OR the service is poor, then the tip will be low
2. If the service is acceptable, then the tip will be medium
3. If the food is great OR the service is amazing, then the tip will be high.

Most people would agree on these rules, but the rules are fuzzy. Mapping the
imprecise rules into a defined, actionable tip is a challenge. This is the
kind of task at which fuzzy logic excels.

Rule application
----------------

What would the tip be in the following circumstance:

* Food *quality* was **6.5**
* *Service* was **9.8**

"""
#
# # We need the activation of our fuzzy membership functions at these values.
# # The exact values 6.5 and 9.8 do not exist on our universes...
# # This is what fuzz.interp_membership exists for!
# qual_level_lo = fuzz.interp_membership(x_qual, qual_lo, 6.5)
# qual_level_md = fuzz.interp_membership(x_qual, qual_md, 6.5)
# qual_level_hi = fuzz.interp_membership(x_qual, qual_hi, 6.5)
#
# serv_level_lo = fuzz.interp_membership(x_serv, serv_lo, 9.8)
# serv_level_md = fuzz.interp_membership(x_serv, serv_md, 9.8)
# serv_level_hi = fuzz.interp_membership(x_serv, serv_hi, 9.8)
#
# # Now we take our rules and apply them. Rule 1 concerns bad food OR service.
# # The OR operator means we take the maximum of these two.
# active_rule1 = np.fmax(qual_level_lo, serv_level_lo)
#
# # Now we apply this by clipping the top off the corresponding output
# # membership function with `np.fmin`
# tip_activation_lo = np.fmin(active_rule1, tip_lo)  # removed entirely to 0
#
# # For rule 2 we connect acceptable service to medium tipping
# tip_activation_md = np.fmin(serv_level_md, tip_md)
#
# # For rule 3 we connect high service OR high food with high tipping
# active_rule3 = np.fmax(qual_level_hi, serv_level_hi)
# tip_activation_hi = np.fmin(active_rule3, tip_hi)
# tip0 = np.zeros_like(x_tip)
#
# # Visualize this
# fig, ax0 = plt.subplots(figsize=(8, 3))
#
# ax0.fill_between(x_tip, tip0, tip_activation_lo, facecolor='b', alpha=0.7)
# ax0.plot(x_tip, tip_lo, 'b', linewidth=0.5, linestyle='--', )
# ax0.fill_between(x_tip, tip0, tip_activation_md, facecolor='g', alpha=0.7)
# ax0.plot(x_tip, tip_md, 'g', linewidth=0.5, linestyle='--')
# ax0.fill_between(x_tip, tip0, tip_activation_hi, facecolor='r', alpha=0.7)
# ax0.plot(x_tip, tip_hi, 'r', linewidth=0.5, linestyle='--')
# ax0.set_title('Output membership activity')
#
# # Turn off top/right axes
# for ax in (ax0,):
#     ax.spines['top'].set_visible(False)
#     ax.spines['right'].set_visible(False)
#     ax.get_xaxis().tick_bottom()
#     ax.get_yaxis().tick_left()
#
# plt.tight_layout()
#
# """
# .. image:: PLOT2RST.current_figure
#
# Rule aggregation
# ----------------
#
# With the *activity* of each output membership function known, all output
# membership functions must be combined. This is typically done using a
# maximum operator. This step is also known as *aggregation*.
#
# Defuzzification
# ---------------
# Finally, to get a real world answer, we return to *crisp* logic from the
# world of fuzzy membership functions. For the purposes of this example
# the centroid method will be used.
#
# The result is a tip of **19.84%**.
# ---------------------------------
# """
#
# # Aggregate all three output membership functions together
# aggregated = np.fmax(tip_activation_lo,
#                      np.fmax(tip_activation_md, tip_activation_hi))
#
# # Calculate defuzzified result
# tip = fuzz.defuzz(x_tip, aggregated, 'centroid')
# tip_activation = fuzz.interp_membership(x_tip, aggregated, tip)  # for plot
#
# # Visualize this
# fig, ax0 = plt.subplots(figsize=(8, 3))
#
# ax0.plot(x_tip, tip_lo, 'b', linewidth=0.5, linestyle='--', )
# ax0.plot(x_tip, tip_md, 'g', linewidth=0.5, linestyle='--')
# ax0.plot(x_tip, tip_hi, 'r', linewidth=0.5, linestyle='--')
# ax0.fill_between(x_tip, tip0, aggregated, facecolor='Orange', alpha=0.7)
# ax0.plot([tip, tip], [0, tip_activation], 'k', linewidth=1.5, alpha=0.9)
# ax0.set_title('Aggregated membership and result (line)')
#
# # Turn off top/right axes
# for ax in (ax0,):
#     ax.spines['top'].set_visible(False)
#     ax.spines['right'].set_visible(False)
#     ax.get_xaxis().tick_bottom()
#     ax.get_yaxis().tick_left()
#
# plt.tight_layout()
#
# """
# .. image:: PLOT2RST.current_figure
#
# Final thoughts
# --------------
#
# The power of fuzzy systems is allowing complicated, intuitive behavior based
# on a sparse system of rules with minimal overhead. Note our membership
# function universes were coarse, only defined at the integers, but
# ``fuzz.interp_membership`` allowed the effective resolution to increase on
# demand. This system can respond to arbitrarily small changes in inputs,
# and the processing burden is minimal.
#
# """


"""
Implements iteration on the Bellman equations to solve the McCall growth model

"""

import numpy as np
from quantecon.distributions import BetaBinomial
from numba import jit

# A default utility function

@jit
def u(c, sigma):
    if c > 0:
        return (c**(1 - sigma) - 1) / (1 - sigma)
    else:
        return -10e6

class McCallModel:
    """
    Stores the parameters and functions associated with a given model.
    """

    def __init__(self, alpha=0.2,    # Job separation rate
                        beta=0.98,   # Discount rate
                        gamma=0.7,   # Job offer rate
                        c=6.0,       # Unemployment compensation
                        sigma=2.0,   # Utility parameter
                        w_vec=None,  # Possible wage values
                        p_vec=None): # Probabilities over w_vec

        self.alpha, self.beta, self.gamma, self.c = alpha, beta, gamma, c
        self.sigma = sigma

        # Add a default wage vector and probabilities over the vector using
        # the beta-binomial distribution
        if w_vec is None:
            n = 60  # number of possible outcomes for wage
            self.w_vec = np.linspace(10, 20, n)     # wages between 10 and 20
            a, b = 600, 400  # shape parameters
            dist = BetaBinomial(n-1, a, b)
            self.p_vec = dist.pdf()
        else:
            self.w_vec = w_vec
            self.p_vec = p_vec

@jit
def _update_bellman(alpha, beta, gamma, c, sigma, w_vec, p_vec, V, V_new, U):
    """
    A jitted function to update the Bellman equations.  Note that V_new is
    modified in place (i.e, modified by this function).  The new value of U is
    returned.

    """
    for w_idx, w in enumerate(w_vec):
        # w_idx indexes the vector of possible wages
        V_new[w_idx] = u(w, sigma) + beta * ((1 - alpha) * V[w_idx] + alpha * U)

    U_new = u(c, sigma) + beta * (1 - gamma) * U + \
                    beta * gamma * np.sum(np.maximum(U, V) * p_vec)

    return U_new

def solve_mccall_model(mcm, tol=1e-5, max_iter=2000):
    """
    Iterates to convergence on the Bellman equations

    Parameters
    ----------
    mcm : an instance of McCallModel
    tol : float
        error tolerance
    max_iter : int
        the maximum number of iterations
    """

    V = np.ones(len(mcm.w_vec))  # Initial guess of V
    V_new = np.empty_like(V)     # To store updates to V
    U = 1                        # Initial guess of U
    i = 0
    error = tol + 1

    while error > tol and i < max_iter:
        U_new = _update_bellman(mcm.alpha, mcm.beta, mcm.gamma,
                mcm.c, mcm.sigma, mcm.w_vec, mcm.p_vec, V, V_new, U)
        error_1 = np.max(np.abs(V_new - V))
        error_2 = np.abs(U_new - U)
        error = max(error_1, error_2)
        V[:] = V_new
        U = U_new
        i += 1

    return V, U