def cells():
    '''
    # 4/ Exercise solutions
    '''

    '''
    '''

    # helper code needed for running in colab
    if 'google.colab' in str(get_ipython()):
        print('Downloading plot_helpers.py to util/ (only neded for colab')
        !mkdir util; wget https://raw.githubusercontent.com/minireference/noBSLAnotebooks/master/util/plot_helpers.py -P util

    '''
    '''

    from sympy import *
    init_printing()

    %matplotlib inline
    import matplotlib.pyplot as mpl
    from util.plot_helpers import plot_augmat, plot_plane, plot_point, plot_line, plot_vec, plot_vecs

    Vector = Matrix  # define alias Vector so I don't have to explain this during video
    Point = Vector   # define alias Point for Vector since they're the same thing

    '''
    '''

    '''
    ### E4.5

    '''

    '''
    '''

    # Given point r
    r = Point([1,3,0])

    # line L
    pL = Point([0,0,2])
    vL = Vector([1,-1,0])  # note this changed in v2.2 of LA book

    # plane P
    nP = Vector([1,1,1])
    dP = 1

    # compute the following closest distances:

    '''
    '''

    # a)  d(r,O) = ||r||
    r.norm()

    '''
    '''

    # b)   d(L,O) = || pL - proj_{L}(pL) ||
    (pL - (pL.dot(vL)/vL.norm()**2)*vL).norm()

    '''
    '''

    # c)   d(P,O) = || proj_{nP}(pP) ||
    pP = Matrix([1,0,0])
    ( (nP.dot(pP)/nP.norm()**2)*nP).norm()

    '''
    '''

    # d)   u=pL-r   d(r,L)=||u - proj_{L}(u)||
    u = pL - r
    (u - (u.dot(vL)/vL.norm()**2)*vL).norm()

    '''
    '''

    # e)     v=pP-r   d(r,P)=||proj_{nP}(v)||
    pP = Matrix([1,0,0])
    v = pP - r
    ( (nP.dot(v)/nP.norm()**2)*nP).norm()

    '''
    '''

    #f)  d(L,P)
    # STEP1: check relative orientation of L and P
    print('vL·nP =', vL.dot(nP), 'so line is parallel to plane. OK to proceed...')
    # STEP2: find vector w=pP-pL arbitrary points on plane nad line,
    pP = Matrix([1,0,0])
    w = pP - pL
    # then compute component of w in perp. to the plane    d(L,P)=||proj_{nP}(w)||
    ( (nP.dot(w)/nP.norm()**2)*nP).norm()

    '''
    '''

    # # debug viz to make sure L parallel to P
    # plot_vec(5*vL, at=pL, color='b')
    # plot_vec(-5*vL, at=pL, color='b')
    # plot_vec(w, color='r', at=pL)
    # plot_plane(nP,dP)

    '''
    '''

    '''
    '''

    '''
    '''

    '''
    ### E4.7
    '''

    '''
    '''

    # Setup the variables of the exercise:
    p = Point([10,10,10])
    pL1 = Point([3,0,5])   # an arbitrary point on L1
    pL2 = Point([6,0,0])   # an arbitrary point on L2
    d = Vector([1,-2,0])   # direction vector of L1 and L2

    '''
    '''

    '''
    a) Projection of $p$ onto $\ell_1$
    '''

    '''
    '''

    # define a vector from a point on L1 to p:
    v1 = p - pL1

    # proj_{L1}(p) = proj_{L1}(v1) + pL1
    p_proj_L1 = (d.dot(v1)/d.norm()**2)*d + pL1
    p_proj_L1

    '''
    '''

    '''
    ^ This is the point on the line $\ell_1$ that is closes to the point $p$
    '''

    '''
    '''

    '''
    b) (shortest) distance form $p$ to $\ell_1$
    '''

    '''
    '''

    # d(p, L1) = subtract from v1 the part that is perp to L1 and compute the length:
    (v1 - (d.dot(v1)/d.norm()**2)*d).norm()

    '''
    '''

    # ... or compute the distance directly:
    (p_proj_L1-p).norm()

    '''
    '''

    (p_proj_L1-p).norm().n()  # numeric approx.

    '''
    '''

    plot_line(d, pL1)  # Line L1

    # vector v1 and it's decomposition into parallel-to-L1 and perp-to-L1 components
    plot_vec(v1, at=pL1)
    plot_vec(p_proj_L1-pL1, at=pL1,     color='b')
    plot_vec(p-p_proj_L1, at=p_proj_L1, color='r')

    ax = mpl.gca()
    mpl.xlim([0,10])
    mpl.ylim([0,10])
    ax.set_zlim([0,10])
    ax.grid(True,which='both')

    '''
    '''

    '''
    Answer to a) is the tip of the blue vector; answer to b) is the length of the red vector.
    '''

    '''
    '''

    '''
    '''

    '''
    Use a similar approach for c) and d)
    '''

    '''
    '''

    # define a vector from a point on L2 to p:
    v2 = p - pL2

    '''
    '''

    # p_proj_L2 =
    (d.dot(v2)/d.norm()**2)*d + pL2

    '''
    '''

    # d(p, L2) =
    (v2 - (d.dot(v2)/d.norm()**2)*d).norm()

    '''
    '''

    (v2 - (d.dot(v2)/d.norm()**2)*d).norm().n()

    '''
    '''

    '''
    '''

    '''
    '''

    '''
    e) distance $\ell_1$ to $\ell_2$
    '''

    '''
    '''

    # first define a vector from a point on L1 to a point on L2:
    v3 = pL2 - pL1
    v3

    '''
    '''

    # d(L1, L2) =
    d_L1L2 = (v3 - (d.dot(v3)/d.norm()**2)*d).norm()
    d_L1L2

    '''
    '''

    d_L1L2.n()

    '''
    '''

    '''
    '''

    '''
    '''

    '''
    '''


###_____________________________ Field Theory ______________________________###
import numpy as np
import matplotlib.pyplot as plt

##_______________________ Drawing a circumference ______________________##
# Como paso previo a la creación de un anillo circular crearemos una circunferencia,
# ya que es un ejemplo más sencillo

# 1. __Creamos `N` puntos de la circunferencia__ de modo que __en `Xc` estén las
# coordenadas $x$ y en `Yc` estén las coordenadas $y$__ de los puntos que la
# forman. Controla el número de puntos mediante un parámetro que se llame `N_puntos`.
    #     $$X_c = x0 + R·cos(\theta)$$
    #     $$Y_c = y0 + R·sin(\theta)$$
# 2. Una vez hayas obtenido los dos arrays `Xc` e `Yc`, __píntalos mediante un
# `scatter`__ para comprobar que todo ha ido bien.
# 3. Pinta también el __centro de la circunferencia__.

# El resultado debería ser parecido a esto:

# Número de puntos de la circunferencia
N_puntos = 100

# Centro
x0 = 0
y0 = 1

#Radio
R = 2

# Se barre un ángulo de 0 a 2 pi
theta = np.linspace(0, 2*np.pi, N_puntos)

# Se crean las coordenadas del los puntos
# de la circunferencia
Xc = x0 + R * np.cos(theta)
Yc = y0 + R * np.sin(theta)

# Lo representamos
plt.figure("Circumference", figsize=(5,5))
plt.title('Circumference', {'fontsize':20})
plt.scatter(Xc, Yc)
plt.scatter(x0, y0, color='orange', marker='x', s=100)
plt.grid()
plt.show()

##________________________ 2D plate with a ring shape _______________________##
# Radial Direction:
Nr = 20  # Número de puntos en la dirección radial.
R_min = 1  # m.
R_max = 5  # m.

# Tangencial Direction:
Nt = 180   # Número de puntos en la dirección tangencial.

r = np.linspace(R_min, R_max, Nr)
t = np.linspace(0, 2*np.pi , Nt)

# Crear la malla:
xx =  r * np.cos(t).reshape([-1, 1])
yy = r * np.sin(t).reshape([-1, 1])

#pintamos la malla para verla
plt.figure(figsize=(6, 6))
plt.plot(xx.flatten(), yy.flatten(), 'k.')
plt.show()

##________________________ Temperature distribution _________________________##
# Imaginemos que nuestra placa se somete a una distribución de temperaturas según
# la función:
    # $$T = -  \log(\sqrt{x^2 + y^2}) $$
# Define una función que dados unos valores de x e y, devuelva el valor de la
# temperatura en el punto.

def temperature_distribution(x, y):
    """Calculate the temperature for a point defined by
    its cartesian coordinates: x, y.
    """
    return -np.log(np.sqrt(x**2 + y**2))

# Calculate temperature values
temps = temperature_distribution(xx, yy)

# Show
plt.figure(figsize=(6, 6))
plt.title('Distribución de temperaturas', fontsize=20)
plt.contour(xx, yy, temps, 10, colors='black', linestyles='-')
plt.contourf(xx, yy, temps, 100, cmap=plt.cm.Spectral) # matplotlib trae por defecto muchos mapas de colores. En las SciPy Lecture Notes dan una lista de todos ellos (http://scipy-lectures.github.io/intro/matplotlib/matplotlib.html#colormaps)
plt.colorbar(fraction=0.045)
plt.gca().set_aspect(1)
plt.show()

# Como más adelante calcularemos el gradiente, quizá sea útil representar la
# temperatura en la placa como una superficie:
from mpl_toolkits.mplot3d import Axes3D

plt.figure('Surface')
ax = plt.subplot(projection='3d')
ax.plot_surface(xx, yy, temps, rstride=5, cstride=5, cmap=plt.cm.Spectral)
plt.title('Surface Plot')
plt.show()
# Más ejemplos de gráficas en 3d en http://matplotlib.org/mpl_toolkits/mplot3d/tutorial.html

##___________________ Temperature Gradient distribution _____________________##
# En primer lugar definiremos una función que calcule el gradiente en cualquier
# punto x, y:
def temp_gradient(x, y):
    """Calculate the temperature gradient for a point
    defined by its cartesian coordinates: x, y.
    """    # Could also use SymPy to calculate derivative tbh
    fx = -x / (x**2 + y**2)
    fy = -y /  (x**2 + y**2)
    return fx, fy

# Find temp gradient
fx, fy = temp_gradient(xx, yy)

# Utilizaremos ahora la función `quiver` de matplotlib para representar los
# vectores del gradiente de la función

# tip: hacer todos los vectores de módulo unitario y representar la norma del
      # gradiente con un colormap.
norm = np.sqrt(fx**2 + fy**2)
fx_norm = fx / norm
fy_norm = fy / norm
plt.figure(figsize=(10, 10))
plt.quiver(xx, yy, fx_norm, fy_norm, norm, scale=40, cmap=plt.cm.Spectral)
plt.show()

# Y quizá otra idea útil sería pintar menos flechas. Para eso podemos reducir
# el número de puntos indexando los arrays, por ejemplo saltando los elementos
# impares.
plt.figure(figsize=(10, 10))
plt.quiver(xx[::2,::2], yy[::2,::2], fx_norm[::2,::2], fy_norm[::2,::2], norm[::2,::2], scale=30, cmap=plt.cm.Spectral)
plt.show()

##___________________________ Displacement Field ____________________________##
# Aplicaremos ahora un campo de desplazamientos de la forma:
    # $$  \vec u(\rho,\theta)=\frac{\sin(\pi \theta/2)}{30\rho}\vec g_{\rho} $$

def desplazamientos(x, y):

    u = np.sqrt(x**2 + y**2)
    v = np.arctan2(y, x)

    ux = np.sin(np.pi * v / 2) / (30 * u) * np.cos(v)  # componente x de los desplazamientos
    uy = np.sin(np.pi * v / 2) / (30 * u) * np.sin(v)    # componente y de los desplazamientos

    return ux, uy

ux, uy = desplazamientos(xx, yy)

plt.figure(figsize=(10, 10))
plt.quiver(xx[::2,::2], yy[::2,::2], ux[::2,::2], uy[::2,::2], scale=0.5, cmap=plt.cm.Spectral)
plt.show()

# Plot the solid before and after the displacement
plt.figure(figsize=(10, 10))

plt.subplot(121)
plt.plot(xx.flatten(), yy.flatten(), 'k.')
plt.gca().set_aspect(1)

plt.subplot(122)
plt.plot(xx.flatten() + 10 * ux.flatten(),
            yy.flatten() + 10 * uy.flatten(), 'k.')
plt.gca().set_aspect(1)
plt.show()

##___________________ Normal Tensions in radial direction ___________________##
# La expresión que proporciona las tensiones, conocidas las deformaciones es:

    # $$ \sigma_{ij}=\lambda \nabla \cdot \vec u \delta_{ij} + 2\mu \epsilon_{ij} $$

# Donde $\lambda$ y $\mu$ son conocidos como los coeficientes de Lamé que dependen
# de las propiedades elásticas de cada material y $\epsilon_{ij}$ es la parte
# simétrica del tensor gradiente del campo vectorial $\vec u$.

# Tomando $\lambda =1$ y $\mu =1$, el tensor de deformaciones en forma matricial
# resulta:

    # $$\sigma_{ij}= \begin{bmatrix} \frac{-\sin(\pi \theta/2)}{15\rho^2} & \frac{\pi\cdot\cos(\pi \theta/2)}{60\rho} & 0 \\ \frac{\pi\cdot\cos(\pi \theta/2)}{60\rho} & \frac{\sin(\pi \theta/2)}{15\rho^2} & 0 \\ 0 & 0 & 0 \end{bmatrix}$$

# Que da lugar a una tensiones normales en la dirección radial:

    # $$ \vec g_{\rho} \cdot \sigma \cdot \vec g_\rho\ $$

def tensiones_normales(x, y):
    u = np.sqrt(x**2 + y**2)
    v = np.arctan2(y, x)

    tens = - np.sin(np.pi * v / 2) / (15 * u**2)

    return tens

ff = tensiones_normales(xx, yy)

plt.figure(figsize=(6, 6))
plt.contourf(xx, yy, ff, 50, cmap=plt.cm.Spectral)
plt.colorbar(fraction=0.045)
plt.gca().set_aspect(1)
plt.show()

# ¿Cómo se ajusta el espacio entre gráficas para que no se solapen los textos?
# Buscamos en Google "plt.subplot adjust" en el primer resultado tenemos la
# respuesta http://stackoverflow.com/a/9827848


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import numpy as np
from scipy import linalg as LA
import itertools
import matplotlib.pyplot as plt
import matplotlib
import sys
import pdb

#problem 10.4, Marder p. 287

c = np.array([[ 0.25819889+0.51639777j,
                0.77459663+0.25819889j,
                0.00000000+0.j        ,
                0.00000000+0.j        ],

              [-0.39235696-0.24968168j,
                 0.32101944-0.07133763j,
                0.10700645+0.74904513j,
                0.32101935+0.j        ],

              [ 0.30385175+0.13841607j,
               -0.22113398+0.08271784j,
                0.09764974-0.08722103j,
                0.88951313+0.15619215j],

              [ 0.00000000+0.j        ,
                2.00000000+0.j        ,
                1.00000000+0.j        ,
                0.00000000+0.j        ]])

def GramSchmidt(c,i):
    # assume columns 0 through i-1 are already mutually orthonormal
    # generate a new orthonormal vector in column i
    # equivalent to generating a basis vector by Gram-Schmidt process, (i.e. http://ltcconline.net/greenl/courses/203/Vectors/orthonormalBases.htm )
    v = c[i]
    for j in range(i):
        v -= (np.vdot(c[i],c[j].transpose()) / np.vdot(c[j],c[j].transpose())) * c[j]

    c[i] = v/LA.norm(v)

    #verify that we're orthonormal to within 1e-6 (should avoid any floating point errors)
    assert(np.allclose(0, [abs(np.vdot(c[j],c[k].transpose())) for j,k in itertools.combinations(range(i),2)], atol=1e-6))
    assert(np.allclose(1, [abs(np.vdot(c[j],c[j].transpose())) for j in range(i)], atol=1e-6))

GramSchmidt(c,i=3) # indices begin at 0, equivalent to calling with i=4 of problem statement

#print(c)
#print(c[3])

#problem 10.5

#Pseudopotential parameters for Aluminum

d = 0.350 # angstrom
rc = 0.943 # angstrom
U0 = -31.30 # eV

def U(K):
    Kn = LA.norm(K)
    return U0 * np.exp(-rc/d) * ((np.sin(rc*Kn) / (d*Kn * ((d*Kn)**2 + 1)) + (np.cos(Kn*rc))/((d*Kn)**2 + 1)))

# a)
#print(U(0))
#this gives an error
#however, the limit does exist:
# U0 * math.exp(-rc/d) * ((math.sin(rc*Kn) / (d*Kn * ((d*Kn)**2 + 1)) + (math.cos(Kn*rc))/((d*Kn)**2 + 1)))
# the cosine term goes to 1, while the sin term goes to rc*Kn / (d*Kn * ((d*Kn)**2 + 1)) == rc / d
# lim_{k -> 0} U(K) == U0 * math.exp(-rc/d)*(rc/d + 1)

#modify U(K) to show this behavior

def U(K):
    Kn = LA.norm(K)
    if Kn > 0:
        return U0 * np.exp(-rc/d) * ((np.sin(rc*Kn) / (d*Kn * ((d*Kn)**2 + 1)) + (np.cos(Kn*rc))/((d*Kn)**2 + 1)))
    else:
        assert(Kn==0)
        return U0*np.exp(-rc/d)*(rc/d+1)

# b)

# Al is an fcc lattice with a spacing of a = 4.05 angstroms
a = 4.05 # angstrom
(a1,a2,a3) = 0.5 * a * np.array([[1,1,0],[1,0,1],[0,1,1]])

# the lattice's reciprocal vectors are given by equation 3.24
b1 = 2 * np.pi * np.cross(a2,a3) / np.dot(a1,np.cross(a2,a3))
b2 = 2 * np.pi * np.cross(a3,a1) / np.dot(a2,np.cross(a3,a1))
b3 = 2 * np.pi * np.cross(a1,a2) / np.dot(a3,np.cross(a1,a2))

# c)

# such a routine is available in my environment,
# numpy.fft.fftn(array) will compute an n-dimensional fast Fourier transform

# d)
# given N (of part e, below), for what values of r will an fft routine compute U(r) = sum over K ( e^(iK.r) U_K ), and how will they be indexed?

# FFT will generate an NxNxN array of values, from an equal sized array of U(K) ranging over the reciprocal lattice vectors
# given r values will be the corresponding physical lattice vector sums, so that exp(i*K.r) == 1

# e)

def l(ji,N):
    return ji-N if ji+1 > (N+1)/2 else ji
#    return np.ceil(ji-np.ceil(N/2))

def generate_lattice(N):
    K = np.empty([N,N,N], dtype=np.ndarray)
    r = np.empty(K.shape, dtype=np.ndarray)
    for i,j,k in np.ndindex((N,N,N)):
            K[i,j,k] = l(i,N)*b1 + l(j,N)*b2 + l(k,N)*b3
            r[i,j,k] = (1/N)*(i*a1 + j*a2 + k*a3)
    return K,r

def generate_potential(N,K,n):
    vector_U = np.vectorize(U)
    #K_offset = K[list(np.ndindex((N,N,N)))[n]]
    #offsets = np.zeros([N,N,N],dtype=np.ndarray); offsets.fill(K_offset)
    return vector_U(K)

eV_to_Ryd = 1/13.6
'''
#pdb.set_trace()
K, r = generate_lattice(N=5)
U_K = generate_potential(5,K,0)
U_r = np.fft.fftn(U_K)
for i,j,k in np.ndindex((5,5,5)):
    print(r[i,j,k],U_r[i,j,k]*eV_to_Ryd) # convert to angstroms, Rydbergs
'''
# problem 10.6

# a)
c = 3.80998 # hbar**2 / (2*m) in  Angstroms^2 * eV

def convolve(a,b,N):
    return (1/N)**3 * np.fft.fftn(np.multiply(np.fft.ifftn(a),np.fft.ifftn(b)))

def compute_psi(psi,K_sq,Emax,U_K,N):
    return np.multiply(c*K_sq-Emax,psi) + convolve(psi,U_K,N)

# b)

vectorized_norm = np.vectorize(LA.norm)

def get_closest_index(arr,val,N):
    minm = np.inf
    ind = None
    for i,j,k in np.ndindex((N,N,N)):
        dist = LA.norm(arr[i,j,k] - val)
        if dist < minm:
            minm = dist
            ind = (i,j,k)
    return ind

#find energy, use (7.33)
def energy(psi,q,U_K,K,N):
    qeff = get_closest_index(K,q,N)
    assert(qeff is not None)
    return np.real((c*np.dot(q,q)*psi[qeff] + convolve(psi,U_K,N)[qeff]) / psi[qeff])

def get_converged_psi(n,K,U_K,N): # n selects band, 0-indexed

    psi = np.zeros([N,N,N], dtype='complex128')
    psi[0,0,0] = 1
    #K_offset = K[list(np.ndindex((N,N,N)))[n]]
    offsets = np.zeros([N,N,N])#,dtype=np.ndarray); offsets.fill(K_offset)
    K_sq = vectorized_norm(np.multiply(K+offsets,K+offsets))

    Emax = c * np.max(K_sq) # E at maximum value of K

    for _ in range(1000):
        psi = compute_psi(psi,K_sq,Emax,U_K,N)
        psi = psi / np.sqrt(np.vdot(psi,psi))

    return psi

def get_ek(K,k):
    a=K+k
    return c * a.dot(a)
vector_get_ek = np.vectorize(get_ek,excluded=[1]) #only vectorize zeroth argument

def get_H_block(k,K,U_K,N):

    eks = vector_get_ek(K,k)

#    H = np.zeros((N**3,N**3), dtype='float64')
    H = np.diagflat(eks)
    m,n = np.indices(H.shape)
    H += U_K.flat[np.abs(m-n)]
#        H[j,j] = (hbar**2 * LA.norm(k)**2)/(2*m)
    #assert(np.allclose(H.real,H)) # is real
    return H

'''
psi = get_converged_psi(0,K,U_K,N=5)
ground_energy = LA.eigvals(get_H_block(np.array([0.,0.,0.]),U_K,N=5))[0]
print(ground_energy*eV_to_Ryd)
'''

#10.7
'''
bands = 6
N=8
k = (2*np.pi / a) * np.array([0.4,0.4,0.4])
K, r = generate_lattice(N)
U_K = generate_potential(N,K,0)
#psi = get_converged_psi(0,K,U_K,N)
H = get_H_block(k,U_K,N)

print(LA.eigvals(H*eV_to_Ryd)[:bands])#,eigvals=(0,bands-1)))
'''
#10.8
bands=6

L     = 2*np.pi/a*np.array([1/2,1/2,1/2])
gamma = 2*np.pi/a*np.array([  0,  0,  0])
X     = 2*np.pi/a*np.array([  0,  1,  0])
pointU= 2*np.pi/a*np.array([1/4,  1,1/4])
W     = 2*np.pi/a*np.array([1/2,  1,  0])

path = [L,gamma, X, pointU,gamma]
labels = ['L', r'$\Gamma$', 'X', 'U', r'$\Gamma$']
ksteps = 50
N = 12
energies = np.zeros([ksteps*(len(path)-1),bands])
kscale = np.zeros([ksteps*(len(path)-1)])
K,r = generate_lattice(N)
U_K = generate_potential(N,K,0)

label_locations = []

for i in range(0,len(path)-1):
    label_locations.append(i*ksteps)
    kstep = (path[i+1]-path[i]) / ksteps
    for step in range(ksteps):
        qval = path[i]+step*kstep
        #pdb.set_trace()
        H = get_H_block(qval,K,U_K,N)
        #free particle test
#        for band in range(bands):
#            energies[step+(i)*ksteps,band] = LA.norm(qval+ordered_K[band])**2 * (hbar**2 / (2*m))
#        import timeit
#        t = timeit.Timer('H2 = get_H_block(qval,K,U_K)', setup='from __main__ import get_H_block,efficient_new_H,generate_lattice,generate_potential,U; import numpy as np; import scipy.linalg as LA; qval=np.array([0.,0.,0.]); N=12; K,r = generate_lattice(N); U_K = generate_potential(K)').timeit(number=100)
#        print(t)
#        t = timeit.Timer('H = efficient_new_H(H,qval,K,U_K)', setup='from __main__ import get_H_block,efficient_new_H,generate_lattice,generate_potential,U; import numpy as np; import scipy.linalg as LA; qval=np.array([0.,0.,0.]); N=12; K,r = generate_lattice(N); U_K = generate_potential(K);H = get_H_block(np.array([0.,0.,0.]),K,U_K)').timeit(number=100)
#        print(t)
        '''
        if not np.allclose(H.real,H):
            print("complex H!")
            pdb.set_trace()
        '''
        energies[step+i*ksteps,:] = LA.eigvalsh(H,eigvals=(0,bands-1))
        kscale[step+i*ksteps] = LA.norm(kstep)

label_locations.append(step+i*ksteps)
kvals = np.cumsum(kscale)

font = {'family' : 'serif',
       'weight' : 'bold',
        'size'   : 18}
matplotlib.rc('text', usetex=True)
matplotlib.rcParams['text.latex.preamble']=[r"\boldmath"]
matplotlib.rc('font', **font)
matplotlib.ticker.ScalarFormatter(useMathText=True)

plt.ylabel('band energy (eV)')
plt.xticks([kvals[x] for x in label_locations], labels)
plt.axes().xaxis.grid(True,which='major')
plt.axes().tick_params(pad=18)

for n in range(bands):
    plt.plot(kvals,energies[:,n])

plt.savefig('al_bands.pdf',bbox_inches='tight')


from sympy import Symbol

x = Symbol( 'x' )

x + x + 1

x.name

a = Symbol( 'x' )
a.name

from sympy import symbols

x,y,z = symbols( 'x,y,z' )

x = Symbol( 'x' )
y = Symbol( 'y' )

s = x*y + x*y

s

p = ( x + 2) * ( x + 3 )
p

x = Symbol( 'x' )
y = Symbol( 'y' )

from sympy import factor, expand
expr = x**2 - y**2

factor( expr )

expr = x**3 + 3*x**2*y + 3*x*y**2 + y**3
factors = factor( expr )

factors

expand( factors )

expr = x + y + x*y
factor( expr )

expr = x*x + 2*x*y + y*y
print( expr )

from sympy import pprint

pprint( expr )

expr = x**2 + 2*x*y + y**2

pprint( expr )

expr = 1 + 2*x + 2*x**2
pprint( expr )

from sympy import init_printing
init_printing( order = 'rev-lex' )
pprint( expr )

expr = x*x + x*y + x*y + y*y
res = expr.subs( {x:1-y } )
res

from sympy import simplify
expr = x*x + x*y + x*y + y*y
expr_subs = expr.subs( {x:1-y } )
simplify( expr_subs )


import numpy as np
from numpy import exp
import matplotlib.pyplot as plt

def u(x):

    return 3.0 / (5.0 - 4.0 * np.cos(x))

def IN(x, z, u, N):

    n = np.arange(-N/2, N/2 + 1)
    cn = []
    u_hat = []

    for i in range(len(n)):

        if abs(n[i]) == N/2:
            ci = (1.0 / (2 * N)) * sum(u(x) * exp(-1j * n[i] * x))
            cn.append(ci)

        else:
            ci = (1.0 / N) * sum(u(x) * exp(-1j * n[i] * x))
            cn.append(ci)

    for k in range(len(z)):
        ui = sum(cn * np.exp(1j * n * z[k]))
        u_hat.append(ui.real)

    return u_hat

M=100
T = 2 * np.pi
z=np.linspace(0, 2.0 * np.pi, M)

fig, ax = plt.subplots(1, 2, figsize=(10,5))
ax[0].plot(z / T, u(z))

legend = [r'$u(x)$']
for N in 2**np.arange(2, 5):
    x = []
    for j in range(N):
        xj = 2.0 * np.pi * j / N
        x.append(xj)
    x = np.array(x)
    u_approx = IN(x, z, u, N)
    ax[0].plot(z / T, u_approx, '--')
    legend = legend + ['N' + ' = ' + str(N)]

ax[0].set_xlim(0, 1.0)
ax[0].set_ylim(0, 4.0)
ax[0].legend(legend)
ax[0].set_xlabel(r'$x / 2 \pi$', fontsize=13)
ax[0].set_ylabel(r'$\mathcal{I}_N u$', fontsize=13)
ax[0].set_title('(a)', loc='left')

legend = []
for N in 2**np.arange(2, 7):
    x = []
    z = []
    for j in range(N):
        xj = 2.0 * np.pi * j / N
        x.append(xj)
    M = 200 * N
    for i in range(M):
        zi = 2.0 * np.pi * i / M
        z.append(zi)
    z = np.array(z)
    x = np.array(x)

    u_approx = IN(x, z, u, N)
    Error = []
    cota=min(abs(u(z) - u_approx))
    for k in range(len(z)):
        ei = abs(u(z[k]) - u_approx[k])
        if ei < cota:
            ei = 0
            Error.append(ei)
        Error.append(np.log(ei))
    Error = np.array(Error)
    ax[1].plot(z / T, Error, '--')
    legend = legend + ['N' + ' = ' + str(N)]

ax[1].set_xlim(0, 1)
ax[1].set_ylim(-33, 0)
ax[1].legend(legend)
ax[1].set_xlabel(r'$x / 2 \pi$', fontsize=13)
ax[1].set_ylabel(r'$\ln |u - \mathcal{I}_N u|$', fontsize=13)
ax[1].set_title('(b)', loc='left')

fig.savefig('example27')

plt.show()


import numpy as np

#             Input:
#               data       - Data matrix. Each row vector of fea is a data point.
#
#     options.ReducedDim   - The dimensionality of the reduced subspace. If 0,
#                         all the dimensions will be kept.
#                         Default is 0.
#
#             Output:
#               eigvector - Each column is an embedding function, for a new
#                           data point (row vector) x,  y = x*eigvector
#                           will be the embedding result of x.
#               eigvalue  - The sorted eigvalue of PCA eigen-problem.

def PCA(data, ReducedDim = 0):
    [nSmp, nFea] = data.shape
    if (ReducedDim > nFea) or (ReducedDim <= 0):
        ReducedDim = nFea

    sampleMean = np.mean(data, 0)
    data = (data - np.tile(sampleMean, (nSmp, 1)))
    eigvector, eigvalue, temp = np.linalg.svd(np.dot(data.T,data))
    eigvector = eigvector.T[:ReducedDim].T
    eigvalue = eigvalue[:ReducedDim]
    return eigvector, eigvalue, sampleMean

if __name__ == "__main__":
    data = np.array([[1,2,3,4],[0.2,0.1,3,2],[0.8,0.9,0.4,0.1],[0,0.3,0,2],[1,1,3,3]])
    vec, val, mea = PCA(data, 3)
    print vec, '\n', val


import numpy as np

def trapeze_integrate(f, interval, npoints = 1e6):
    n = npoints
    n = int(n)

    a, b = interval
    delta = (b-a)/n

    x = (a + delta*(h) for h in range(1, n))

    v = (delta/2)*(f(a)+f(b)) + (delta*sum((f(x_k) for x_k in x)))

    return v

def simpson_integrate(f, interval, method = '1/3', npoints = 1e6):

    npoints = int(npoints)
    n = npoints

    a, b = interval

    if method == '1/3':

        delta = (b-a)/(2*n)
        x = [a + delta*(h) for h in range(1, 2*n)]

        v =   (  delta/3)*   (f(a)+f(b)) \
            + (2*delta/3)*sum(f(x[2*h+1]) for h in range(int(n-1))) \
            + (4*delta/3)*sum(f(x[2*h]) for h in range(int(n)))

    elif method == '3/8':
        # something is not working properly here...
        delta = (b-a)/(3*n)
        x = [a + delta*(h) for h in range(1, 3*n)]

        v =   (3*delta/8)*   (f(a)+f(b)) \
            + (6*delta/8)*sum(f(x[3*h+1]) for h in range(int(n-1))) \
            + (9*delta/8)*sum(f(x[3*h-1])+f(x[3*h]) \
                    for h in range(int(n)))

    else:
        raise ValueError("invalid value for method: {!s}".format(method))

    return v

def recursive_integrate(f, interval, method = 'simpson',
    iters = 15, prec = None, verbose = False):

    if prec is not None:
        iters = 2

    a, b = interval

    t = (b-a)/2 * (f(a)+f(b))
    s = None
    v = None

    i = 0
    while i < iters:

        pt, ps = t, s
        pv = ps if method in 'simpson' else pt

        n = int(2**i)

        delta = (b-a)/2**(i+1)
        x = [a + delta*h for h in range(1, 2*n)]

        t = pt/2 + delta * sum(f(x[2*i]) for i in range(n))
        s = (4*t-pt)/3

        v = s if method in 'simpson' else t

        i += 1
        try:
            if prec is not None and i == iters and abs(v-pv) > prec:
                iters += 1
        except Exception as e:
            try:
                olderr
            except NameError:
                olderr = True
            if olderr:
                raise(e)

    if verbose:
        print(f"result took {iters} iterations")

    return v

def romberg_integrate(f, interval, upgrade = 5, prec = None, showres = False):
    a, b = interval
    u = upgrade

    r = dict()

    t = (b-a)/2 * (f(a)+f(b))
    for j in range(1, u+1):
        try:
            r[j]
        except:
            r[j] = dict()
            r[j][1] = t
        pt = t
        n = int(2**(j-1))
        delta = (b-a)/2**j
        x = [a + delta*(h) for h in range(1, 2*n)]

        t = pt/2 + delta * sum(f(x[2*i]) for i in range(n))

    for j in range(1, u+1):
        for k in range(2, u+1):
            if j >= k:
                r[j][k] = (4**(k-1) * r[j][k-1] - r[j-1][k-1])/(4**(k-1)-1)

    if prec is not None:
        if abs(r[u][u-1] - r[u][u]) > prec:
            return romberg_integrate(f, interval, upgrade = u+1, prec = prec,
                showres = showres)

    if showres:
        return (r[u][u],
              [[r[n][j] for j in r[n].keys()] for n in r.keys()],
               [r[n][len(r[n])] for n in r.keys()])

    return r[u][u]

# if returning r from romberg_integrate:
# print(*[[r[n][j] for j in r[n].keys()] for n in r.keys()], sep = '\n') # matrix
# print(*[r[n][len(r[n])] for n in r.keys()], sep = '\n') # diagonal

import random
def montecarlo_integrate(f, interval, npoints = 1e6):
    n = int(npoints)
    a, b = interval
    v = b-a

    mf, mfsq = 0, 0
    for i in range(n):
        d = random.uniform(a, b)
        mf += f(d)
        mfsq += f(d)**2

    mf /= n
    mfsq /= n

    v, err = v*mf, v*np.sqrt((mfsq - mf**2)/n)
    return v, err


"""
Kindly install these libraries before executing this code:
  1. numpy
  2. matplotlib
  3. scipy
"""

import numpy as np
import matplotlib.pyplot as plt
import cmath
import math
from numpy import random
from scipy.special import beta
from scipy import stats
import time

# if using a Jupyter notebook, kindly uncomment following line:
%matplotlib inline

def plot(numbers, size, flag, mean = 0, var = 1):
  normalize = False
  if(flag):
    normalize = True
    plt.title('Density Function Comparison: Sample size = {}'.format(size))
  else:
    plt.title('Frequency Distribution: Sample size = {}'.format(size))

  hist, bins = np.histogram(numbers, bins = 80, normed = normalize)
  w = 0.7 * (bins[1] - bins[0])
  center = (bins[:-1] + bins[1:]) / 2
  plt.bar(center, hist, align = 'center', width = w, label = 'Sample PDF')
  if(flag == 0):
    plt.ylabel('Frequency')
  else:
    plt.ylabel('Probability (P(x))')
  plt.xlabel('X (generated values)')

  if(normalize):
    x = np.arange(mean - 7.5, mean + 7.5, 0.1)
    y = stats.norm.pdf(x, mean, math.sqrt(var))
    plt.plot(x, y, color = 'red', label = 'Theoretical PDF')
    plt.legend(loc = "upper left")

  plt.show()

def plot_cdf(numbers, size):
  sorted_Y = np.sort(numbers)
  y_val = np.arange(len(sorted_Y))/float(len(sorted_Y) - 1)

  plt.title('CDF of generated Random Numbers: Sample size = {}'.format(size))
  plt.ylabel('Probability - P(X <= x)')
  plt.xlabel('X (generated values)')
  plt.plot(sorted_Y, y_val, color = 'blue')
  plt.show()

def boxMuller():
  sample = [50, 5000]
  random_numbers = []

  for i in sample:
    time1 = time.time()
    U1 = np.random.uniform(size = i)
    U2 = np.random.uniform(size = i)

    R = [-2*math.log(ui) for ui in U1]
    V = [2*math.pi*ui for ui in U2]

    Z = []
    for idx in range(0, i):
      Z.append(math.sqrt(R[idx]) * math.cos(V[idx]))
      Z.append(math.sqrt(R[idx]) * math.sin(V[idx]))

    time2 = time.time()
    random_numbers.append(Z)

    print("\n\n****************** Box-Muller Method ******************")
    print("Theoretical Mean\t= 0")
    print("Theoretical Variance\t= 1")
    print("Sample Mean\t= {}".format(np.mean(Z)))
    print("Sample Variance\t= {}".format(np.var(Z)))
    print("Size of sample\t= {}".format(len(Z)))

    plot(Z, len(Z), 0)

  return random_numbers

def marsagliaAndBray():
  sample = [50, 5000]
  random_numbers = []

  for i in sample:
    X = []
    U1 = []
    U2 = []

    counter = 0
    time1 = time.time()
    while len(X) != i:
      u1 = np.random.random()
      u2 = np.random.random()

      counter += 1
      u1 = 2*u1 - 1
      u2 = 2*u2 - 1
      x = u1*u1 + u2*u2

      if(x > 1):
        continue

      X.append(x)
      U1.append(u1)
      U2.append(u2)

    Y = [math.sqrt(-2*math.log(x)/x) for x in X]

    Z = []
    for idx in range(0, i):
      Z.append(U1[idx]*Y[idx])
      Z.append(U2[idx]*Y[idx])

    time2 = time.time()
    random_numbers.append(Z)

    print("\n\n****************** Marsaglia and Bray Method ******************")
    print("Theoretical Mean\t= 0")
    print("Theoretical Variance\t= 1")
    print("Sample Mean\t= {}".format(np.mean(Z)))
    print("Sample Variance\t= {}".format(np.var(Z)))
    print("Size of sample\t= {}".format(len(Z)))

    plot(Z, len(Z), 0)

  return random_numbers

def normal_dist(sample1, sample2, mean, var):

  std = math.sqrt(var)
  for i in range(0, 2):
    modified_dist = []
    for idx in range(0, len(sample1[i])):
      modified_dist.append(mean + std*sample1[i][idx])

    print("\n\n****************** Box-Muller Method ******************")
    print("Theoretical Mean\t= {}".format(mean))
    print("Theoretical Variance\t= {}".format(var))
    print("Sample Mean\t= {}".format(np.mean(modified_dist)))
    print("Sample Variance\t= {}".format(np.var(modified_dist)))
    print("Size of sample\t= {}".format(len(modified_dist)))
    plot(modified_dist, len(modified_dist), 1, mean, var)
    plot_cdf(modified_dist, len(modified_dist))

  for i in range(0, 2):
    modified_dist = []
    for idx in range(0, len(sample2[i])):
       modified_dist.append(mean + std*sample2[i][idx])

    print("\n\n****************** Marsaglia and Bray Method ******************")
    print("Theoretical Mean\t= {}".format(mean))
    print("Theoretical Variance\t= {}".format(var))
    print("Sample Mean\t= {}".format(np.mean(modified_dist)))
    print("Sample Variance\t= {}".format(np.var(modified_dist)))
    print("Size of sample\t= {}".format(len(modified_dist)))
    plot(modified_dist, len(modified_dist), 1, mean, var)
    plot_cdf(modified_dist, len(modified_dist))

def main():
  print(" ---------------------- Solutions for sample from N(0, 1) ----------------------")
  sample1 = boxMuller()
  sample2 = marsagliaAndBray()

  print(" ---------------------- Solutions for sample from N(0, 5) ----------------------")
  normal_dist(sample1, sample2, 0, 5)

  print(" ---------------------- Solutions for sample from N(5, 5) ----------------------")
  normal_dist(sample1, sample2, 5, 5)

if __name__ == "__main__":
    main()


"""Tool for solving systems of linear equations
using iterative methods(Jacobi and Nekrasov methods).
Iterative method is a mathematical procedure
that uses an initial guess to generate a sequence
of improving approximate solutions for a class of problems,
in which the n-th approximation is derived from the previous ones.

Functions:
    perform_iteration_Nekrasov(A, b, x0) -> float
    perform_iteration_Jacobi(A, b, x0) -> float
    approximate_Nekrasov(A, b, x0, eps) -> float
    approximate_Jacobi(A, b, x0, eps) -> float
"""

from numpy import zeros

def perform_iteration_Nekrasov(A, b, x0):
    """Returns the next approximating value using the Nekrasov method.

    Arguments:
    A -- square matrix with diagonal predominance
    b -- vector
    x0 -- previous approximation
    """

    n = A.shape[0]
    x = zeros(n)
    x0 = x0.copy()
    for k in range(n):
        product_k = x0 * A[k]
        product_k[k] = 0
        x[k] = (b[k] - sum(product_k)) / A[k, k]
        x0[k] = x[k]

    return x

def perform_iteration_Jacobi(A, b, x0):
    """Returns the next approximating value using the Jacobi method.

    Arguments:
    A -- square matrix with diagonal predominance
    b -- vector
    x0 -- previous approximation
    """

    n = A.shape[0]
    x = zeros(n)
    for k in range(n):
        product_k = x0 * A[k]
        product_k[k] = 0
        x[k] = (b[k] - sum(product_k)) / A[k, k]

    return x

def approximate_Nekrasov(A, b, x0, eps):
    """Find the solution of SLE with a given accuracy
    using Nekrasov method.

    Arguments:
    A -- square matrix with diagonal predominance
    b -- vector
    x0 -- previous approximation
    eps -- accuracy
    """

    while True:
        prev = x0
        _log['Nekrasov'].append(prev)
        x0 = perform_iteration_Nekrasov(A, b, prev)
        if (sum(abs(x0 - prev))) < eps:
            break

    return x0

def approximate_Jacobi(A, b, x0, eps):
    """Find the solution of SLE with a given accuracy
    using Nekrasov method.

    Arguments:
    A -- square matrix with diagonal predominance
    b -- vector
    x0 -- previous approximation
    eps -- accuracy
    """

    while True:
        prev = x0
        _log['Jacobi'].append(prev)
        x0 = perform_iteration_Jacobi(A, b, prev)
        if (sum(abs(x0 - prev))) < eps:
            break

    return x0

_log = {'Nekrasov': [], 'Jacobi': []}


import numpy as np
import math
arr1=np.array([1,2])
print(np.std(arr1))

# print(np.average(arr1))
#
# print((48+43+62+44)/4.)

def compute_std(arr:[]):
    sum_arr=0.
    for a in arr:
        sum_arr+=a
    avg=sum_arr/len(arr)
    sum_2=0.
    for a in arr:
        sum_2+=(a-avg)**2
    return math.sqrt(sum_2/len(arr))

print(compute_std(arr1))


#coding:utf-8

# 主成分分析 PCA 是一种无监督学习方法。它保留了数据中的绝大部分变化，也就是数据分布的方向被最大程度地保留下来.
# 特征值，特征向量

# PCA 算法
# 1. 将数据集标准化，均值0
# 2. 找出数据集的相关矩阵和单位标准偏差值
# 3. 将相关矩阵分解成它的特征向量和值
# 4. 基于降序的特征值选择 Top—N 特征向量
# 5. 投射输入的特征向量矩阵到一个新的子空间。

import numpy as np
from sklearn.datasets import load_iris
from sklearn.preprocessing import scale
import scipy
import matplotlib.pyplot as plt

data = load_iris()
x = data['data']
y = data['target']

x_s = scale(x, with_mean=True, with_std=True, axis=0)

#
x_c = np.corrcoef(x_s.T)

eig_val, r_eig_vec = scipy.linalg.eig(x_c)

print 'Eigen values \n %s'%(eig_val)
print '\n Eigen vectors \n %s'%(r_eig_vec)

w = r_eig_vec[:,0:2]
x_rd = x_s.dot(w)

plt.figure(1)
plt.scatter(x_rd[:,0], x_rd[:,1], c=y)
plt.xlabel('1')
plt.ylabel('2')

#plt.show()

print "Component, Eigen Value, % of Variance, Cummulative %"
cum_per = 0
per_var = 0

print eig_val

for i,e_val in enumerate(eig_val):
    per_var = round((e_val.real / len(eig_val)),3)
    cum_per+=per_var
    print ('%d, %0.2f, %0.2f, %0.2f')%(i+1, e_val.real, per_var*100,cum_per*100)

# 将相关矩阵分解为特征向量和特征值是一种通用的技术，可以应用到任意矩阵
# 目的是了解数据分布的主轴，通过主轴观察数据的最大变化
# PCA 可以用来进行探索数据，也可以用来给后续算法做准备，降维只保留最相关的特征给后续分类算法

# PCA 的一个缺点是运算代价昂贵.
# Numpy 里的 corrcoef 函数内置了数据标准化


import sympy as sp

def bisection(f, x, xl, xr):
    """
    Use bisection method to find the roots of an equation.

    params:
    f - the function equation
    x - the Sympy Symbol object
    xl - x left boundary
    xr - x right boundary

    returns the approximation of the root of the equation.
    """
    fxl = f.evalf(subs={x : xl})
    xm = 0

    while True:
        xm = (xl + xr)/2
        fxl = f.evalf(subs={x : xl})
        fxm = f.evalf(subs={x : xm})
        if fxl * fxm < 0:
            xr = xm
        else:
            xl = xm

        if abs(fxm) < 10**(-4):
            break

    return xm

def newton(f, x, df, xVal):
    """
    Use Newton's method to find the root of an equation.

    params:
    f - the function equation
    x - the Sympy Symbol object
    df - the derivative function
    xVal - starting point of x
    """
    while True:
        fx = f.evalf(subs={x : xVal})
        xVal = xVal -  fx/ df.evalf(subs={x : xVal})

        if abs(fx) < 10**(-4):
            break

    return xVal

def secant(f, x, x1, x2):
    xNew = 0

    while True:
        fx1 = f.evalf(subs={x : x1})
        fx2 = f.evalf(subs={x : x2})
        denominator = fx2 - fx1
        if denominator != 0:
            xNew = x2 - ((x2-x1)/denominator) * fx2
            if abs(xNew - x2) < 10*(-4):
                break
            else:
                x1 = x2
                x2 = xNew
        else:
            break

    return xNew


import sympy as sym
x, y = sym.symbols('x,y')
eq1 = sym.Eq(x+y, 5)
eq2 = sym.Eq(x**2+y**2, 17)
result = sym.solve([eq1, eq2], (x, y))
print(result)

'''
[(1, 4), (4, 1)] #these are the solutions for x,y. There are 2 solutions because the equations represent a line passing through a circle.
'''


from numpy import exp, sqrt, pi

# gaussian normalized a unity area
def gaussian(mu, sigma, x):
  var = sigma**2
  # calculates the probability of x for 1-dim Gaussian with mean mu and var. sigma
  return exp(- ((mu - x) ** 2) / ( 2.0 * var)) / sqrt(2.0 * pi * var)

# gaussian normalized a unity peak
def ngaussian(mu, sigma, x):
  var = sigma**2
  # calculates the probability of x for 1-dim Gaussian with mean mu and var. sigma
  return exp(- ((mu - x) ** 2) / ( 2.0 * var))


# bernstein_polynomial.py
import math
import numpy as np

def bernstein_polynomial(i: int, p: int, nti: int = 2, verbose: bool = False):
    """Computes the Bernstein polynomial

    Args:
        i (int): control point, i >= 0 and i <=p
        p (int): polynomial degree, p >= 1
        nti (int): number of time intervals for t in [0, 1]
            intervals are equidistant
            nti = 2 is default => t = [0.0, 0.5, 1.0]
            nti >=2
        verbose (bool): prints polynomial or error checking

    Returns:
        array: of floats, the polynomial evaluated at t values.
    """

    if i >= 0 and p >= 1 and nti >= 2 and i <= p:
        t = np.linspace(0, 1, nti + 1)
        bp = (
            math.factorial(p)
            / (math.factorial(i) * math.factorial(p - i))
            * t ** i
            * (1 - t) ** (p - i)
        )
        if verbose:
            print(f"Bernstein polynomial = {bp}")

        return bp

    else:
        if verbose:
            print(f"Input (i, p, nti) = ({i}, {p}, {nti}) is out of range.")
            print("i is non-negative integer 0, 1, 2, ... p")
            print("p is integer >= 1")
            print("nti is integer >= 2")

        return None


import numpy as np

# ----- Helper functions linear regression -----------------------------------------------------------
def calculate_mse(e):
    """Calculate the mse for vector e."""
    return 1/2 * np.mean(e**2)

def compute_loss_mse(y, tx, w):
    """Calculate the loss using mean squared error loss function"""
    e = y - tx @ w
    return calculate_mse(e)

def compute_gradient(y, tx, w):
    """Compute the gradient."""
    N = len(y)
    e = y - tx @ w
    gradient = -(1.0 / N) * (tx.T @ e)
    return gradient, e

# ----- Implement ML methods -------------------------------------------------------------------------
# Linear regression using gradient descent
def least_squares_GD(y, tx, initial_w, max_iters, gamma):
    """Gradient descent algorithm."""
    weights = initial_w
    for n_iter in range(max_iters):
        # Compute gradient and loss
        gradient, e = compute_gradient(y, tx, weights)
        loss = calculate_mse(e)
        # Update w by gradient
        # print("loss", loss)
        weights = weights - gamma * gradient

    # return the last w and loss
    return weights, loss

# Linear regression using stochastic gradient descent
def least_squares_SGD(y, tx, initial_w, max_iters, gamma):
    """Stochastic gradient descent with batch size of 1"""
    np.random.seed(1)
    weights = initial_w
    min_weights = weights
    min_loss = calculate_mse(y - tx.dot(weights))
    for n_iter in range(max_iters):
        #Select a random element of y and tx
        r = np.random.randint(0, len(y))
        y_elem = np.array([y[r]])
        tx_elem = np.array([tx[r]])
        # Compute its stochastic gradient
        gradient, err = compute_gradient(y_elem, tx_elem, weights)
        # Update w
        weights = weights - gamma * gradient

        #Compute loss using mean squared error
        loss = calculate_mse(y - tx.dot(weights))
        if(loss < min_loss):
            min_loss = loss
            min_weights = weights
        #print("loss", loss)

    return min_weights, min_loss

# Least squares regression using normal equations
def least_squares(y, tx):
    """calculate the least squares solution."""
    weights = np.linalg.solve(tx.T @ tx, tx.T @ y)
    return weights, compute_loss_mse(y, tx, weights)

# Ridge regression using normal equations
def ridge_regression(y, tx, lambda_):
    """implement ridge regression."""
    l = 2 * tx.shape[0] * lambda_
    weights = np.linalg.solve(tx.T @ tx + l * np.identity(tx.shape[1]), tx.T @ y)
    return weights, compute_loss_mse(y, tx, weights)

# Logistic regression using gradient descent
def logistic_regression(y, tx, initial_w, max_iters, gamma):
    """implements logistic regression using gradient descent."""
    w = initial_w

    # start the logistic regression
    for iter in range(max_iters):
        # get loss, gradient and update w.
        loss = calculate_loss_logistic_regression(y, tx, w)
        gradient = calculate_gradient_logistic_regression(y, tx, w)
        w = w - gamma * gradient

        # log info
        if iter % 100 == 0:
            print("Current iteration={i}, loss={l}".format(i=iter, l=loss))

    loss = calculate_loss_logistic_regression(y, tx, w)
    print("loss={l}".format(l=loss))
    return w, loss

# Regularized logistic regression using gradient descent
def reg_logistic_regression(y, tx, lambda_, initial_w, max_iters, gamma):
    """implements regularized logistic regression using gradient descent."""
    w = initial_w

    # start the logistic regression
    for iter in range(max_iters):
        # get loss, gradient and update w.
        loss = calculate_loss_reg_logistic_regression(y, tx, w, lambda_)
        gradient = calculate_gradient_reg_logistic_regression(y, tx, w, lambda_)
        w = w - gamma * gradient

        # log info
        if iter % 100 == 0:
            print("Current iteration={i}, loss={l}".format(i=iter, l=loss))
            print("Weights size:" + str(np.squeeze(w.T @ w)))

    loss = calculate_loss_reg_logistic_regression(y, tx, w, lambda_)
    print("loss={l}".format(l=loss))
    return w, loss

# Regularized logistic regression using SGD
def reg_logistic_regression_SGD(y, tx, lambda_, initial_w, max_iters, gamma):
    """implements regularized logistic regression using stochastic gradient descent."""
    w = initial_w
    min_weights = w
    min_loss = calculate_loss_reg_logistic_regression(y, tx, w, lambda_)

    # start the logistic regression
    for iter in range(max_iters):
        # get loss, gradient and update w.
        # stochastic -> select random element of y and tx
        r = np.random.randint(0, len(y))
        y_elem = np.array([y[r]])
        tx_elem = np.array([tx[r]])

        gradient = calculate_gradient_reg_logistic_regression(y_elem, tx_elem, w, lambda_)
        w = w - gamma * gradient

        # log info
        if iter % 10000 == 0:
            loss = calculate_loss_reg_logistic_regression(y, tx, w, lambda_)
            print("Current iteration={i}, loss={l}".format(i=iter, l=loss))
            print("weights size:" + str(np.squeeze(w.T @ w)))

    loss = calculate_loss_reg_logistic_regression(y, tx, w, lambda_)
    print("loss={l}".format(l=loss))
    return w, loss

# ----- Helper functions for logistic regression ----------------------------------------------------
def sigmoid(t):
    """apply sigmoid function on t."""
    return 1.0 / (1 + np.exp(-t))

def calculate_loss_logistic_regression(y, tx, w):
    """compute the cost by negative log likelihood."""
    # Note: this function takes y with values either 0 or 1
    return - np.squeeze((y.T @ np.log(sigmoid(tx @ w)) + (1 - y).T @ np.log(1 - sigmoid(tx @ w))))

def calculate_gradient_logistic_regression(y, tx, w):
    """compute the gradient of loss."""
    # Note: this function takes y with values either 0 or 1
    return tx.T @ (sigmoid(tx @ w) - y)

# ----- Helper functions for penalized logistic regression -------------------------------------------
def calculate_loss_reg_logistic_regression(y, tx, w, lambda_):
    """compute the cost by negative log likelihood."""
    # Here we use lambda as defined as in class !
    return calculate_loss_logistic_regression(y, tx, w) + lambda_ / 2 * np.squeeze(w.T @ w)

def calculate_gradient_reg_logistic_regression(y, tx, w, lambda_):
    """compute the gradient of loss."""
    # Here we use lambda as defined as in class !
    return calculate_gradient_logistic_regression(y, tx, w) + lambda_ * w

# ----- Additional section: Newton method ---------------------------------------------------------------------------
# Note this Newton does not have the regularization term, easy to add !
def calculate_hessian(y, tx, w):
    """return the hessian of the loss function."""
    # compute S matrix
    N = tx.shape[0]
    S = np.zeros((N,N))
    for n in range(N):
        sig = sigmoid(tx[n].T @ w)
        S[n, n] = sig * (1 - sig)
    H = tx.T @ S @ tx
    return H

def learning_by_newton_method(y, tx, w, lambda_, gamma):
    """
    Do one step on Newton's method.
    return the loss and updated w.
    """
    loss = calculate_loss_logistic_regression(y, tx, w) + lambda_ / 2 * np.squeeze(w.T @ w)
    gradient = calculate_gradient_logistic_regression(y, tx, w) + lambda_ * w
    hessian = calculate_hessian(y, tx, w) + lambda_
    w = w - gamma * np.linalg.inv(hessian) @ gradient
    return w, loss

def logistic_regression_newton(y, tx, lambda_, initial_w, max_iters, gamma):
    ''' Perform logistic regression with Newton's method '''
    w = initial_w

    # start the logistic regression
    for iter in range(max_iters):
        # get loss and update w.
        w, loss = learning_by_newton_method(y, tx, w, lambda_, gamma)
        # log info
        if iter % 100 == 0:
            print("Current iteration={i}, the loss={l}".format(i=iter, l=loss))
            print("Weights size:" + str(np.squeeze(w.T @ w)))

    loss = calculate_loss_logistic_regression(y, tx, w)
    print("loss={l}".format(l=loss))
    return w, loss


# --------------
# Code starts here

import numpy as np

# Code starts here

# Adjacency matrix
adj_mat = np.array([[0,0,0,0,0,0,1/3,0],
                   [1/2,0,1/2,1/3,0,0,0,0],
                   [1/2,0,0,0,0,0,0,0],
                   [0,1,0,0,0,0,0,0],
                  [0,0,1/2,1/3,0,0,1/3,0],
                   [0,0,0,1/3,1/3,0,0,1/2],
                   [0,0,0,0,1/3,0,0,1/2],
                   [0,0,0,0,1/3,1,1/3,0]])

# Compute eigenvalues and eigencevectrs
eigenvalues, eigenvectors = np.linalg.eig(adj_mat)

# Eigen vector corresponding to 1

eigen_1 = abs(eigenvectors[:,0])
# normalzing the vector

eigen_1 = eigen_1/np.linalg.norm(eigenvectors[:,0],1)

# most important page , finding maximum value within the eigenvector
print(eigen_1)
page = np.argmax(eigen_1) + 1

print(page)

# Code ends here

# --------------
# Code starts here

# Initialize stationary vector I
init_I = np.array([1,0,0,0,0,0,0,0])

print(init_I.shape)
print(adj_mat.shape)

# Perform iterations for power method
for i in range(10):
    init_I = np.dot(adj_mat,init_I)/np.linalg.norm(init_I,1)

power_value = np.max(init_I)

power_page = np.argmax(init_I) + 1 # np.where(np.max(adj_mat) == power_value)

print(power_value)
print(power_page)

# Code ends here

# --------------
# Code starts here

# New Adjancency matrix
new_adj_mat = np.array([[0,0,0,0,0,0,0,0],
                   [1/2,0,1/2,1/3,0,0,0,0],
                  [1/2,0,0,0,0,0,0,0],
                   [0,1,0,0,0,0,0,0],
                   [0,0,1/2,1/3,0,0,1/2,0],
                   [0,0,0,1/3,1/3,0,0,1/2],
                   [0,0,0,0,1/3,0,0,1/2],
                   [0,0,0,0,1/3,1,1/2,0]])

# Initialize stationary vector I
new_init_I = np.array([1,0,0,0,0,0,0,0])

# Perform iterations for power method
for i in range(10):
    #getting the dot product for adjacency matrix and stationary vector , also normalising the result
    new_init_I = np.dot(new_adj_mat,new_init_I)/np.linalg.norm(new_init_I,1)

print(new_init_I)
# Here we get pagerank value for 3rd webpage as zero. Is it not possible as it has incoming connections.
# Code ends here

# --------------
# Alpha value
alpha = 0.85

# Code starts here

# Modified adjancency matrix(new hyperlink matrix)
#Using G=αS+(1−α)n1 ,1 as the new hyperlink matrix and 1 is a n*n matrix whose all entries are 1.α can #take any value between 0 and 1.
G = alpha * new_adj_mat + (1 - alpha)*(1/len(new_adj_mat))*np.ones(new_adj_mat.shape)

# Initialize stationary vector I
final_init_I = np.array([1,0,0,0,0,0,0,0])

# Perform iterations for power method
# taking dot product of adjacency matrix and stationary vector and normalising the same
for i in range(1000):
    final_init_I = np.dot(G,final_init_I)/np.linalg.norm(final_init_I,1)

print(final_init_I)

# Code ends here


import numpy as np
import math

# Kinematics helper functions
def x_rotation(theta):
    """The 3x3 rotation matrix for a rotation of `theta` radians about the x-axis."""
    return np.array([
        [1, 0, 0],
        [0, math.cos(theta), -math.sin(theta)],
        [0, math.sin(theta), math.cos(theta)]])

def y_rotation(theta):
    """The 3x3 rotation matrix for a rotation of `theta` radians about the y-axis."""
    return np.array([
        [math.cos(theta), 0, math.sin(theta)],
        [0, 1, 0],
        [-math.sin(theta), 0, math.cos(theta)]])

def z_rotation(theta):
    """The 3x3 rotation matrix for a rotation of `theta` radians about the z-axis."""
    return np.array([
        [math.cos(theta), -math.sin(theta), 0],
        [math.sin(theta), math.cos(theta), 0],
        [0, 0, 1]])

def get_extrinsic_rotation(rpy):
    """Gets the extrinsic rotation matrix defined by roll about x, then pitch about y, then yaw
    about z. This is the rotation matrix used in URDF.

    Args:
        rpy: A numpy array containing the roll, pitch, and yaw angles in radians.

    Returns:
        A 3x3 numpy array for the resulting extrinsic rotation.
    """

    x_rot = x_rotation(rpy[0])
    y_rot = y_rotation(rpy[1])
    z_rot = z_rotation(rpy[2])
    return np.matmul(z_rot, np.matmul(y_rot, x_rot))

def inv_tf(tf):
    """Get the inverse of a homogeneous transform"""
    inv_tf = np.eye(4)
    inv_tf[0:3, 0:3] = np.transpose(tf[0:3, 0:3])
    inv_tf[0:3, 3] = -1.0 * np.matmul(np.transpose(tf[0:3, 0:3]), tf[0:3, 3])
    return inv_tf

def get_dh_frame(dh_params):
    """Get the tf for the given dh parameters."""
    d = dh_params[0]
    theta = dh_params[1]
    r = dh_params[2]
    alpha = dh_params[3]

    dh_frame = np.eye(4)

    dh_frame[0, 0] = math.cos(theta)
    dh_frame[0, 1] = -math.sin(theta) * math.cos(alpha)
    dh_frame[0, 2] = math.sin(theta) * math.sin(alpha)
    dh_frame[0, 3] = r * math.cos(theta)

    dh_frame[1, 0] = math.sin(theta)
    dh_frame[1, 1] = math.cos(theta) * math.cos(alpha)
    dh_frame[1, 2] = -math.cos(theta) * math.sin(alpha)
    dh_frame[1, 3] = r * math.sin(theta)

    dh_frame[2, 1] = math.sin(alpha)
    dh_frame[2, 2] = math.cos(alpha)
    dh_frame[2, 3] = d
    return dh_frame


import numpy as np

# B/B,B/W,W/W
#

# ways to produce black = 1 b/b,1 b/b, b/w 2 = 4 ways
# from those only 2 can produce black when turned ~ 2/4 = 0.5

# inspired by https://github.com/cavaunpeu/statistical-rethinking/blob/master/chapter-2/homework.R
# [b/b, b/w, w/w]
#in which ways can the cards produce 1st black
ways_producing_black = np.array([2., 2., 0.])

priors = ways_producing_black / ways_producing_black.sum()
print(priors)

# [b/b,b/w,w/w]
# in which ways can they then produce second black
ways_can_produce_black_on_other_side = np.array([2., 0., 0.])
posterior = ways_can_produce_black_on_other_side * priors / ways_can_produce_black_on_other_side.sum()
print(posterior)
print('prob that other side is black = ' + str(posterior[0]))
# inspired by https://rpubs.com/andersgs/my_solutions_chapter2_statrethink
# b1/b2, b1/w1, w1/w2
#
# card |pick|other side
#
# b1/b2| 1 | 1
# b2/b1| 1 | 1
# b1/w1| 1 | 0
# w1/b1| 0 | 0
# b1/w1| 1 | 0
# w1/b1| 0 | 0
# w1/w2| 0 | 0
# w2/w1| 0 | 0
# w1/w2| 0 | 0
# w2/w1| 0 | 0
# w1/w2| 0 | 0
# w2/w1| 0 | 0
#
# to produce black on other side 4
# from to produce black on the first side 2
# 2/4 ~ 0.5

#bayes theorem
# probability that the second side is black given that the first side is black
# P(2black|1black) = P(1black|2black)*P(2black)/P(1black)
# probability that a card has black on both sides is 2 card from 12 ways
# P(2black) = 2/12 = 1/6
# probability that a card has 1st black side from 12 producing ways of card settings
# is 4, so 4/12 = 1/3
# P(1black) = 1/3
# probability that 1st side is black given the second side is black, e.g.
# that both sides are black is 1
# P(1black|2black) = 1 (hm)
# 1*1/6 / 1/3 ~ 1/2 ~ 0.5


# Copyright 2021 Neuron-AI GitHub Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================

from math import log1p
from typing import Sequence

from easyneuron.exceptions.exceptions import DimensionsError
from numpy import array, sqrt

def mean_squared_error(x: Sequence, y: Sequence) -> float:
	"""Returns the mean squared error between x and y.

	Parameters
	----------
	x : Sequence
		Any sequence, with equivalent total number of items to y
	y : Sequence
		Any sequence, with equivalent total number of items to x

	Returns
	-------
	float
		The mean squared error.

	Raises
	------
	DimensionsError
		If the total number of items in x and y differ.
	"""
	x = array(x).reshape(1, -1)[0]
	y = array(y).reshape(1, -1)[0]

	if x.shape != y.shape:
		raise DimensionsError(
			f"x and y must have the same number of items in it, not {len(x.shape)} and {len(y.shape)}.")

	return sum((i - j)**2 for i, j in zip(x, y)) / len(x)

def mean_squared_log_error(x: Sequence, y: Sequence) -> float:
	"""Returns the mean squared logarithmic error between x and y.

	Parameters
	----------
	x : Sequence
		Any sequence, with equivalent total number of items to y
	y : Sequence
		Any sequence, with equivalent total number of items to x

	Returns
	-------
	float
		The mean squared logarithmic error.

	Raises
	------
	DimensionsError
		If the total number of items in x and y differ.
	"""
	x = array(x).reshape(1, -1)[0]
	y = array(y).reshape(1, -1)[0]

	if x.shape != y.shape:
		raise DimensionsError(
			"x and y must have the same number of items in it.")

	return sum(
		(log1p(i) - log1p(j))**2 # log error squared
		for i, j in zip(x, y)
	) / len(x)

def root_mean_squared_log_error(x: Sequence, y: Sequence) -> float:
	"""Returns the root mean squared logarithmic error between x and y.

	Parameters
	----------
	x : Sequence
		Any sequence, with equivalent total number of items to y
	y : Sequence
		Any sequence, with equivalent total number of items to x

	Returns
	-------
	float
		The root mean squared logarithmic error.

	Raises
	------
	DimensionsError
		If the total number of items in x and y differ.
	"""
	return sqrt(mean_squared_log_error(x, y)) # guard clauses in method call, none needed here

def mean_absolute_error(x: Sequence, y: Sequence) -> float:
	"""Returns the mean absolute error between x and y.

	Parameters
	----------
	x : Sequence
		Any sequence, with equivalent total number of items to y
	y : Sequence
		Any sequence, with equivalent total number of items to x

	Returns
	-------
	float
		The mean absolute error.

	Raises
	------
	DimensionsError
		If the total number of items in x and y differ.
	"""
	x = array(x).reshape(1, -1)[0]
	y = array(y).reshape(1, -1)[0]

	if x.shape != y.shape:
		raise DimensionsError(
			"x and y must have the same number of items in it.")

	return sum(abs(i - j) for i, j in zip(x, y)) / len(x)

def root_mean_squared_error(x: Sequence, y: Sequence) -> float:
	"""Returns the root mean squared error between x and y.

	Parameters
	----------
	x : Sequence
		Any sequence, with equivalent total number of items to y
	y : Sequence
		Any sequence, with equivalent total number of items to x

	Returns
	-------
	float
		The root mean squared error.

	Raises
	------
	DimensionsError
		If the total number of items in x and y differ.
	"""
	return sqrt(mean_squared_error(x, y))


import numpy as np

def _mean_squared_error(y_true, y_pred):
    if y_pred.ndim == 1:
        return 0.5*np.sum((y_true - y_pred)**2)
    return 0.5*np.sum((y_true - y_pred)**2)/y_pred.shape[0]

def _categorical_crossentropy(y_true, y_pred):
    epsilon = 1e-7
    if y_pred.ndim == 1:
        return -np.sum(y_true * np.log(y_pred + epsilon))
    return -np.sum(y_true * np.log(y_pred + epsilon))/y_pred.shape[0]

def categorical_accuracy(y_true, y_pred):
        y_pred = np.argmax(y_pred, axis=1)
        y_true = np.argmax(y_true, axis=1)
        return np.sum(y_pred == y_true) / float(y_true.shape[0])

class MSE:
    def loss(self, y_true, y_pred):
        return _mean_squared_error(y_true, y_pred)
    def grad(self, y_true, y_pred ):
        return y_pred - y_true

class CrossEntropy:
    def loss(self, y_true, y_pred):
        return _categorical_crossentropy(y_true, y_pred)

    def grad(self, y_true, y_pred):
        epsilon = 1e-7
        return -y_true/(y_pred + epsilon)


# -*- coding: utf-8 -*-
"""
Created on Mon Dec  9 15:54:48 2019

@author: Alexander Campbell
"""
from math import sin, pi
from numpy import exp, linspace
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp

def solve_analytical(Cd=1.0, r=10, H=8000, v0=20e3, theta=45, rho0=3000, dt=0.1):
    """
    Solves for the analytical solution of the simplified case. Also solves numerically
    using scipy function solve_ivp.
    ----------
    Cd : float, optional
        The drag coefficient
    r : float, optional
        the meteor radius (m)
    H : float, optional
        Atmospheric scale height (m)
    v0 : float, optional
        initial velocity (m/s)
    Ch : float, optional
        The heat transfer coefficient
    theta : float, optional
            angle (degrees)
    rho0 : float, optional
        Air density at zero altitude (kg/m^3)
    dt : float, optional
        timestep (s)

    Returns
    -------
    z : numpy array
        altitude (m)
    V : numpy array
       velocity (m/s)
    ivp_sol : numpy array
        contains columns of velocity, altitude and distance.
    """
    ## input variables ##
    #H = 8000 # atmospheric scale height
    #Cd = 1 # drag coefficient
    #r = 10 ## radius
    A = pi*r**2 # cross-sectional area
    P0 = 1.2 # air density at zero altitude
    #v0 = 20e3 # initial velocity
    #theta = 45 # trajectory angle from horizontal
    z0 = 100e3 # initial z-position
    x0 = 0
    theta = theta*pi/180 # convert to radians
    # calc mass:
    rho = rho0 # density
    vol = (4/3)*pi*r**3 # volume
    m = rho*vol # mass

    # timestepping parameters:
    t_0 = 0 # initisal time
    t_f = 17 # final times
    state_0 = np.array([v0, z0, x0]) # initial array for solver
    z = linspace(0,100000,100) # z array
    t = np.arange(t_0, t_f, dt) # time array

    def f(t, state):
        v, z, x = state
        f = np.zeros_like(state)
        f[0] = -Cd * P0* np.exp(-z/H) * A * v**2 / (2 * m)
        f[1] = -v*np.sin(theta)
        f[2] = v*np.cos(theta)
        return f

    # analytical solution:
    M = -H*Cd*A*P0/(2*m*sin(theta)) # collecting constant terms
    V = v0*exp(M*exp(-z/H) - M*exp(-z0/H)) # analytical solution
    # numerical solution:
    ivp_sol = solve_ivp(f, [t_0, t_f], state_0, t_eval = t)

    return z, V, ivp_sol


#! /usr/bin/env python

"""
File: centered_diff.py
Copyright (c) 2016 Chinmai Raman
License: MIT

Course: PHYS227
Assignment: 1.3
Date: Feb 12, 2016
Email: raman105@mail.chapman.edu
Name: Chinmai Raman
Description: Function for numberical differentiation and errors in approximation
"""

import numpy as np

def diff(f, x, h = 1e-5):
    """
    Approximates the derivative of a function at a given value of x.
    """
    return ((f(x + h) - f(x - h)) / (2.0 * h))

def test_diff():
    """
    Tests the diff function above.
    """
    f = lambda x: float(x**2)
    assert(abs(diff(f, 3) - 6.0) < 1e-10), "Failure."

def application():
    """
    Prints the errors of the approximations of the derivatives of given functions.
    """
    ex = lambda x: np.exp(x)
    ex2 = lambda x: np.exp(-2.0 * (x**2))
    cos = lambda x: np.cos(x)
    ln = lambda x: np.log(x)

    print("f(x)\t\tx\tf'(x)\tapproximation of f'(x)\terror")
    print("e^x\t\t0\t1\t" + str(diff(ex, 0, 0.01)) + "\t\t" + str(abs(diff(ex, 0, 0.01) - 1)))
    print("e^(-2x^2)\t0\t0\t" + str(diff(ex2, 0, 0.01)) + "\t\t\t" + str(abs(diff(ex2, 0, 0.01) - 0)))
    print("cos(x)\t\t2pi\t0\t" + str(diff(cos, 2 * np.pi, 0.01)) + "\t\t\t" + str(abs(diff(cos, 2 * np.pi, 0.01) - 0)))
    print("ln(x)\t\t1\t1\t" + str(diff(ln, 1, 0.01)) + "\t\t" + str(abs(diff(ln, 1, 0.01) - 1)))


import numpy as np
import scipy.optimize as so

"""
    Utilisation de la formule d'Euler pour la dérivation numérique
    Paramètres :
        f : la fonction à dériver
        epsilon :
"""

def derivative(f, epsilon=1e-6):
    def g(x):
        return (f(x+epsilon) - f(x)) / epsilon
    return g

"""
    Classe pour l'intégration numérique via la méthode des trapèzes
"""

class Trapezoid:
    """
        Constructeur
        Paramètres :
            f : la fonction à intégrer
            a : la borne inférieure
            b : la borne supérieure
            n : la valeur d'échantillonnage
    """
    def __init__(self, f, a, b, n):
        self.function = f
        self.a = a
        self.b = b
        self.n = n

    """
        Fonction de calcul de l'aire et de l'estimation de l'erreur
        Retour : Un tuple avec l'aire et l'erreur d'intégration
    """
    def compute(self):

        """
            Calcul de l'aire suivant la formule suivante

                Aire = h * [s + SOMME(1, n-1, f(xi))]

            1) on calcul la valeur d'une unité d'aire (ua) pour n (le pas) ua's dans l'intervalle [a; b]
            2) on calcul la valeur de f(x) avec x = (a + b) / 2, soit le milieu de la fonction dans l'intervalle [a; b]
            3) on calcul la valeur de f(x) avec x allant de 1 jusqu'à n avec un pas égal à h
        """
        h = (self.b - self.a) / self.n
        s = 0
        try:
            s = (self.function(self.a) + self.function(self.b)) / 2
        except ZeroDivisionError:
            print("Erreur : la fonction ne convèrge pas")
            return (0,0)

        for i in range(1, self.n):
            s += self.function(self.a + i * h)
        integration = s * h

        """
            Calcul de l'estimation de l'erreur en :

                1) on calcul la dérivée seconde de la fonction
                2) on cherche la  valeur de x telle que pour tout x' appartenant à l'intervalle [a; b] on a f(x') <= f(x)
                3) on calcul l'estimation de l'erreur via la formule suivante :

                    - [ (b - a)^3 / (n² / 12) ] * M2
        """
        second_derivative = derivative(derivative(self.function))
        m2 = so.fmin(lambda x: -second_derivative(x), 0)[0]
        error = -(np.power((self.b - self.a), 3) / (np.power(self.n, 2) * 12)) * second_derivative(m2)

        return integration, error


import numpy as np
import statistics as s
from scipy.stats import pearsonr
Xs = np.array([0.0339, 0.0423, 0.213, 0.257, 0.273, 0.273, 0.450, 0.503, 0.503, \
0.637, 0.805, 0.904, 0.904, 0.910, 0.910, 1.02, 1.11, 1.11, 1.41, \
1.72, 2.03, 2.02, 2.02, 2.02])

Ys = np.array([-19.3, 30.4, 38.7, 5.52, -33.1, -77.3, 398.0, 406.0, 436.0, 320.0, 373.0, \
93.9, 210.0, 423.0, 594.0, 829.0, 718.0, 561.0, 608.0, 1.04E3, 1.10E3, \
840.0, 801.0, 519.0])

N = 24

#sample covariance function
def cov(a, b):

    if len(a) != len(b):
        return

    a_mean = np.mean(a)
    b_mean = np.mean(b)

    sum=(np.add(a,-a_mean)*np.add(b,-b_mean)).sum()

    return sum/(len(a)-1)

print(cov(Xs,Ys))

#pearson correlation covariance(X, Y) / (stdv(X) * stdv(Y))
corr=cov(Xs,Ys)/(s.stdev(Xs) * s.stdev(Ys))
print(corr)

#pearson lib
corr2, _ = pearsonr(Xs, Ys)
print('Pearsons correlation: %.3f' % corr2)
bet1=corr2*(s.stdev(Ys)/s.stdev(Xs) )
print('beta1: %.3f' % bet1)

bet0=np.mean(Ys)-bet1*np.mean(Xs)
print('beta0: %.3f' % bet0)

#testing Xt = np.arange(0, 1.1, .1)
Xt = np.random.normal(-1, 1, 1000)
Yt = Xt**2

Xt_Yt_r_coeff = np.corrcoef(Xt,Yt)
corrx=pearsonr(Xt, Yt)
print(corrx)


'''
Helper functions that implement Gram-Schmidt:

    - orthogonalize: (params: v)
    - orthonormalize: (params: v)

    Returns an M x N np.array of the vectors processed via the Gram-Schmidt algorithm.

Args:

    - v: an M x N np.array of vectors, where the
         *rows* are the vectors, not the columns.

Usage:

    from blmath.geometry.gram_schmidt import gram_schmidt

    # ...

    o = gram_schmidt.orthogonalize(v)

    # do stuff with o...
'''

import numpy as np

def orthogonalize(v):
    o = []

    for vec in v:
        w = vec.copy()

        proj_cmp = sum(np.array([proj(u, w) for u in o]))
        w = w - proj_cmp

        o.append(w)

    return np.array(o)

def orthonormalize(v, indices=None):
    if indices is None:
        indices = range(len(v))

    res = orthogonalize(v)

    for idx in indices:
        v = res[idx]
        v /= np.linalg.norm(v)

    return res

def proj(u, v):
    '''
    Projection of u onto v. Helper method for the Gram-Schmidt algorithm.
    '''
    return (np.dot(u, v) / np.dot(u, u)) * u

def main():
    v = np.array([[3, 1], [2, 2]], dtype=float)

    print orthogonalize(v)
    print orthonormalize(v)

    v = np.array([[1., 1., 1.], [2., 1., 0.], [5., 1., 3.]])

    print orthogonalize(v)
    print orthonormalize(v)

if __name__ == '__main__':
    main()


from numpy import *
from pylab import *
from scipy import *

from datetime import datetime
import pdb

ion()
i = 1j

"""
A script demonstrating the method of smoothing data in the whole using
Fourier analysis. To run the script, download the data at
https://datamarket.com/data/set/22pw/monthly-lake-erie-levels-1921-1970#!ds=22pw&display=line
and put it in the same directory as this file.

The method is discussed in detail here:
http://dantaylor688.github.io/2015/01/28/smoothing-by-fourier-series/
"""
def fst(f):
    """
    Compute the Fourier sine series.
    """
    N = len(f)
    F = zeros(N)
    for k in range(N):
        F[k] = (2.0/N)*sum([f[a]*sin(k*a*pi/N) for a in range(1,N)])
    return F

def ifst(F):
    """
    Compute the inverse Fourier sine series.
    """
    N = len(F)
    f = zeros(N)
    for x in range(N):
        f[x] = sum([F[k]*sin(x*k*pi/N) for k in range(1,N)])
    return f

def ifst_trunc(F, nu):
    """
    Compute the inverse Fourier sine series using only nu terms. As
    discussed in the link above, the mere truncating of the series
    provides the smoothed fit to the data.
    """
    N = len(F)
    f = zeros(N)
    for x in range(N):
        f[x] = sum([F[k]*sin(x*k*pi/N) for k in range(1,nu)])
    return f

if __name__=="__main__":
    # load the data
    # a numpy array of tuples: array([('"1970-12"', 16.584), ... ])
    data = genfromtxt('sample-data/monthly-lake-erie-levels-1921-19.csv', delimiter=',',\
                      skip_header=1,dtype=None)
    # dates for plotting
    raw_x =  [datetime.strptime(dat[0],'"%Y-%m"') for dat in data[:-1]]

    # data is now just an array of numbers
    data = array([dat[1] for dat in data[:-1]])

    figure(1)
    plot(raw_x,data)
    title("Monthly Lake Erie Levels between 1921 - 1970")

    figure(2)
    plot(data)
    title("Data with Numeric x-values")

    # subtract a linear term so that our data fits an odd, continuous, and
    # smooth function
    x = arange(len(raw_x))
    mu = data[0] + (data[-1] - data[0])/(len(data) - 1)*x
    cdata = data - mu

    figure(3)
    plot(cdata)
    title("Removing the Linear Term")

    ## here is where we will smooth using Fourier Series
    F = fst(cdata)
    magF = abs(F)

    figure(4)
    plot(magF)
    title("Magnitude of Fourier Coefficients")

    # Just checking
    ff1 = ifst(F)

    figure(55)
    plot(cdata,label="Original")
    plot(ff1,label="Recovered")
    legend()
    title("Getting Back the Original")

    # Fit with sample of Fourier Coefficients
    fit_idx = r_[0:105]

    figure(5)
    plot(magF[fit_idx])
    title("Magnitude of Chosen Fourier Coefficients")

    ff = ifst_trunc(F,len(fit_idx))

    figure(6)
    plot(cdata,label="Original")
    plot(ff,label="Recovered")
    legend()
    title("Smoothed Fit")

    figure(7)
    plot(cdata-ff)
    title("Residuals")

    figure(8)
    plot(raw_x,data,label="Original")
    plot(raw_x,ff+mu,label="Recovered")
    legend()
    title("Smoothed Fit")


#Academic work of Numerical Differential Equations - Federal University of Minas Gerais
""" Utils """
import math, sys
import numpy as np
import matplotlib.pyplot as plt
from ipywidgets import interact, interactive
from scipy import integrate
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.colors import cnames
from matplotlib import animation

def PredictorCorrector(xt, yt, zt,n = 3500, T = 35):
    x = np.zeros(n+2)
    y = np.zeros(n+2)
    z = np.zeros(n+2)
    t = np.zeros(n+2)

    x[0] = 8.0
    y[0] = 1.0
    z[0] = 1.0
    t[0] = 0
    dt = T/float(n) #0.01

    x, y, z, t = RungeKutta4(xt, yt, zt)

    f0_dx = xt(x[0],y[0],z[0],t[0])
    f0_dy = yt(x[0],y[0],z[0],t[0])
    f0_dz = zt(x[0],y[0],z[0],t[0])

    f1_dx = xt(x[1],y[1],z[1],t[1])
    f1_dy = yt(x[1],y[1],z[1],t[1])
    f1_dz = zt(x[1],y[1],z[1],t[1])

    f2_dx = xt(x[2],y[2],z[2],t[2])
    f2_dy = yt(x[2],y[2],z[2],t[2])
    f2_dz = zt(x[2],y[2],z[2],t[2])

    f3_dx = xt(x[3],y[3],z[3],t[3])
    f3_dy = yt(x[3],y[3],z[3],t[3])
    f3_dz = zt(x[3],y[3],z[3],t[3])

    for k in range(n-1,0,-1):
        #Predictor: The fourth-order Adams-Bashforth technique, an explicit four-step method:
        x[k+1] = x[k] + (dt/24) *(55*f3_dx - 59*f2_dx + 37*f1_dx - 9*f0_dx)
        y[k+1] = y[k] + (dt/24) *(55*f3_dy - 59*f2_dy + 37*f1_dy - 9*f0_dy)
        z[k+1] = z[k] + (dt/24) *(55*f3_dz - 59*f2_dz + 37*f1_dz - 9*f0_dz)

        f4_dx = xt(x[k+1],y[k+1],z[k+1],t[k+1])
        f4_dy = yt(x[k+1],y[k+1],z[k+1],t[k+1])
        f4_dz = zt(x[k+1],y[k+1],z[k+1],t[k+1])

        #Corrector: The fourth-order Adams-Moulton technique, an implicit three-step method:
        x[k+1] = x[k] + (dt/24) *(9*xt(x[k+1],y[k+1],z[k+1],t[k+1]) + 19*f3_dx - 5*f2_dx + f1_dx)
        y[k+1] = y[k] + (dt/24) *(9*yt(x[k+1],y[k+1],z[k+1],t[k+1]) + 19*f3_dy - 5*f2_dx + f1_dy)
        z[k+1] = z[k] + (dt/24) *(9*yt(x[k+1],y[k+1],z[k+1],t[k+1]) + 19*f3_dz - 5*f2_dx + f1_dz)

    return x, y, z, t


import numpy as np
from scipy import linalg

def get_spectral_radius(W):
    """Given a squarred weight matrix (e.g. the recurrent reservoir matrix),
    returns the spectral radius"""
    return max(abs(linalg.eig(W)[0]))

def compute_error_NRMSE(teacher_signal, predicted_signal, verbose=False):
    """ Computes Normalized Root-Mean-Squarred Error between a teacher signal and a predicted signal
    Return the errors in this order: nmrse mean, nmrse max-min, rmse, mse.
    By default, only NMRSE mean should be considered as a general measure to be
    compared for different datasets.

    For more information, see:
    - Mean Squared Error https://en.wikipedia.org/wiki/Mean_squared_error
    - Root Mean Squared Error https://en.wikipedia.org/wiki/Root-mean-square_deviation for more info
    """
    mse = np.mean((teacher_signal - predicted_signal)**2)
    rmse = np.sqrt(mse)
    nmrse_mean = abs(rmse / np.mean(test_out[:])) # Normalised RMSE (based on mean)
    nmrse_maxmin = rmse / abs(np.max(test_out[:]) - np.min(test_out[:])) # Normalised RMSE (based on max - min)
    if verbose:
        print("Errors computed over %d time steps" % (errorLen))
        print("\nMean Squared error (MSE):\t\t%.4e" % (mse) )
        print("Root Mean Squared error (RMSE):\t\t%.4e\n" % rmse )
        print("Normalized RMSE (based on mean):\t%.4e" % (nmrse_mean) )
        print("Normalized RMSE (based on max - min):\t%.4e" % (nmrse_maxmin) )
    return nmrse_mean, nmrse_maxmin, rmse, mse


"""
Profit when Betting

Example from the book: Mathematics for Finance by Donald G. Sarri

Odds
Game: Vikings - Packers

Bob 25-1 (Vikings win)

Sue 1.20-1 (Packers win)

Assumptions:
- Money to bet: $100.00
    x: amount to bet on the Packers
    100 - x: amount to bet on the Vikings

If Packers win:
25x - (100 - x) --> 26x - 100
solving x:
    x = 3.85

If Vikings win:
1.20(100 - x) - x --> 2.20x = 120
solving x:
    x = 54.55

3.85 < x < 54.55

x = Symbol('x')
a = solve(x*2.20 - 120, x)
type(a) is a list with the answer of the equation

Links:
https://pythonforundergradengineers.com/sympy-two-equations-for-two-unknows-and-statics-problem.html

https://www.google.com/search?channel=fs&client=ubuntu&q=sympy
"""

from sympy.solvers import solve
from sympy import Symbol
import math

class Bets:
    def __init__(self, principal, o1, o2):
        self.principal = principal
        self.o1 = o1
        self.o2 = o2
    def solution(self, option1 = None, option2=None):
        if option1 == True and option2 == None:
            x = Symbol('x')
            solution = solve(x*self.o1 - (self.principal - x), x)
        elif option1 == None and option2 == True:
            x = Symbol('x')
            solution = solve(self.o2*(self.principal - x) - x,x)
        else:
            print("Error")
        return solution
    def solution_o1(self):
        x = Symbol('x')
        self.sol_o1 = solve(self.o1*x - (self.principal - x), x)
        for i in self.sol_o1:
            self.sol_o1 = float(i)
        return self.sol_o1
    def solution_o2(self):
        x = Symbol('x')
        self.sol_o2 = solve(self.o2 * (self.principal - x) - x, x)
        for i in self.sol_o2:
            self.sol_o2 = i
        return self.sol_o2
    def try_o1(self):
        res = []
        ind = []
        for i in range(math.floor(self.sol_o1),math.floor(self.sol_o2), 5):
            eq = self.o1 * i - self.principal
            res.append(eq)
            ind.append(i)
        ans = zip(ind, res)
        return ans

# Test
test = Bets(principal = 100, o1 = 25, o2 = 1.20)
#print(test.solution(option1 = None, option2 = True))
test.solution_o1()
test.solution_o2()

print(*test.try_o1())


"""
    In England the currency is made up of pound, £, and pence, p, and there are eight coins in general circulation:

        1p, 2p, 5p, 10p, 20p, 50p, £1 (100p) and £2 (200p).

    It is possible to make £2 in the following way:

        1×£1 + 1×50p + 2×20p + 1×5p + 1×2p + 3×1p

    How many different ways can £2 be made using any number of coins?
"""

import numpy as np

coefs = np.array([1, 2, 5, 10, 20, 50, 100, 200])

# coefs = np.array([1, 2, 5])
num_coef = coefs.size

def find_combs(rez=200, start_idx=0):
    global coefs, num_coef
    found_combis = []
    if start_idx < num_coef:
        mult = 0
        while mult*coefs[start_idx] <= rez:
            for comb2 in find_combs(rez-mult*coefs[start_idx], start_idx+1):
                # merge combinations
                comb2[start_idx] = mult
                if (coefs*comb2).sum() == rez:
                    # print("%d =" % rez),
                    # print(comb2)
                    found_combis.append(comb2)
            mult += 1
    else:
        found_combis.append([0]*num_coef)
    return found_combis

sol = find_combs(200)
print(sol)
print(len(sol)) # 73682


# GoldBach Conjecture
import math

from sympy import Range

MAX = 10 ** 5
primes: list[int] = []

def sieveSundaram() -> None:
    marked = [False] * (int(MAX / 2) + 100)
    for i in Range(1, int((math.sqrt(MAX) - 1) / 2) + 1):
        for j in range((i * (i + 1)) << 1, int(MAX / 2) + 1, 2 * i + 1):
            marked[j] = True

    primes.append(2)

    for i in Range(1, int(MAX / 2) + 1):
        if marked[i] is False:
            primes.append(2 * i + 1)

def findPrimes(n: int) -> None:
    if n <= 2 or n % 2 != 0:
        print("Even Number")
        return
    i: int = 0
    while primes[i] <= n // 2:
        diff = n - primes[i]
        if diff in primes:
            print(primes[i], "+", diff, "=", n)
            return
        i += 1

if __name__ == "__main__":
    sieveSundaram()
    findPrimes(int(input("Enter a number: ")))


import random,math
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats

measurements=np.array([4.72, 9.11, 5.57, 7.66, 4.69, 4.86, 6.51, 6.65, 4.77, 0.26, 4.67, 6.37, 5.05, 5.91, 5.54, 2.13, 6.77, 3.84, 4.0, 6.18, 3.54, 5.52, 3.29, 4.62, 4.25, 4.08, 3.37, 4.91, 5.24, 6.85, 3.85, 5.11, 9.41, 5.78, 5.97, 5.87, 1.59, 4.51, 0.72, 6.72, 8.4, 3.94, 8.53, 2.76, 2.62, 6.11, 3.37, 4.01, 7.17, 3.05])
N=10000
def bootstrap(Y,size,mode):

    Ystar = np.empty(size)

    # Bootstraping
    for i in range(size):
        samples = np.random.choice(Y,size=len(Y))
        if mode == 'mean':
            Ystar[i] = np.mean(samples)
        if mode == 'std':
            Ystar[i] = np.std(samples,ddof=1)

    return Ystar

if True:
	bootstrap_sample = bootstrap(measurements,N,'mean')

	CI_95 = np.percentile(bootstrap_sample,[2.5,97.5])

	bins = np.linspace(4, 6, 11)
	# Bootstrap PDF Plot
	plt.hist(bootstrap_sample,density=True,color='green',bins=np.arange(min(measurements), max(measurements) + .1, .1))

	# 95% confidence interval
	plt.axvline(x=np.percentile(bootstrap_sample,[2.5]), ymin=0, ymax=1,label='2.5th percentile',color='black',linestyle='dashed')
	plt.axvline(x=np.percentile(bootstrap_sample,[97.5]), ymin=0, ymax=1,label='97.5th percentile',color='red',linestyle='dashed')
	plt.axvline(x=np.percentile(bootstrap_sample,[50]), ymin=0, ymax=1,label='mean',color='yellow',linestyle='dashed')

	plt.xlabel("$\overline {X}$")
	plt.ylabel("Frequency")
	plt.title(r'Bootstrap : $\overline {X}$'+"= {0:.3f} ; 95 % C.I = [{1:.3f}, {2:.3f}]".format(np.mean(bootstrap_sample),CI_95[0],CI_95[1]))
	plt.legend()
	plt.grid()
	plt.xlim([3.8, 6.2])
	plt.show()

if True:
	bootstrap_sample = bootstrap(measurements,N,'std')

	CI_95 = np.percentile(bootstrap_sample,[2.5,97.5])

	bins = np.linspace(4, 6, 11)
	# Bootstrap PDF Plot
	plt.hist(bootstrap_sample,density=True,color='green',bins=np.arange(min(measurements), max(measurements) + .1, .1))

	# 95% confidence interval
	plt.axvline(x=np.percentile(bootstrap_sample,[2.5]), ymin=0, ymax=1,label='2.5th percentile',color='black',linestyle='dashed')
	plt.axvline(x=np.percentile(bootstrap_sample,[97.5]), ymin=0, ymax=1,label='97.5th percentile',color='red',linestyle='dashed')
	plt.axvline(x=np.percentile(bootstrap_sample,[50]), ymin=0, ymax=1,label='std',color='yellow',linestyle='dashed')

	plt.xlabel("$\overline {\sigma}$")
	plt.ylabel("Frequency")
	plt.title(r'Bootstrap : $\overline {\sigma}$'+"= {0:.3f} ; 95 % C.I = [{1:.3f}, {2:.3f}]".format(np.mean(bootstrap_sample),CI_95[0],CI_95[1]))
	plt.legend()
	plt.grid()
	plt.xlim([1, 2.7])
	plt.show()

if True:
	# Bayesian 1:
	mean, var, std = stats.bayes_mvs(measurements)
	print(mean,'\n',std)

	# Bayesian Model 2:

	import pymc3 as pm

	model = pm.Model()

	if __name__ == '__main__':

	    with model:

		    # Prior
	        mu = pm.Normal('mu',4)
	        sigma = pm.Normal('sigma', 1.5)

		    # Likelihood function.
	        y_obs = pm.Normal('y_obs', mu=mu,sd=sigma, observed=measurements)

		    # Try 330 draws and 3 chains.
	        trace = pm.sample(draws=330, chains=3)

	    pm.traceplot(trace)
	    pm.plot_posterior(trace)

	plt.show()


import numpy as np

arr = np.array([1.1, 1.4, 1.5, 1.6, 1.9, -1.1, -1.4, -1.5, -1.6, -1.9])

# 1.
print(np.trunc(arr))

# 2.
print(np.fix(arr))

# 3.
print(np.around(arr))

# 4.
print(np.floor(arr))

# 5.
print(np.ceil(arr))


"""
    [summary] Chapter 2.
    > poetry run python src/chap2.py
"""

import numpy as np

def AND_nobias(x1: int, x2: int) -> int:
    """
    [summary] AND operator wituout bias term

    Args:
        x1 (int): 1st input for AND
        x2 (int): 2nd input for AND

    Returns:
        int: result of AND operator
    """

    (w1, w2, theta) = (0.5, 0.5, 0.7)
    tmp = w1 * x1 + w2 * x2
    if tmp <= theta:
        return 0
    else:
        return 1

def AND(x1: int, x2: int) -> int:
    """
    [summary] AND operator with bias term

    Args:
        x1 (int): 1st input for AND
        x2 (int): 2nd input for AND

    Returns:
        int: result of AND operator
    """

    x = np.array([x1, x2])
    w = np.array([0.5, 0.5])
    b = -0.7
    # w1 * x1 + w2 * x2 + b is condition
    tmp = np.sum(w * x) + b
    if tmp <= 0:
        return 0
    else:
        return 1

def NAND(x1: int, x2: int) -> int:
    """
    [summary] NAND Operator

    Args:
        x1 (int): 1st input
        x2 (int): 2md input

    Returns:
        int: output of NAND
    """

    x = np.array([x1, x2])
    w = np.array([-0.5, -0.5])
    b = 0.7
    tmp = np.sum(w * x) + b
    if tmp <= 0:
        return 0
    else:
        return 1

def OR(x1: int, x2: int) -> int:
    """
    [summary] OR Operator

    Args:
        x1 (int): 1st input
        x2 (int): 2nd input

    Returns:
        int: output of OR operator
    """
    x = np.array([x1, x2])
    w = np.array([0.5, 0.5])
    b = -0.1
    tmp = np.sum(w * x) + b
    if tmp <= 0:
        return 0
    else:
        return 1

def XOR(x1: int, x2: int) -> int:
    """
    [summary] XOR operator

    Args:
        x1 (int): 1st input
        x2 (int): 2nd input

    Returns:
        int: output of XOR
    """
    return AND(NAND(x1, x2), OR(x1, x2))

if __name__ == "__main__":
    # check AND operator
    assert AND(0, 0) == 0
    assert AND(0, 1) == 0
    assert AND(1, 0) == 0
    assert AND(1, 1) == 1

    # check NAND operator
    assert NAND(0, 0) == 1
    assert NAND(0, 1) == 1
    assert NAND(1, 0) == 1
    assert NAND(1, 1) == 0

    # check OR operator
    assert OR(0, 0) == 0
    assert OR(0, 1) == 1
    assert OR(1, 0) == 1
    assert OR(1, 1) == 1

    # check XOR operator
    assert XOR(0, 0) == 0
    assert XOR(0, 1) == 1
    assert XOR(1, 0) == 1
    assert XOR(1, 1) == 0


import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

sns.set(style='whitegrid', context='notebook')

def least_squares(b0, b1, points):
    total_error = 0
    n = float(len(points))
    for x, y in points:
        total_error += (y - (b1 * x + b0)) ** 2
    return total_error / 2. * n

def step_gradient(b0_current, b1_current, points, learningRate):
    b0_gradient = 0
    b1_gradient = 1
    n = float(len(points))
    for x, y in points:
        b0_gradient += (1 / n) * (y - ((b1_current * x) + b0_current))
        b1_gradient += (1 / n) * x * (y - ((b1_current * x) + b0_current))
    new_b0 = b0_current + (learningRate * b0_gradient)
    new_b1 = b1_current + (learningRate * b1_gradient)
    return [new_b0, new_b1, least_squares(new_b0, new_b1, points)]

def run_gradient_descent(points, starting_b0, starting_b1, learning_rate, num_iterations):
    b0 = starting_b0
    b1 = starting_b1
    slope = []
    intersect = []
    error = []
    for i in range(num_iterations):
        b0, b1, e = step_gradient(b0, b1, np.array(points), learning_rate)
        slope.append(b1)
        intersect.append(b0)
        error.append(e)
    return [b0, b1, e, slope, intersect, error]

# This process could prove inefficient when the convergence rate is high, wasting CPU iterations.
# A more clever stop condition would consist of adding an acceptable error value, which would stop the iteration.

iris = sns.load_dataset('iris')
X = iris['petal_width'].tolist()
Y = iris['petal_length'].tolist()
points = np.dstack((X, Y))[0]

learning_rate = 0.0001
initial_b0 = 0
initial_b1 = 0
num_iterations = 1000
[b0, b1, e, slope, intersect, error] = run_gradient_descent(points, initial_b0, initial_b1, learning_rate,
                                                            num_iterations)
plt.figure(figsize=(7, 5))
plt.scatter(X, Y)
xr = np.arange(0, 3.5)
plt.plot(xr, (xr * b1) + b0)
plt.title('Regression, alpha= 0.001, initial values=(0,0), it=1000')
plt.show()
# We are far from what we are looking for, so take a look at errors

plt.figure(figsize=(7, 5))
xr = np.arange(0, 1000)
plt.plot(xr, np.array(error).transpose())
plt.title("Error for 1000 iterations")
plt.show()
# the process seems to be working, but a little slow, we can try to increase the step by a factor of 10 to see if
# it converges quickly

learning_rate = 0.001  # This is the updated value
initial_b0 = 0
initial_b1 = 0
num_iterations = 1000
[b0, b1, e, slope, intersect, error] = run_gradient_descent(points, initial_b0, initial_b1, learning_rate,
                                                            num_iterations)
plt.figure(figsize=(7, 5))
xr = np.arange(0, 1000)
plt.plot(xr, np.array(error).transpose())
plt.title("Error for 1000 iterations, increased step by tenfold")
plt.show()
# This is more alike to what we are looking

plt.figure(figsize=(7, 5))
plt.scatter(X, Y)
xr = np.arange(0, 3.5)
plt.plot(xr, (xr * b1) + b0)
plt.title("Regression, alpha= 0.01, initial values = (0,0), it=1000")
plt.show()

# To go faster we my try this:
# but this end up to a bad move, due to error now tends to infinity
learning_rate = 0.85  # last one was 0.0001
initial_b0 = 0
initial_b1 = 0
num_iterations = 1000
[b0, b1, e, slope, intersect, error] = run_gradient_descent(points, initial_b0, initial_b1, learning_rate,
                                                            num_iterations)
plt.figure(figsize=(7, 5))
xr = np.arange(0, 1000)
plt.plot(xr, np.array(error).transpose())
plt.title('Error for 1000 iterations, big step')

# No trying with semi-random initial parameters
learning_rate = 0.001  # Same as last time
initial_b0 = 0.8  # pseudo random value
initial_b1 = 1.5  # pseudo random value
num_iterations = 1000
[b0, b1, e, slope, intersect, error] = run_gradient_descent(points, initial_b0, initial_b1, learning_rate,
                                                            num_iterations)
plt.figure(figsize=(7, 5))
xr = np.arange(0, 1000)
plt.plot(xr, np.array(error).transpose())
plt.title('Error for 1000 iterations, step 0.001, random initial parameter values')
plt.show()
# As you can see, even if you have the same sloppy error rate, the initial error value decreases tenfold
# (from 2e5 to 2e4). Now let's try a final technique to improve the convergence of the parameters based on the
# normalization of the input values.

learning_rate = 0.001  # Same as last time
initial_b0 = 0.8  # pseudo random value
initial_b1 = 1.5  # pseudo random value
num_iterations = 1000
x_mean = np.mean(points[:, 0])
y_mean = np.mean(points[:, 1])
x_std = np.std(points[:, 0])
y_std = np.std(points[:, 1])

X_normalized = (points[:, 0] - x_mean) / x_std
Y_normalized = (points[:, 1] - y_mean) / y_std

plt.figure(figsize=(7, 5))
plt.scatter(X_normalized, Y_normalized)
plt.show()
# Now that we have this set of clean and tidy data, let's try again with the last slow convergence parameters,
# and see what happens to the error minimization speed:

points = np.dstack((X_normalized, Y_normalized))[0]
learning_rate = 0.001  # Same as last time
initial_b0 = 0.8  # pseudo random value
initial_b1 = 1.5  # pseudo random value
num_iterations = 1000
[b0, b1, e, slope, intersect, error] = run_gradient_descent(points, initial_b0, initial_b1, learning_rate,
                                                            num_iterations)
plt.figure(figsize=(7, 5))
xr = np.arange(0, 1000)
plt.plot(xr, np.array(error).transpose())
plt.title('Error for 1000 iterations, step 0.001, random initial parameter values, normalized initial values')
plt.show()
# A very good starting point indeed! Just by normalizing the data, we have half the initial error values, and the error
# went down 20% after 1,000 iterations. The only thing we have to remember is to denormalize after we have the results,
# in order to have the initial scale and data center. So, that's all for now on gradient descent. We will be revisiting
# it in the next chapters for new challenges.


# used for manipulating directory paths
import os

# Scientific and vector computation for python
import numpy as np

# Plotting library
from matplotlib import pyplot
from mpl_toolkits.mplot3d import Axes3D  # needed to plot 3-D surfaces

def featureNormalize(X):
    """
    Normalizes the features in X. returns a normalized version of X where
    the mean value of each feature is 0 and the standard deviation
    is 1. This is often a good preprocessing step to do when working with
    learning algorithms.

    Parameters
    ----------
    X : array_like
        The dataset of shape (m x n).

    Returns
    -------
    X_norm : array_like
        The normalized dataset of shape (m x n).

    Instructions
    ------------
    First, for each feature dimension, compute the mean of the feature
    and subtract it from the dataset, storing the mean value in mu.
    Next, compute the  standard deviation of each feature and divide
    each feature by it's standard deviation, storing the standard deviation
    in sigma.

    Note that X is a matrix where each column is a feature and each row is
    an example. You needto perform the normalization separately for each feature.

    Hint
    ----
    You might find the 'np.mean' and 'np.std' functions useful.
    """
    # You need to set these values correctly
    X_norm = X.copy()
    mu = np.zeros(X.shape[1])
    sigma = np.zeros(X.shape[1])

    mu = np.mean(X_norm, axis=0)
    X_norm = X_norm - mu

    sigma = np.std(X, axis=0)

    X_norm = X_norm/sigma

    return X_norm, mu, sigma

if __name__ == '__main__':
    data = np.loadtxt(os.path.join("/home/thelichking/Desktop/ml-coursera-python-assignments/Exercise1/Data",
                                   "ex1data2.txt"), delimiter=',')
    X = data[:, :3]  # the first two rows
    y = data[:, 2]  # the actual 3rd row
    m = y.size

    X_norm, mu, sigma = featureNormalize(X)
    print(mu, sigma)


"""
Filename: log_linear_growth_model.py
Authors: John Stachurski, Thomas Sargent

The log linear growth model, wrapped as classes.  For use with the
optgrowth.py module.
"""

import numpy as np
import matplotlib.pyplot as plt
from optgrowth import bellman_operator
from quantecon import compute_fixed_point
from joblib import Memory

memory = Memory(cachedir='./joblib_cache')

@memory.cache
def compute_value_function_cached(grid, beta, alpha, shocks):
    """
    Compute the value function by iterating on the Bellman operator.
    The work is done by QuantEcon's compute_fixed_point function.
    """
    Tw = np.empty(len(grid))
    initial_w = 5 * np.log(grid) - 25

    v_star = compute_fixed_point(bellman_operator,
            initial_w,
            1e-4,  # error_tol
            100,   # max_iter
            True,  # verbose
            5,     # print_skip
            grid,
            beta,
            np.log,
            lambda k: k**alpha,
            shocks,
            Tw=Tw,
            compute_policy=False)
    return v_star

class LogLinearGrowthModel:
    """
    Stores parameters and computes solutions for the basic log utility / Cobb
    Douglas production growth model.  Shocks are lognormal.
    """
    def __init__(self,
            alpha=0.65,         # Productivity parameter
            beta=0.95,          # Discount factor
            mu=1,               # First parameter in lognorm(mu, sigma)
            sigma=0.1,          # Second parameter in lognorm(mu, sigma)
            grid_max=8,
            grid_size=150):

        self.alpha, self.beta, self.mu, self.sigma = alpha, beta, mu, sigma
        self.grid = np.linspace(1e-6, grid_max, grid_size)
        self.shocks = np.exp(mu + sigma * np.random.randn(250))

    def compute_value_function(self, show_plot=False):
        """
        Calls compute_value_function_cached and optionally adds a plot.
        """
        v_star = compute_value_function_cached(self.grid,
                                                   self.beta,
                                                   self.alpha,
                                                   self.shocks)
        if show_plot:
            fig, ax = plt.subplots()
            ax.plot(self.grid, v_star, lw=2, alpha=0.6, label='value function')
            ax.legend(loc='lower right')
            plt.show()

        return v_star

    def compute_greedy(self, w=None, show_plot=False):
        """
        Compute the w-greedy policy on the grid points given w
        (the value of the input function on grid points).  If w is not
        supplied, use the approximate optimal value function.
        """
        if w is None:
            w = self.compute_value_function()

        Tw, sigma = bellman_operator(w,
                                    self.grid,
                                    self.beta,
                                    np.log,
                                    lambda k: k**self.alpha,
                                    self.shocks,
                                    compute_policy=True)

        if show_plot:
            fig, ax = plt.subplots()
            ax.plot(self.grid, sigma, lw=2, alpha=0.6, label='approximate policy function')
            cstar = (1 - self.alpha * self.beta) * self.grid
            ax.plot(self.grid, cstar, lw=2, alpha=0.6, label='true policy function')
            ax.legend(loc='upper left')
            plt.show()

        return sigma


import numpy as np
import matplotlib.pyplot as plt

# y' = u
# u' = -y

"""
y'' + y = 0

y(0) = 0 and y'(0) = 1/pi
"""

def F(y, u, x):
    return -y

a = 0
b = 2 * np.pi
N = 100
h = (b - a) / N

xpoints = np.arange(a, b, h)
ypoints = []
upoints = []

y = 0.0
u = 1.0 / np.pi

for x in xpoints:
    ypoints.append(y)
    upoints.append(u)

    m1 = h * u
    k1 = h * F(y, u, x)  # (x, v, t)

    m2 = h * (u + 0.5 * k1)
    k2 = h * F(y + 0.5 * m1, u + 0.5 * k1, x + 0.5 * h)

    m3 = h * (u + 0.5 * k2)
    k3 = h * F(y + 0.5 * m2, u + 0.5 * k2, x + 0.5 * h)

    m4 = h * (u + k3)
    k4 = h * F(y + m3, u + k3, x + h)

    y += (m1 + 2 * m2 + 2 * m3 + m4) / 6
    u += (k1 + 2 * k2 + 2 * k3 + k4) / 6

plt.plot(xpoints, ypoints)
# plt.xlim([0,4])
# plt.xlim([0,4])
plt.show()